From 341462a90397b97b32e54e4a1e7ec082280f2bad Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 4 Jan 2023 21:06:16 +0000 Subject: [PATCH 1/3] Upversion to 1.0.5-SNAPSHOT for next cycle. --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 0053280..185748f 100644 --- a/project.clj +++ b/project.clj @@ -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" :codecov? true :emma-xml? true} From aadf7f46a091fdfaa4664df4392e1e5071d66e97 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 4 Jan 2023 22:55:29 +0000 Subject: [PATCH 2/3] Considerably improved reliability, return token if no message found. --- src/scot/weft/i18n/core.clj | 46 +++++++++++++++++++------------ test/scot/weft/i18n/test/core.clj | 3 +- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/scot/weft/i18n/core.clj b/src/scot/weft/i18n/core.clj index 0e2117c..1ed6a45 100644 --- a/src/scot/weft/i18n/core.clj +++ b/src/scot/weft/i18n/core.clj @@ -119,7 +119,7 @@ (try (slurp (io/resource name)) (catch Exception _ - (timbre/error (str "Resource at " name " does not exist.")) + (timbre/warn (str "Resource at " name " does not exist.")) nil))) @@ -167,23 +167,31 @@ Returns a map of message keys to strings; if no useable file is found, returns nil." {:doc/format :markdown} [^String accept-language-header ^String resource-path ^String default-locale] - (let [file-path (first - (remove - nil? + (let [file-paths (remove + empty? (map #(find-language-file-name % resource-path) - (acceptable-languages accept-language-header))))] - (timbre/debug (str "Found i18n file at '" file-path "'")) - (try - (read-string - (slurp-resource - (or - file-path - (join java.io.File/separator + (acceptable-languages accept-language-header))) + default-path (join java.io.File/separator [resource-path - (str default-locale ".edn")])))) - (catch Exception any - (timbre/error (str "Failed to load internationalisation because " (.getMessage any))) + (str default-locale ".edn")]) + paths (concat file-paths (list default-path)) + 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)))) (def get-messages @@ -200,7 +208,8 @@ (def get-message "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; * `accept-language-header` should be the value of an RFC2616 `Accept-Language` header; @@ -209,8 +218,9 @@ * `default-locale` should be a locale specifier to use if no acceptable locale can be identified." (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 ((get-messages accept-language-header resource-path default-locale) token)] + (or message (name token)))) ([^Keyword token ^String accept-language-header] (get-message token accept-language-header *resource-path* *default-language*)) ([^Keyword token] - (get-message token nil *resource-path* *default-language*)))) \ No newline at end of file + (get-message token *default-language* *resource-path* *default-language*)))) \ No newline at end of file diff --git a/test/scot/weft/i18n/test/core.clj b/test/scot/weft/i18n/test/core.clj index de3038d..9b1d7f4 100644 --- a/test/scot/weft/i18n/test/core.clj +++ b/test/scot/weft/i18n/test/core.clj @@ -220,4 +220,5 @@ (is (= "Ceci n'est pas une pipe." (get-message :pipe "en-GB;q=0.9, fr-FR"))) - (is (= "это не труба." (get-message :pipe "de-DE" "i18n" "ru")))))) + (is (= "это не труба." (get-message :pipe "de-DE" "i18n" "ru"))) + (is (= "froboz" (get-message :froboz)))))) From d1c96732b5d4a655a52b39aeeab848bee67c9b9b Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Thu, 5 Jan 2023 12:45:02 +0000 Subject: [PATCH 3/3] Deprecated individual config variables in favour of a config map. --- README.md | 35 +++++++++++++++++++++++-------- resources/i18n/en-GB.edn | 2 +- src/scot/weft/i18n/core.clj | 19 ++++++++++++----- test/scot/weft/i18n/test/core.clj | 28 +++++++++++++++++++------ 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 309c761..8f36ea2 100644 --- a/README.md +++ b/README.md @@ -62,21 +62,21 @@ For example: (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 - translation files will be sought. Initialised to `i18n`. -* `*default-language*`, the language tag for the language to use when no +* `:resource-path`, whose value should be a string representation of the default + 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 otherwise suitable language can be identified. Initialised to the default language of the runtime session, so this may well be different on your machine from someone elses running identical software. Thus ```clojure -(binding [*resource-path* "language-files" - *default-language* "en-CA"] - (get-message :pipe "en-GB;q=0.9, fr-FR") -) +(binding [*config* {:resource-path "language-files" + :default-language "en-CA"}] + (get-message :pipe "en-GB;q=0.9, fr-FR")) ``` and ```clojure @@ -116,10 +116,27 @@ In this project you will find two very simple example files, which should give y ## Documentation -Documentation may be generated by running +Documentation can be found here. It may be generated by running 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 Copyright © 2017 Simon Brooke diff --git a/resources/i18n/en-GB.edn b/resources/i18n/en-GB.edn index 3d1133c..a05de3b 100644 --- a/resources/i18n/en-GB.edn +++ b/resources/i18n/en-GB.edn @@ -1,3 +1,3 @@ ;;;; This is a British English translation file. -{:pipe "This is not a pipe"} +{:pipe "This is not a pipe."} diff --git a/src/scot/weft/i18n/core.clj b/src/scot/weft/i18n/core.clj index 1ed6a45..b074c2a 100644 --- a/src/scot/weft/i18n/core.clj +++ b/src/scot/weft/i18n/core.clj @@ -22,13 +22,18 @@ (def ^:dynamic *resource-path* "The default path within the resources space on which translation files - will be sought." + will be sought. Deprecated, prefer `(:resource-path *config*)`." "i18n") (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)) +(def ^:dynamic *config* + "Extensible configuration for i18n." + {:default-language (-> (locale/get-default) locale/to-language-tag) + :resource-path "i18n"}) + (def accept-language-grammar "Grammar for `Accept-Language` headers" "HEADER := SPECIFIER | SPECIFIERS; @@ -218,9 +223,13 @@ * `default-locale` should be a locale specifier to use if no acceptable locale can be identified." (fn ([^Keyword token ^String accept-language-header ^String resource-path ^String default-locale] - (let [message ((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] - (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] - (get-message token *default-language* *resource-path* *default-language*)))) \ No newline at end of file + (get-message token + (or (:default-language *config*) *default-language*))))) \ No newline at end of file diff --git a/test/scot/weft/i18n/test/core.clj b/test/scot/weft/i18n/test/core.clj index 9b1d7f4..91004e0 100644 --- a/test/scot/weft/i18n/test/core.clj +++ b/test/scot/weft/i18n/test/core.clj @@ -1,7 +1,8 @@ (ns ^{:doc "Tests for Internationalisation." :author "Simon Brooke"} scot.weft.i18n.test.core (:require [clojure.test :refer [deftest is testing]] - [scot.weft.i18n.core :refer [*default-language* + [scot.weft.i18n.core :refer [*config* + *default-language* acceptable-languages generate-accept-languages get-message @@ -206,7 +207,7 @@ (testing "Top level functionality" (is (= - "This is not a pipe" + "This is not a pipe." (:pipe (get-messages "en-GB, fr-FR;q=0.9" "i18n" "en-GB")))) (is (= @@ -215,10 +216,25 @@ (is (= 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.") - (binding [*default-language* "en-GB"] - (is (= "This is not a pipe" (get-message :pipe))) + (binding [*config* (assoc *config* :default-language "fr-FR")] + (is (= "Ceci n'est pas une pipe." (get-message :pipe))) (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 (= "froboz" (get-message :froboz)))))) + (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))))))