Merge branch 'develop'

This commit is contained in:
Simon Brooke 2023-01-05 12:45:28 +00:00
commit d515763cda
No known key found for this signature in database
GPG key ID: A7A4F18D1D4DF987
5 changed files with 91 additions and 38 deletions

View file

@ -62,21 +62,21 @@ For example:
(get-message :pipe "de-DE" "i18n" "ru") (get-message :pipe "de-DE" "i18n" "ru")
``` ```
So how does this work? When one calls `(get-message token accept-language-header)`, how does it know where to find resources? The answer is that there are two dynamic variables: So how does this work? When one calls
`(get-message token accept-language-header)`, how does it know where to find resources? The answer is that there is a `*config*` map, with (currently) two significant keys:
* `*resource-path*`, the default path within the resources space on which * `:resource-path`, whose value should be a string representation of the default
translation files will be sought. Initialised to `i18n`. path within the resources space on which translation files will be sought. Initialised to `i18n`.
* `*default-language*`, the language tag for the language to use when no * `:default-language`, the language tag for the language to use when no
otherwise suitable language can be identified. Initialised to the default otherwise suitable language can be identified. Initialised to the default
language of the runtime session, so this may well be different on your language of the runtime session, so this may well be different on your
machine from someone elses running identical software. machine from someone elses running identical software.
Thus Thus
```clojure ```clojure
(binding [*resource-path* "language-files" (binding [*config* {:resource-path "language-files"
*default-language* "en-CA"] :default-language "en-CA"}]
(get-message :pipe "en-GB;q=0.9, fr-FR") (get-message :pipe "en-GB;q=0.9, fr-FR"))
)
``` ```
and and
```clojure ```clojure
@ -116,10 +116,27 @@ In this project you will find two very simple example files, which should give y
## Documentation ## Documentation
Documentation may be generated by running Documentation can be found here. It may be generated by running
lein codox lein codox
## Future direction
It's likely that in future configuration will be extended
1. To read per-language keys/messages from CSV files;
2. To read per-language keys/messages from database tables;
3. potentially, to read per-language keys/messages from other sources.
Pull requests implementing any of these things will be welcomed.
## Deprecated features
There are still two dynamic configuration variables, `*default-language*`
and `*resource-path*`, but these are now superceded by the `*config*` map,
which is extensible. Consequently, if you are using these configuration
variables in production, you should bind `*config*` to `nil`.
## License ## License
Copyright © 2017 Simon Brooke Copyright © 2017 Simon Brooke

View file

