<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <link rel="stylesheet" href="../../../coverage.css"/> <title> scot/weft/i18n/core.clj </title> </head> <body> <span class="covered" title="1 out of 1 forms covered"> 001 (ns ^{:doc "Internationalisation." </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 002 :author "Simon Brooke"} </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 003 scot.weft.i18n.core </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 004 (:require [clojure.java.io :as io] </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 005 [clojure.pprint :refer [pprint]] </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 006 [clojure.string :refer [join]] </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 007 [instaparse.core :as insta] </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 008 [taoensso.timbre :as timbre] </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 009 [trptr.java-wrapper.locale :as locale]) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 010 (:import [clojure.lang Keyword])) </span><br/> <span class="blank" title="0 out of 0 forms covered"> 011 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 012 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 013 ;;;; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 014 ;;;; scot.weft.i18n: a simple internationalisation library for Clojure. </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 015 ;;;; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 016 ;;;; This library is distributed under the Eclipse Licence in the hope </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 017 ;;;; that it may be useful, but without guarantee. </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 018 ;;;; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 019 ;;;; Copyright (C) 2017 Simon Brooke </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 020 ;;;; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 021 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; </span><br/> <span class="blank" title="0 out of 0 forms covered"> 022 </span><br/> <span class="covered" title="2 out of 2 forms covered"> 023 (def ^:dynamic *resource-path* </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 024 "The default path within the resources space on which translation files </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 025 will be sought." </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 026 "i18n") </span><br/> <span class="blank" title="0 out of 0 forms covered"> 027 </span><br/> <span class="covered" title="1 out of 1 forms covered"> 028 (def ^:dynamic *default-language* </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 029 "The default language to seek." </span><br/> <span class="covered" title="4 out of 4 forms covered"> 030 (-> (locale/get-default) locale/to-language-tag)) </span><br/> <span class="blank" title="0 out of 0 forms covered"> 031 </span><br/> <span class="covered" title="2 out of 2 forms covered"> 032 (def accept-language-grammar </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 033 "Grammar for `Accept-Language` headers" </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 034 "HEADER := SPECIFIER | SPECIFIERS; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 035 SPECIFIERS:= SPECIFIER | SPECIFIER SPEC-SEP SPECIFIERS; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 036 SPEC-SEP := #',\\s*'; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 037 SPECIFIER := LANGUAGE-TAG | LANGUAGE-TAG Q-SEP Q-VALUE; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 038 LANGUAGE-TAG := PRIMARY-TAG | PRIMARY-TAG '-' SUB-TAGS; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 039 PRIMARY-TAG := #'[a-zA-Z]+'; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 040 SUB-TAGS := SUB-TAG | SUB-TAG '-' SUB-TAGS; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 041 SUB-TAG := #'[a-zA-Z0-9]+'; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 042 Q-SEP := #';\\s*q=' </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 043 Q-VALUE := '1' | #'0.[0-9]+';") </span><br/> <span class="blank" title="0 out of 0 forms covered"> 044 </span><br/> <span class="covered" title="1 out of 1 forms covered"> 045 (def parse-accept-language-header </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 046 "Parse an `Accept-Language` header" </span><br/> <span class="covered" title="3 out of 3 forms covered"> 047 (insta/parser accept-language-grammar)) </span><br/> <span class="blank" title="0 out of 0 forms covered"> 048 </span><br/> <span class="covered" title="1 out of 1 forms covered"> 049 (defn generate-accept-languages </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 050 "From a `parse-tree` generated by the `language-specifier-grammar`, generate </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 051 a list of maps each having a `:language` key, a `:preference` key and a </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 052 `:qualifier` key." </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 053 {:doc/format :markdown} </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 054 [parse-tree] </span><br/> <span class="partial" title="1 out of 2 forms covered"> 055 (if </span><br/> <span class="covered" title="3 out of 3 forms covered"> 056 (nil? parse-tree) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 057 nil </span><br/> <span class="partial" title="2 out of 4 forms covered"> 058 (case </span><br/> <span class="covered" title="3 out of 3 forms covered"> 059 (first parse-tree) </span><br/> <span class="covered" title="5 out of 5 forms covered"> 060 :HEADER (generate-accept-languages (second parse-tree)) </span><br/> <span class="covered" title="2 out of 2 forms covered"> 061 :SPECIFIERS (cons </span><br/> <span class="covered" title="5 out of 5 forms covered"> 062 (generate-accept-languages (second parse-tree)) </span><br/> <span class="covered" title="6 out of 6 forms covered"> 063 (when (>= (count parse-tree) 3) </span><br/> <span class="covered" title="5 out of 5 forms covered"> 064 (generate-accept-languages (nth parse-tree 3)))) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 065 :SPEC-SEP nil </span><br/> <span class="covered" title="3 out of 3 forms covered"> 066 :SPECIFIER (assoc </span><br/> <span class="covered" title="5 out of 5 forms covered"> 067 (generate-accept-languages (second parse-tree)) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 068 :preference </span><br/> <span class="covered" title="2 out of 2 forms covered"> 069 (if </span><br/> <span class="covered" title="4 out of 4 forms covered"> 070 (>= (count parse-tree) 3) </span><br/> <span class="covered" title="5 out of 5 forms covered"> 071 (generate-accept-languages (nth parse-tree 3)) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 072 1)) </span><br/> <span class="covered" title="1 out of 1 forms covered"> 073 :LANGUAGE-TAG (if </span><br/> <span class="covered" title="4 out of 4 forms covered"> 074 (>= (count parse-tree) 3) </span><br/> <span class="covered" title="3 out of 3 forms covered"> 075 (assoc </span><br/> <span class="covered" title="5 out of 5 forms covered"> 076 (generate-accept-languages (second parse-tree)) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 077 :qualifier </span><br/> <span class="covered" title="5 out of 5 forms covered"> 078 (generate-accept-languages (nth parse-tree 3))) </span><br/> <span class="covered" title="5 out of 5 forms covered"> 079 (generate-accept-languages (second parse-tree))) </span><br/> <span class="covered" title="7 out of 7 forms covered"> 080 :PRIMARY-TAG {:language (second parse-tree) :qualifier "*"} </span><br/> <span class="covered" title="1 out of 1 forms covered"> 081 :SUB-TAGS (if </span><br/> <span class="covered" title="4 out of 4 forms covered"> 082 (>= (count parse-tree) 3) </span><br/> <span class="not-covered" title="0 out of 3 forms covered"> 083 (str </span><br/> <span class="not-covered" title="0 out of 5 forms covered"> 084 (generate-accept-languages (second parse-tree)) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 085 "-" </span><br/> <span class="not-covered" title="0 out of 5 forms covered"> 086 (generate-accept-languages (nth parse-tree 3))) </span><br/> <span class="covered" title="5 out of 5 forms covered"> 087 (generate-accept-languages (second parse-tree))) </span><br/> <span class="covered" title="3 out of 3 forms covered"> 088 :SUB-TAG (second parse-tree) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 089 :Q-SEP nil </span><br/> <span class="covered" title="5 out of 5 forms covered"> 090 :Q-VALUE (read-string (second parse-tree)) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 091 ;; default </span><br/> <span class="not-covered" title="0 out of 2 forms covered"> 092 (do </span><br/> <span class="not-covered" title="0 out of 16 forms covered"> 093 (timbre/error "Unable to parse header.") </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 094 nil)))) </span><br/> <span class="blank" title="0 out of 0 forms covered"> 095 </span><br/> <span class="covered" title="1 out of 1 forms covered"> 096 (defn acceptable-languages </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 097 "Generate an ordered list of acceptable languages, most-preferred first. </span><br/> <span class="blank" title="0 out of 0 forms covered"> 098 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 099 * `accept-language-header` should be the value of an RFC2616 `Accept-Language` header. </span><br/> <span class="blank" title="0 out of 0 forms covered"> 100 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 101 Returns a list of maps as generated by `generate-accept-languages`, in descending order </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 102 of preference." </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 103 {:doc/format :markdown} </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 104 [accept-language-header] </span><br/> <span class="covered" title="4 out of 4 forms covered"> 105 (let [parse-tree (parse-accept-language-header accept-language-header)] </span><br/> <span class="covered" title="4 out of 4 forms covered"> 106 (if (vector? parse-tree) </span><br/> <span class="covered" title="2 out of 2 forms covered"> 107 (reverse </span><br/> <span class="covered" title="3 out of 3 forms covered"> 108 (sort-by </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 109 :preference </span><br/> <span class="covered" title="2 out of 2 forms covered"> 110 (generate-accept-languages </span><br/> <span class="covered" title="1 out of 1 forms covered"> 111 parse-tree))) </span><br/> <span class="covered" title="21 out of 21 forms covered"> 112 (timbre/error "Failed to parse Accept-Language header '" accept-language-header "':\n" (str parse-tree))))) </span><br/> <span class="blank" title="0 out of 0 forms covered"> 113 </span><br/> <span class="blank" title="0 out of 0 forms covered"> 114 </span><br/> <span class="covered" title="1 out of 1 forms covered"> 115 (defn slurp-resource </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 116 "Slurp the resource of this name and return its contents as a string; but if it doesn't </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 117 exist log the fact and return nil, rather than throwing an exception." </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 118 [name] </span><br/> <span class="covered" title="2 out of 2 forms covered"> 119 (try </span><br/> <span class="covered" title="5 out of 5 forms covered"> 120 (slurp (io/resource name)) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 121 (catch Exception _ </span><br/> <span class="covered" title="20 out of 20 forms covered"> 122 (timbre/error (str "Resource at " name " does not exist.")) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 123 nil))) </span><br/> <span class="blank" title="0 out of 0 forms covered"> 124 </span><br/> <span class="blank" title="0 out of 0 forms covered"> 125 </span><br/> <span class="covered" title="1 out of 1 forms covered"> 126 (defn find-language-file-name </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 127 "Find the name of a messages file on this resource path which matches this `language-spec`. </span><br/> <span class="blank" title="0 out of 0 forms covered"> 128 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 129 * `language-spec` should be either a map as generated by `generate-accept-languages`, or </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 130 else a string; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 131 * `resource-path` should be the path name of the directory in which message files are stored, </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 132 within the resources on the classpath. </span><br/> <span class="blank" title="0 out of 0 forms covered"> 133 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 134 Returns the name of an appropriate file if any is found, else nil." </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 135 {:doc/format :markdown} </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 136 [language-spec resource-path] </span><br/> <span class="covered" title="3 out of 3 forms covered"> 137 (let [file-path (when </span><br/> <span class="covered" title="3 out of 3 forms covered"> 138 (string? language-spec) </span><br/> <span class="covered" title="2 out of 2 forms covered"> 139 (join </span><br/> <span class="covered" title="1 out of 1 forms covered"> 140 java.io.File/separator </span><br/> <span class="covered" title="6 out of 6 forms covered"> 141 [resource-path (str language-spec ".edn")])) </span><br/> <span class="covered" title="6 out of 6 forms covered"> 142 contents (when file-path (slurp-resource file-path))] </span><br/> <span class="covered" title="3 out of 3 forms covered"> 143 (cond </span><br/> <span class="covered" title="1 out of 1 forms covered"> 144 contents </span><br/> <span class="covered" title="1 out of 1 forms covered"> 145 file-path </span><br/> <span class="covered" title="3 out of 3 forms covered"> 146 (map? language-spec) </span><br/> <span class="covered" title="5 out of 5 forms covered"> 147 (or </span><br/> <span class="covered" title="2 out of 2 forms covered"> 148 (find-language-file-name </span><br/> <span class="covered" title="9 out of 9 forms covered"> 149 (str (:language language-spec) "-" (:qualifier language-spec)) </span><br/> <span class="covered" title="1 out of 1 forms covered"> 150 resource-path) </span><br/> <span class="covered" title="1 out of 1 forms covered"> 151 (find-language-file-name </span><br/> <span class="covered" title="3 out of 3 forms covered"> 152 (:language language-spec) </span><br/> <span class="covered" title="1 out of 1 forms covered"> 153 resource-path))))) </span><br/> <span class="blank" title="0 out of 0 forms covered"> 154 </span><br/> <span class="blank" title="0 out of 0 forms covered"> 155 </span><br/> <span class="covered" title="1 out of 1 forms covered"> 156 (defn raw-get-messages </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 157 "Return the most acceptable messages collection we have given this `accept-language-header`. </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 158 Do not use this function directly, use the memoized variant `get-messages`, as performance </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 159 will be very much better. </span><br/> <span class="blank" title="0 out of 0 forms covered"> 160 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 161 * `accept-language-header` should be the value of an RFC2616 `Accept-Language` header; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 162 * `resource-path` should be the fully-qualified path name of the directory in which </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 163 message files are stored; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 164 * `default-locale` should be a locale specifier to use if no acceptable locale can be </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 165 identified. </span><br/> <span class="blank" title="0 out of 0 forms covered"> 166 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 167 Returns a map of message keys to strings; if no useable file is found, returns nil." </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 168 {:doc/format :markdown} </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 169 [^String accept-language-header ^String resource-path ^String default-locale] </span><br/> <span class="covered" title="3 out of 3 forms covered"> 170 (let [file-path (first </span><br/> <span class="covered" title="2 out of 2 forms covered"> 171 (remove </span><br/> <span class="covered" title="1 out of 1 forms covered"> 172 nil? </span><br/> <span class="covered" title="3 out of 3 forms covered"> 173 (map </span><br/> <span class="covered" title="4 out of 4 forms covered"> 174 #(find-language-file-name % resource-path) </span><br/> <span class="covered" title="3 out of 3 forms covered"> 175 (acceptable-languages accept-language-header))))] </span><br/> <span class="covered" title="20 out of 20 forms covered"> 176 (timbre/debug (str "Found i18n file at '" file-path "'")) </span><br/> <span class="covered" title="2 out of 2 forms covered"> 177 (try </span><br/> <span class="covered" title="2 out of 2 forms covered"> 178 (read-string </span><br/> <span class="covered" title="2 out of 2 forms covered"> 179 (slurp-resource </span><br/> <span class="covered" title="5 out of 5 forms covered"> 180 (or </span><br/> <span class="covered" title="1 out of 1 forms covered"> 181 file-path </span><br/> <span class="covered" title="2 out of 2 forms covered"> 182 (join java.io.File/separator </span><br/> <span class="covered" title="2 out of 2 forms covered"> 183 [resource-path </span><br/> <span class="covered" title="4 out of 4 forms covered"> 184 (str default-locale ".edn")])))) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 185 (catch Exception any </span><br/> <span class="covered" title="19 out of 19 forms covered"> 186 (timbre/error (str "Failed to load internationalisation because " (.getMessage any))) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 187 nil)))) </span><br/> <span class="blank" title="0 out of 0 forms covered"> 188 </span><br/> <span class="covered" title="1 out of 1 forms covered"> 189 (def get-messages </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 190 "Return the most acceptable messages collection we have given this `accept-language-header` </span><br/> <span class="blank" title="0 out of 0 forms covered"> 191 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 192 * `accept-language-header` should be the value of an RFC2616 `Accept-Language` header; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 193 * `resource-path` should be the fully-qualified path name of the directory in which </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 194 message files are stored; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 195 * `default-locale` should be a locale specifier to use if no acceptable locale can be </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 196 identified. </span><br/> <span class="blank" title="0 out of 0 forms covered"> 197 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 198 Returns a map of message keys to strings.; if no useable file is found, returns nil." </span><br/> <span class="covered" title="3 out of 3 forms covered"> 199 (memoize raw-get-messages)) </span><br/> <span class="blank" title="0 out of 0 forms covered"> 200 </span><br/> <span class="covered" title="1 out of 1 forms covered"> 201 (def get-message </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 202 "Return the message keyed by this `token` from the most acceptable messages collection </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 203 we have given this `accept-language-header`. </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 204 </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 205 * `token` should be a clojure keyword identifying the message to be retrieved; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 206 * `accept-language-header` should be the value of an RFC2616 `Accept-Language` header; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 207 * `resource-path` should be the fully-qualified path name of the directory in which </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 208 message files are stored; </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 209 * `default-locale` should be a locale specifier to use if no acceptable locale can be </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 210 identified." </span><br/> <span class="covered" title="1 out of 1 forms covered"> 211 (fn ([^Keyword token ^String accept-language-header ^String resource-path ^String default-locale] </span><br/> <span class="covered" title="7 out of 7 forms covered"> 212 ((get-messages accept-language-header resource-path default-locale) token)) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 213 ([^Keyword token ^String accept-language-header] </span><br/> <span class="covered" title="6 out of 6 forms covered"> 214 (get-message token accept-language-header *resource-path* *default-language*)) </span><br/> <span class="not-tracked" title="0 out of 0 forms covered"> 215 ([^Keyword token] </span><br/> <span class="covered" title="6 out of 6 forms covered"> 216 (get-message token nil *resource-path* *default-language*)))) </span><br/> </body> </html>