@ -1,4 +1,4 @@
(defproject org.clojars.simon_brooke/internationalisation "1.0.4" (defproject org.clojars.simon_brooke/internationalisation "1.0.5-SNAPSHOT"
:cloverage {:output "docs/cloverage" :cloverage {:output "docs/cloverage"
:codecov? true :codecov? true
:emma-xml? true} :emma-xml? true}

View file

@ -1,3 +1,3 @@
;;;; This is a British English translation file. ;;;; This is a British English translation file.
{:pipe "This is not a pipe"} {:pipe "This is not a pipe."}

View file

@ -22,13 +22,18 @@
(def ^:dynamic *resource-path* (def ^:dynamic *resource-path*
"The default path within the resources space on which translation files "The default path within the resources space on which translation files
will be sought." will be sought. Deprecated, prefer `(:resource-path *config*)`."
"i18n") "i18n")
(def ^:dynamic *default-language* (def ^:dynamic *default-language*
"The default language to seek." "The default language to seek. Deprecated, prefer `(:default-language *config*)`."
(-> (locale/get-default) locale/to-language-tag)) (-> (locale/get-default) locale/to-language-tag))
(def ^:dynamic *config*
"Extensible configuration for i18n."
{:default-language (-> (locale/get-default) locale/to-language-tag)
:resource-path "i18n"})
(def accept-language-grammar (def accept-language-grammar
"Grammar for `Accept-Language` headers" "Grammar for `Accept-Language` headers"
"HEADER := SPECIFIER | SPECIFIERS; "HEADER := SPECIFIER | SPECIFIERS;
@ -119,7 +124,7 @@
(try (try
(slurp (io/resource name)) (slurp (io/resource name))
(catch Exception _ (catch Exception _
(timbre/error (str "Resource at " name " does not exist.")) (timbre/warn (str "Resource at " name " does not exist."))
nil))) nil)))
@ -167,23 +172,31 @@
Returns a map of message keys to strings; if no useable file is found, returns nil." Returns a map of message keys to strings; if no useable file is found, returns nil."
{:doc/format :markdown} {:doc/format :markdown}
[^String accept-language-header ^String resource-path ^String default-locale] [^String accept-language-header ^String resource-path ^String default-locale]
(let [file-path (first (let [file-paths (remove
(remove empty?
nil?
(map (map
#(find-language-file-name % resource-path) #(find-language-file-name % resource-path)
(acceptable-languages accept-language-header))))] (acceptable-languages accept-language-header)))
(timbre/debug (str "Found i18n file at '" file-path "'")) default-path (join java.io.File/separator
(try
(read-string
(slurp-resource
(or
file-path
(join java.io.File/separator
[resource-path [resource-path
(str default-locale ".edn")])))) (str default-locale ".edn")])
(catch Exception any paths (concat file-paths (list default-path))
(timbre/error (str "Failed to load internationalisation because " (.getMessage any))) text (first
(remove empty?
(map
slurp-resource
paths)))]
(if text
(try
(read-string text)
(catch Exception any
(timbre/error "Failed to load internationalisation because "
(.getName (.getClass any))
(.getMessage any))
nil))
;; else
(doall
(timbre/error "No valid i18n files found, not even default. Tried" paths)
nil)))) nil))))
(def get-messages (def get-messages
@ -200,7 +213,8 @@
(def get-message (def get-message
"Return the message keyed by this `token` from the most acceptable messages collection "Return the message keyed by this `token` from the most acceptable messages collection
we have given this `accept-language-header`. we have given this `accept-language-header`, if passed, or the current default language
otherwise. If no message is found, return the token.
* `token` should be a clojure keyword identifying the message to be retrieved; * `token` should be a clojure keyword identifying the message to be retrieved;
* `accept-language-header` should be the value of an RFC2616 `Accept-Language` header; * `accept-language-header` should be the value of an RFC2616 `Accept-Language` header;
@ -209,8 +223,13 @@
* `default-locale` should be a locale specifier to use if no acceptable locale can be * `default-locale` should be a locale specifier to use if no acceptable locale can be
identified." identified."
(fn ([^Keyword token ^String accept-language-header ^String resource-path ^String default-locale] (fn ([^Keyword token ^String accept-language-header ^String resource-path ^String default-locale]
((get-messages accept-language-header resource-path default-locale) token)) (let [message (token (get-messages accept-language-header resource-path default-locale))]
(or message (name token))))
([^Keyword token ^String accept-language-header] ([^Keyword token ^String accept-language-header]
(get-message token accept-language-header *resource-path* *default-language*)) (get-message token
accept-language-header
(or (:resource-path *config*) *resource-path*)
(or (:default-language *config*) *default-language*)))
([^Keyword token] ([^Keyword token]
(get-message token nil *resource-path* *default-language*)))) (get-message token
(or (:default-language *config*) *default-language*)))))

View file

@ -1,7 +1,8 @@
(ns ^{:doc "Tests for Internationalisation." (ns ^{:doc "Tests for Internationalisation."
:author "Simon Brooke"} scot.weft.i18n.test.core :author "Simon Brooke"} scot.weft.i18n.test.core
(:require [clojure.test :refer [deftest is testing]] (:require [clojure.test :refer [deftest is testing]]
[scot.weft.i18n.core :refer [*default-language* [scot.weft.i18n.core :refer [*config*
*default-language*
acceptable-languages acceptable-languages
generate-accept-languages generate-accept-languages
get-message get-message
@ -206,7 +207,7 @@
(testing "Top level functionality" (testing "Top level functionality"
(is (is
(= (=
"This is not a pipe" "This is not a pipe."
(:pipe (get-messages "en-GB, fr-FR;q=0.9" "i18n" "en-GB")))) (:pipe (get-messages "en-GB, fr-FR;q=0.9" "i18n" "en-GB"))))
(is (is
(= (=
@ -215,9 +216,25 @@
(is (is
(= nil (get-messages "xx-XX;q=0.5, yy-YY" "i18n" "zz-ZZ")) (= nil (get-messages "xx-XX;q=0.5, yy-YY" "i18n" "zz-ZZ"))
"If no usable file is found, an exception should not be thrown.") "If no usable file is found, an exception should not be thrown.")
(binding [*default-language* "en-GB"] (binding [*config* (assoc *config* :default-language "fr-FR")]
(is (= "This is not a pipe" (get-message :pipe))) (is (= "Ceci n'est pas une pipe." (get-message :pipe)))
(is (is
(= (=
"Ceci n'est pas une pipe." (get-message :pipe "en-GB;q=0.9, fr-FR"))) "This is not a pipe." (get-message :pipe "en-GB, fr-FR;q=0.9")))
(is (= "это не труба." (get-message :pipe "de-DE" "i18n" "ru")))))) (is (= "это не труба." (get-message :pipe "de-DE" "i18n" "ru")))
(is (= "froboz" (get-message :froboz)))))
(testing "Final fall through if no suitable language found"
(binding [*config* (assoc *config* :default-language "de-DE")]
;; there is no 'de-DE' language resource in the resources,
;; and that's exactly why we've chosen it for this test.
(is (= "pipe" (get-message :pipe)))))
(testing "Deprecated variables still work"
(binding [*config* nil
*default-language* "en-GB"]
(is (= "This is not a pipe." (get-message :pipe)))
(is
(= "Ceci n'est pas une pipe."
(get-message :pipe "en-GB;q=0.9, fr-FR"))))
(binding [*config* nil
*default-language* "ru"]
(is (= "это не труба." (get-message :pipe))))))