Merge branch 'develop'

This commit is contained in:
Simon Brooke 2023-01-04 20:53:08 +00:00
commit e2fa7b98bb
No known key found for this signature in database
GPG key ID: A7A4F18D1D4DF987
21 changed files with 2086 additions and 240 deletions

View file

@ -6,28 +6,92 @@ A Clojure library designed to provide simple interationalisation of user-facing
To use this library in your project, add the following leiningen dependency:
[org.clojars.simon_brooke/internationalisation "1.0.0"]
[org.clojars.simon_brooke/internationalisation "1.0.4"]
To use it in your namespace, require:
[scot.weft.i18n.core :refer [get-messages]]
[scot.weft.i18n.core :refer [get-message get-messages]]
There is only one function you should need to use:
There is only two functions you may need to use:
### get-messages
(get-messages accept-language-header resource-path default-locale)
```clojure
(get-messages accept-language-header resource-path default-locale)
```
Return the most acceptable messages collection we have given this `accept-language-header`.
Return the most acceptable messages collection we have given this accept-language-header. Use this function instrad of the unmemoized variant raw-get-messages, as performance will be very much better.
* `accept-language-header` should be a string representing the value of an RFC 2616 Accept-Language header;
* `resource-path` should be a string representing the fully-qualified path name of the directory in which message files are stored;
* `default-locale` should be a string representing a locale specifier to use if no acceptable locale can be identified.
* `accept-language-header` should be the value of an RFC 2616 Accept-Language header;
* `resource-path` should be the fully-qualified path name of the directory in which message files are stored;
* `default-locale` should be a locale specifier to use if no acceptable locale can be identified.
For example:
```clojure
(get-messages "en-GB;q=0.9, fr-FR" "i18n" "en-GB")
```
Returns a map of message keys to strings.
See [RFC 2616](https://www.ietf.org/rfc/rfc2616.txt).
**NOTE THAT** `get-messages` is [memoized](https://clojuredocs.org/clojure.core/memoize) resulting in faster response when called repeatedly with similar arguments.
### get-message
A wrapper around `get-messages` to resolve a single, particular message.
```clojure
(get-message token)
(get-message token accept-language-header)
(get-message token accept-language-header resource-path default-locale)
```
where
* `token` is the Clojure [keyword](https://clojuredocs.org/clojure.core/keyword) which identifies the particular message you want to retrieve;
and all the other arguments are as defined above.
For example:
```clojure
(get-message :pipe)
(get-message :pipe "en-GB;q=0.9, fr-FR")
(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:
* `*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
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")
)
```
and
```clojure
(get-message :pipe "en-GB;q=0.9, fr-FR" "language-files" "en-CA")
```
are effectively the same.
The intelligent reader will note that if one calls
```clojure
(get-message :pipe)
```
there's no mechanism to set the `accept-header`. This is true. The expected use case for this arity of the function is in desktop applications where the locale of the local machine will always be the correct locale to use. The two-argument arity of the function is intended for web applications, where different clients may have different linguistic needs.
**NOTE THAT** `get-message` is also memoized, and for the same reason.
## The translation files
Obviously, this only works if you provide files with translations of your interesting strings. These files should contain Clojure maps, and the file names should be the locale string for which the file is relevent followed by the extension ".edn". All the translation files should be in the same directory.

View file

@ -1,31 +1,97 @@
# Introduction to internationalisation
# internationalisation
A Clojure library designed to provide simple interationalisation of user-facing messages.
## Usage
To use this library in your project, add the following leiningen dependency:
[org.clojars.simon_brooke/internationalisation "1.0.0"]
[org.clojars.simon_brooke/internationalisation "1.0.4"]
To use it in your namespace, require:
[scot.weft.i18n.core :refer [get-messages]]
[scot.weft.i18n.core :refer [get-message get-messages]]
There is only one function you should need to use:
There is only two functions you may need to use:
### get-messages
(get-messages accept-language-header resource-path default-locale)
```clojure
(get-messages accept-language-header resource-path default-locale)
```
Return the most acceptable messages collection we have given this `accept-language-header`.
Return the most acceptable messages collection we have given this accept-language-header. Use this function instrad of the unmemoized variant raw-get-messages, as performance will be very much better.
* `accept-language-header` should be a string representing the value of an RFC 2616 Accept-Language header;
* `resource-path` should be a string representing the fully-qualified path name of the directory in which message files are stored;
* `default-locale` should be a string representing a locale specifier to use if no acceptable locale can be identified.
* `accept-language-header` should be the value of an RFC 2616 Accept-Language header;
* `resource-path` should be the fully-qualified path name of the directory in which message files are stored;
* `default-locale` should be a locale specifier to use if no acceptable locale can be identified.
For example:
```clojure
(get-messages "en-GB;q=0.9, fr-FR" "i18n" "en-GB")
```
Returns a map of message keys to strings.
See [RFC 2616](https://www.ietf.org/rfc/rfc2616.txt).
**NOTE THAT** `get-messages` is [memoized](https://clojuredocs.org/clojure.core/memoize) resulting in faster response when called repeatedly with similar arguments.
### get-message
A wrapper around `get-messages` to resolve a single, particular message.
```clojure
(get-message token)
(get-message token accept-language-header)
(get-message token accept-language-header resource-path default-locale)
```
where
* `token` is the Clojure [keyword](https://clojuredocs.org/clojure.core/keyword) which identifies the particular message you want to retrieve;
and all the other arguments are as defined above.
For example:
```clojure
(get-message :pipe)
(get-message :pipe "en-GB;q=0.9, fr-FR")
(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:
* `*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
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")
)
```
and
```clojure
(get-message :pipe "en-GB;q=0.9, fr-FR" "language-files" "en-CA")
```
are effectively the same.
The intelligent reader will note that if one calls
```clojure
(get-message :pipe)
```
there's no mechanism to set the `accept-header`. This is true. The expected use case for this arity of the function is in desktop applications where the locale of the local machine will always be the correct locale to use. The two-argument arity of the function is intended for web applications, where different clients may have different linguistic needs.
**NOTE THAT** `get-message` is also memoized, and for the same reason.
## The translation files
Obviously, this only works if you provide files with translations of your interesting strings. These files should contain Clojure maps, and the file names should be the locale string for which the file is relevent followed by the extension ".edn". All the translation files should be in the same directory.

BIN
docs/MagrittePipe.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,18 @@
{"coverage":
{"scot/weft/i18n/core.clj":
[null, 1, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, 1,
null, null, null, null, 1, null, 1, null, 1, null, null, null, null,
null, null, null, null, null, null, null, null, 1, null, 1, null, 1,
null, null, null, null, null, true, 119, null, true, 119, 6, 16, 16,
16, 8, null, 18, 18, null, 18, 18, 9, null, 19, 19, 15, 15, null,
15, 4, 20, 15, 15, 0, 0, null, 0, 15, 15, null, 10, null, 0, 0,
null, null, 1, null, null, null, null, null, null, null, null, 6, 6,
5, 5, null, 5, 5, 1, null, null, 1, null, null, null, 13, 13, null,
7, null, null, null, 1, null, null, null, null, null, null, null,
null, null, null, 13, 13, 8, 8, 8, 13, 13, 13, 2, 11, 5, 5, 5, 5, 3,
3, 3, null, null, 1, null, null, null, null, null, null, null, null,
null, null, null, null, null, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3,
3, 3, null, 1, null, null, 1, null, null, null, null, null, null,
null, null, null, 1, null, 1, null, null, null, null, null, null,
null, null, null, 1, 3, null, 1, null, 1]}}

View file

@ -0,0 +1,40 @@
.covered {
font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
background-color: #558B55;
}
.not-covered {
font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
background-color: red;
}
.partial {
font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
background-color: orange;
}
.not-tracked {
font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
}
.blank {
font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
}
td {
padding-right: 10px;
}
td.with-bar {
width: 250px;
text-align: center;
}
td.with-number {
text-align: right;
}
td.ns-name {
min-width: 150px;
padding-right: 25px;
}

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><report><stats><packages value="1"/><methods value="367"/><srcfiles value="1"/><srclines value="93"/></stats><data><all name="total"><coverage type="class, %" value="0% (0/1)"/><coverage type="method, %" value="0% (0/1)"/><coverage type="block, %" value="91% (333/367)"/><coverage type="line, %" value="92% (86/93)"/><package name="scot.weft.i18n.core"><coverage type="class, %" value="0% (0/1)"/><coverage type="method, %" value="0% (0/1)"/><coverage type="block, %" value="91% (333/367)"/><coverage type="line, %" value="92% (86/93)"/></package></all></data></report>

42
docs/cloverage/index.html Normal file
View file

@ -0,0 +1,42 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" href="./coverage.css"/>
<title>Coverage Summary</title>
</head>
<body>
<table>
<thead><tr>
<td class="ns-name"> Namespace </td>
<td class="with-bar"> Forms </td>
<td class="with-number">Forms %</td>
<td class="with-bar"> Lines </td>
<td class="with-number">Lines %</td>
<td class="with-number">Total</td><td class="with-number">Blank</td><td class="with-number">Instrumented</td>
</tr></thead>
<tr>
<td><a href="scot/weft/i18n/core.clj.html">scot.weft.i18n.core</a></td><td class="with-bar"><div class="covered"
style="width:90.73569482288828%;
float:left;"> 333 </div><div class="not-covered"
style="width:9.264305177111716%;
float:left;"> 34 </div></td>
<td class="with-number">90.74 %</td>
<td class="with-bar"><div class="covered"
style="width:92.47311827956989%;
float:left;"> 86 </div><div class="partial"
style="width:2.150537634408602%;
float:left;"> 2 </div><div class="not-covered"
style="width:5.376344086021505%;
float:left;"> 5 </div></td>
<td class="with-number">94.62 %</td>
<td class="with-number">216</td><td class="with-number">23</td><td class="with-number">93</td>
</tr>
<tr><td>Totals:</td>
<td class="with-bar"></td>
<td class="with-number">90.74 %</td>
<td class="with-bar"></td>
<td class="with-number">94.62 %</td>
</tr>
</table>
</body>
</html>

View file

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

551
docs/codox/css/default.css Normal file
View file

@ -0,0 +1,551 @@
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 15px;
}
pre, code {
font-family: Monaco, DejaVu Sans Mono, Consolas, monospace;
font-size: 9pt;
margin: 15px 0;
}
h1 {
font-weight: normal;
font-size: 29px;
margin: 10px 0 2px 0;
padding: 0;
}
h2 {
font-weight: normal;
font-size: 25px;
}
h5.license {
margin: 9px 0 22px 0;
color: #555;
font-weight: normal;
font-size: 12px;
font-style: italic;
}
.document h1, .namespace-index h1 {
font-size: 32px;
margin-top: 12px;
}
#header, #content, .sidebar {
position: fixed;
}
#header {
top: 0;
left: 0;
right: 0;
height: 22px;
color: #f5f5f5;
padding: 5px 7px;
}
#content {
top: 32px;
right: 0;
bottom: 0;
overflow: auto;
background: #fff;
color: #333;
padding: 0 18px;
}
.sidebar {
position: fixed;
top: 32px;
bottom: 0;
overflow: auto;
}
.sidebar.primary {
background: #e2e2e2;
border-right: solid 1px #cccccc;
left: 0;
width: 250px;
}
.sidebar.secondary {
background: #f2f2f2;
border-right: solid 1px #d7d7d7;
left: 251px;
width: 200px;
}
#content.namespace-index, #content.document {
left: 251px;
}
#content.namespace-docs {
left: 452px;
}
#content.document {
padding-bottom: 10%;
}
#header {
background: #3f3f3f;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);
z-index: 100;
}
#header h1 {
margin: 0;
padding: 0;
font-size: 18px;
font-weight: lighter;
text-shadow: -1px -1px 0px #333;
}
#header h1 .project-version {
font-weight: normal;
}
.project-version {
padding-left: 0.15em;
}
#header a, .sidebar a {
display: block;
text-decoration: none;
}
#header a {
color: #f5f5f5;
}
.sidebar a {
color: #333;
}
#header h2 {
float: right;
font-size: 9pt;
font-weight: normal;
margin: 4px 3px;
padding: 0;
color: #bbb;
}
#header h2 a {
display: inline;
}
.sidebar h3 {
margin: 0;
padding: 10px 13px 0 13px;
font-size: 19px;
font-weight: lighter;
}
.sidebar h3 a {
color: #444;
}
.sidebar h3.no-link {
color: #636363;
}
.sidebar ul {
padding: 7px 0 6px 0;
margin: 0;
}
.sidebar ul.index-link {
padding-bottom: 4px;
}
.sidebar li {
display: block;
vertical-align: middle;
}
.sidebar li a, .sidebar li .no-link {
border-left: 3px solid transparent;
padding: 0 10px;
white-space: nowrap;
}
.sidebar li .no-link {
display: block;
color: #777;
font-style: italic;
}
.sidebar li .inner {
display: inline-block;
padding-top: 7px;
height: 24px;
}
.sidebar li a, .sidebar li .tree {
height: 31px;
}
.depth-1 .inner { padding-left: 2px; }
.depth-2 .inner { padding-left: 6px; }
.depth-3 .inner { padding-left: 20px; }
.depth-4 .inner { padding-left: 34px; }
.depth-5 .inner { padding-left: 48px; }
.depth-6 .inner { padding-left: 62px; }
.sidebar li .tree {
display: block;
float: left;
position: relative;
top: -10px;
margin: 0 4px 0 0;
padding: 0;
}
.sidebar li.depth-1 .tree {
display: none;
}
.sidebar li .tree .top, .sidebar li .tree .bottom {
display: block;
margin: 0;
padding: 0;
width: 7px;
}
.sidebar li .tree .top {
border-left: 1px solid #aaa;
border-bottom: 1px solid #aaa;
height: 19px;
}
.sidebar li .tree .bottom {
height: 22px;
}
.sidebar li.branch .tree .bottom {
border-left: 1px solid #aaa;
}
.sidebar.primary li.current a {
border-left: 3px solid #a33;
color: #a33;
}
.sidebar.secondary li.current a {
border-left: 3px solid #33a;
color: #33a;
}
.namespace-index h2 {
margin: 30px 0 0 0;
}
.namespace-index h3 {
font-size: 16px;
font-weight: bold;
margin-bottom: 0;
}
.namespace-index .topics {
padding-left: 30px;
margin: 11px 0 0 0;
}
.namespace-index .topics li {
padding: 5px 0;
}
.namespace-docs h3 {
font-size: 18px;
font-weight: bold;
}
.public h3 {
margin: 0;
float: left;
}
.usage {
clear: both;
}
.public {
margin: 0;
border-top: 1px solid #e0e0e0;
padding-top: 14px;
padding-bottom: 6px;
}
.public:last-child {
margin-bottom: 20%;
}
.members .public:last-child {
margin-bottom: 0;
}
.members {
margin: 15px 0;
}
.members h4 {
color: #555;
font-weight: normal;
font-variant: small-caps;
margin: 0 0 5px 0;
}
.members .inner {
padding-top: 5px;
padding-left: 12px;
margin-top: 2px;
margin-left: 7px;
border-left: 1px solid #bbb;
}
#content .members .inner h3 {
font-size: 12pt;
}
.members .public {
border-top: none;
margin-top: 0;
padding-top: 6px;
padding-bottom: 0;
}
.members .public:first-child {
padding-top: 0;
}
h4.type,
h4.dynamic,
h4.added,
h4.deprecated {
float: left;
margin: 3px 10px 15px 0;
font-size: 15px;
font-weight: bold;
font-variant: small-caps;
}
.public h4.type,
.public h4.dynamic,
.public h4.added,
.public h4.deprecated {
font-size: 13px;
font-weight: bold;
margin: 3px 0 0 10px;
}
.members h4.type,
.members h4.added,
.members h4.deprecated {
margin-top: 1px;
}
h4.type {
color: #717171;
}
h4.dynamic {
color: #9933aa;
}
h4.added {
color: #508820;
}
h4.deprecated {
color: #880000;
}
.namespace {
margin-bottom: 30px;
}
.namespace:last-child {
margin-bottom: 10%;
}
.index {
padding: 0;
font-size: 80%;
margin: 15px 0;
line-height: 16px;
}
.index * {
display: inline;
}
.index p {
padding-right: 3px;
}
.index li {
padding-right: 5px;
}
.index ul {
padding-left: 0;
}
.type-sig {
clear: both;
color: #088;
}
.type-sig pre {
padding-top: 10px;
margin: 0;
}
.usage code {
display: block;
color: #008;
margin: 2px 0;
}
.usage code:first-child {
padding-top: 10px;
}
p {
margin: 15px 0;
}
.public p:first-child, .public pre.plaintext {
margin-top: 12px;
}
.doc {
margin: 0 0 26px 0;
clear: both;
}
.public .doc {
margin: 0;
}
.namespace-index .doc {
margin-bottom: 20px;
}
.namespace-index .namespace .doc {
margin-bottom: 10px;
}
.markdown p, .markdown li, .markdown dt, .markdown dd, .markdown td {
line-height: 22px;
}
.markdown li {
padding: 2px 0;
}
.markdown h2 {
font-weight: normal;
font-size: 25px;
margin: 30px 0 10px 0;
}
.markdown h3 {
font-weight: normal;
font-size: 20px;
margin: 30px 0 0 0;
}
.markdown h4 {
font-size: 15px;
margin: 22px 0 -4px 0;
}
.doc, .public, .namespace .index {
max-width: 680px;
overflow-x: visible;
}
.markdown pre > code {
display: block;
padding: 10px;
}
.markdown pre > code, .src-link a {
border: 1px solid #e4e4e4;
border-radius: 2px;
}
.markdown code:not(.hljs), .src-link a {
background: #f6f6f6;
}
pre.deps {
display: inline-block;
margin: 0 10px;
border: 1px solid #e4e4e4;
border-radius: 2px;
padding: 10px;
background-color: #f6f6f6;
}
.markdown hr {
border-style: solid;
border-top: none;
color: #ccc;
}
.doc ul, .doc ol {
padding-left: 30px;
}
.doc table {
border-collapse: collapse;
margin: 0 10px;
}
.doc table td, .doc table th {
border: 1px solid #dddddd;
padding: 4px 6px;
}
.doc table th {
background: #f2f2f2;
}
.doc dl {
margin: 0 10px 20px 10px;
}
.doc dl dt {
font-weight: bold;
margin: 0;
padding: 3px 0;
border-bottom: 1px solid #ddd;
}
.doc dl dd {
padding: 5px 0;
margin: 0 0 5px 10px;
}
.doc abbr {
border-bottom: 1px dotted #333;
font-variant: none;
cursor: help;
}
.src-link {
margin-bottom: 15px;
}
.src-link a {
font-size: 70%;
padding: 1px 4px;
text-decoration: none;
color: #5555bb;
}

View file

@ -0,0 +1,97 @@
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

3
docs/codox/index.html Normal file
View file

@ -0,0 +1,3 @@
<!DOCTYPE html PUBLIC ""
"">
<html><head><meta charset="UTF-8" /><title>Internationalisation 1.0.3-SNAPSHOT</title><link rel="stylesheet" type="text/css" href="css/default.css" /><link rel="stylesheet" type="text/css" href="css/highlight.css" /><script type="text/javascript" src="js/highlight.min.js"></script><script type="text/javascript" src="js/jquery.min.js"></script><script type="text/javascript" src="js/page_effects.js"></script><script>hljs.initHighlightingOnLoad();</script></head><body><div id="header"><h2>Generated by <a href="https://github.com/weavejester/codox">Codox</a></h2><h1><a href="index.html"><span class="project-title"><span class="project-name">Internationalisation</span> <span class="project-version">1.0.3-SNAPSHOT</span></span></a></h1></div><div class="sidebar primary"><h3 class="no-link"><span class="inner">Project</span></h3><ul class="index-link"><li class="depth-1 current"><a href="index.html"><div class="inner">Index</div></a></li></ul><h3 class="no-link"><span class="inner">Topics</span></h3><ul><li class="depth-1 "><a href="intro.html"><div class="inner"><span>internationalisation</span></div></a></li></ul><h3 class="no-link"><span class="inner">Namespaces</span></h3><ul><li class="depth-1 "><a href="scot.weft.i18n.core.html"><div class="inner"><span>scot.weft.i18n.core</span></div></a></li></ul></div><div class="namespace-index" id="content"><h1><span class="project-title"><span class="project-name">Internationalisation</span> <span class="project-version">1.0.3-SNAPSHOT</span></span></h1><h5 class="license">Released under the <a href="http://www.eclipse.org/legal/epl-v10.html">Eclipse Public License</a></h5><div class="doc"><p>Internationalisation library for Clojure.</p></div><h2>Installation</h2><p>To install, add the following dependency to your project or build file:</p><pre class="deps">[org.clojars.simon_brooke/internationalisation "1.0.3-SNAPSHOT"]</pre><h2>Topics</h2><ul class="topics"><li><a href="intro.html">internationalisation</a></li></ul><h2>Namespaces</h2><div class="namespace"><h3><a href="scot.weft.i18n.core.html">scot.weft.i18n.core</a></h3><div class="doc"><div class="markdown"><p>Internationalisation.</p></div></div><div class="index"><p>Public variables and functions:</p><ul><li> <a href="scot.weft.i18n.core.html#var-*default-language*">*default-language*</a> </li><li> <a href="scot.weft.i18n.core.html#var-*resource-path*">*resource-path*</a> </li><li> <a href="scot.weft.i18n.core.html#var-accept-language-grammar">accept-language-grammar</a> </li><li> <a href="scot.weft.i18n.core.html#var-acceptable-languages">acceptable-languages</a> </li><li> <a href="scot.weft.i18n.core.html#var-find-language-file-name">find-language-file-name</a> </li><li> <a href="scot.weft.i18n.core.html#var-generate-accept-languages">generate-accept-languages</a> </li><li> <a href="scot.weft.i18n.core.html#var-get-message">get-message</a> </li><li> <a href="scot.weft.i18n.core.html#var-get-messages">get-messages</a> </li><li> <a href="scot.weft.i18n.core.html#var-parse-accept-language-header">parse-accept-language-header</a> </li><li> <a href="scot.weft.i18n.core.html#var-raw-get-messages">raw-get-messages</a> </li><li> <a href="scot.weft.i18n.core.html#var-slurp-resource">slurp-resource</a> </li></ul></div></div></div></body></html>

87
docs/codox/intro.html Normal file
View file

@ -0,0 +1,87 @@
<!DOCTYPE html PUBLIC ""
"">
<html><head><meta charset="UTF-8" /><title>internationalisation</title><link rel="stylesheet" type="text/css" href="css/default.css" /><link rel="stylesheet" type="text/css" href="css/highlight.css" /><script type="text/javascript" src="js/highlight.min.js"></script><script type="text/javascript" src="js/jquery.min.js"></script><script type="text/javascript" src="js/page_effects.js"></script><script>hljs.initHighlightingOnLoad();</script></head><body><div id="header"><h2>Generated by <a href="https://github.com/weavejester/codox">Codox</a></h2><h1><a href="index.html"><span class="project-title"><span class="project-name">Internationalisation</span> <span class="project-version">1.0.3-SNAPSHOT</span></span></a></h1></div><div class="sidebar primary"><h3 class="no-link"><span class="inner">Project</span></h3><ul class="index-link"><li class="depth-1 "><a href="index.html"><div class="inner">Index</div></a></li></ul><h3 class="no-link"><span class="inner">Topics</span></h3><ul><li class="depth-1 current"><a href="intro.html"><div class="inner"><span>internationalisation</span></div></a></li></ul><h3 class="no-link"><span class="inner">Namespaces</span></h3><ul><li class="depth-1 "><a href="scot.weft.i18n.core.html"><div class="inner"><span>scot.weft.i18n.core</span></div></a></li></ul></div><div class="document" id="content"><div class="doc"><div class="markdown"><h1><a href="#internationalisation" name="internationalisation"></a>internationalisation</h1>
<p>A Clojure library designed to provide simple interationalisation of user-facing messages.</p>
<h2><a href="#usage" name="usage"></a>Usage</h2>
<p>To use this library in your project, add the following leiningen dependency:</p>
<pre><code>[org.clojars.simon_brooke/internationalisation "1.0.4"]
</code></pre>
<p>To use it in your namespace, require:</p>
<pre><code>[scot.weft.i18n.core :refer [get-message get-messages]]
</code></pre>
<p>There is only two functions you may need to use:</p>
<h3><a href="#get-messages" name="get-messages"></a>get-messages</h3>
<pre><code class="clojure">(get-messages accept-language-header resource-path default-locale)
</code></pre>
<p>Return the most acceptable messages collection we have given this <code>accept-language-header</code>.</p>
<ul>
<li><code>accept-language-header</code> should be a string representing the value of an RFC 2616 Accept-Language header;</li>
<li><code>resource-path</code> should be a string representing the fully-qualified path name of the directory in which message files are stored;</li>
<li><code>default-locale</code> should be a string representing a locale specifier to use if no acceptable locale can be identified.</li>
</ul>
<p>For example:</p>
<pre><code class="clojure">(get-messages "en-GB;q=0.9, fr-FR" "i18n" "en-GB")
</code></pre>
<p>Returns a map of message keys to strings.</p>
<p>See <a href="https://www.ietf.org/rfc/rfc2616.txt">RFC 2616</a>.</p>
<p><strong>NOTE THAT</strong> <code>get-messages</code> is <a href="https://clojuredocs.org/clojure.core/memoize">memoized</a> resulting in faster response when called repeatedly with similar arguments.</p>
<h3><a href="#get-message" name="get-message"></a>get-message</h3>
<p>A wrapper around <code>get-messages</code> to resolve a single, particular message. </p>
<pre><code class="clojure">(get-message token)
(get-message token accept-language-header)
(get-message token accept-language-header resource-path default-locale)
</code></pre>
<p>where</p>
<ul>
<li><code>token</code> is the Clojure <a href="https://clojuredocs.org/clojure.core/keyword">keyword</a> which identifies the particular message you want to retrieve;</li>
</ul>
<p>and all the other arguments are as defined above.</p>
<p>For example:</p>
<pre><code class="clojure">(get-message :pipe)
(get-message :pipe "en-GB;q=0.9, fr-FR")
(get-message :pipe "de-DE" "i18n" "ru")
</code></pre>
<p>So how does this work? When one calls <code>(get-message token accept-language-header)</code>, how does it know where to find resources? The answer is that there are two dynamic variables:</p>
<ul>
<li><code>*resource-path*</code>, the default path within the resources space on which translation files will be sought. Initialised to <code>i18n</code>.</li>
<li><code>*default-language*</code>, 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.</li>
</ul>
<p>Thus</p>
<pre><code class="clojure">(binding [*resource-path* "language-files"
*default-language* "en-CA"]
(get-message :pipe "en-GB;q=0.9, fr-FR")
)
</code></pre>
<p>and</p>
<pre><code class="clojure">(get-message :pipe "en-GB;q=0.9, fr-FR" "language-files" "en-CA")
</code></pre>
<p>are effectively the same.</p>
<p>The intelligent reader will note that if one calls</p>
<pre><code class="clojure">(get-message :pipe)
</code></pre>
<p>theres no mechanism to set the <code>accept-header</code>. This is true. The expected use case for this arity of the function is in desktop applications where the locale of the local machine will always be the correct locale to use. The two-argument arity of the function is intended for web applications, where different clients may have different linguistic needs.</p>
<p><strong>NOTE THAT</strong> <code>get-message</code> is also memoized, and for the same reason.</p>
<h2><a href="#the-translation-files" name="the-translation-files"></a>The translation files</h2>
<p>Obviously, this only works if you provide files with translations of your interesting strings. These files should contain Clojure maps, and the file names should be the locale string for which the file is relevent followed by the extension “.edn”. All the translation files should be in the same directory.</p>
<p>In this project you will find two very simple example files, which should give you the idea:</p>
<h3><a href="#en-gb-edn" name="en-gb-edn"></a>en-GB.edn</h3>
<pre><code>;;;; This is a British English translation file.
{:pipe "This is not a pipe"}
</code></pre>
<h3><a href="#fr-fr-edn" name="fr-fr-edn"></a>fr-FR.edn</h3>
<pre><code>;;;; This is a French translation file.
{:pipe "Ceci n'est pas une pipe."}
</code></pre>
<h2><a href="#documentation" name="documentation"></a>Documentation</h2>
<p>Documentation may be generated by running</p>
<pre><code>lein codox
</code></pre>
<h2><a href="#license" name="license"></a>License</h2>
<p>Copyright © 2017 Simon Brooke</p>
<p>Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.</p></div></div></div></body></html>

2
docs/codox/js/highlight.min.js vendored Normal file

File diff suppressed because one or more lines are too long

4
docs/codox/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,112 @@
function visibleInParent(element) {
var position = $(element).position().top
return position > -50 && position < ($(element).offsetParent().height() - 50)
}
function hasFragment(link, fragment) {
return $(link).attr("href").indexOf("#" + fragment) != -1
}
function findLinkByFragment(elements, fragment) {
return $(elements).filter(function(i, e) { return hasFragment(e, fragment)}).first()
}
function scrollToCurrentVarLink(elements) {
var elements = $(elements);
var parent = elements.offsetParent();
if (elements.length == 0) return;
var top = elements.first().position().top;
var bottom = elements.last().position().top + elements.last().height();
if (top >= 0 && bottom <= parent.height()) return;
if (top < 0) {
parent.scrollTop(parent.scrollTop() + top);
}
else if (bottom > parent.height()) {
parent.scrollTop(parent.scrollTop() + bottom - parent.height());
}
}
function setCurrentVarLink() {
$('.secondary a').parent().removeClass('current')
$('.anchor').
filter(function(index) { return visibleInParent(this) }).
each(function(index, element) {
findLinkByFragment(".secondary a", element.id).
parent().
addClass('current')
});
scrollToCurrentVarLink('.secondary .current');
}
var hasStorage = (function() { try { return localStorage.getItem } catch(e) {} }())
function scrollPositionId(element) {
var directory = window.location.href.replace(/[^\/]+\.html$/, '')
return 'scroll::' + $(element).attr('id') + '::' + directory
}
function storeScrollPosition(element) {
if (!hasStorage) return;
localStorage.setItem(scrollPositionId(element) + "::x", $(element).scrollLeft())
localStorage.setItem(scrollPositionId(element) + "::y", $(element).scrollTop())
}
function recallScrollPosition(element) {
if (!hasStorage) return;
$(element).scrollLeft(localStorage.getItem(scrollPositionId(element) + "::x"))
$(element).scrollTop(localStorage.getItem(scrollPositionId(element) + "::y"))
}
function persistScrollPosition(element) {
recallScrollPosition(element)
$(element).scroll(function() { storeScrollPosition(element) })
}
function sidebarContentWidth(element) {
var widths = $(element).find('.inner').map(function() { return $(this).innerWidth() })
return Math.max.apply(Math, widths)
}
function calculateSize(width, snap, margin, minimum) {
if (width == 0) {
return 0
}
else {
return Math.max(minimum, (Math.ceil(width / snap) * snap) + (margin * 2))
}
}
function resizeSidebars() {
var primaryWidth = sidebarContentWidth('.primary')
var secondaryWidth = 0
if ($('.secondary').length != 0) {
secondaryWidth = sidebarContentWidth('.secondary')
}
// snap to grid
primaryWidth = calculateSize(primaryWidth, 32, 13, 160)
secondaryWidth = calculateSize(secondaryWidth, 32, 13, 160)
$('.primary').css('width', primaryWidth)
$('.secondary').css('width', secondaryWidth).css('left', primaryWidth + 1)
if (secondaryWidth > 0) {
$('#content').css('left', primaryWidth + secondaryWidth + 2)
}
else {
$('#content').css('left', primaryWidth + 1)
}
}
$(window).ready(resizeSidebars)
$(window).ready(setCurrentVarLink)
$(window).ready(function() { persistScrollPosition('.primary')})
$(window).ready(function() {
$('#content').scroll(setCurrentVarLink)
$(window).resize(setCurrentVarLink)
})

View file

@ -0,0 +1,30 @@
<!DOCTYPE html PUBLIC ""
"">
<html><head><meta charset="UTF-8" /><title>scot.weft.i18n.core documentation</title><link rel="stylesheet" type="text/css" href="css/default.css" /><link rel="stylesheet" type="text/css" href="css/highlight.css" /><script type="text/javascript" src="js/highlight.min.js"></script><script type="text/javascript" src="js/jquery.min.js"></script><script type="text/javascript" src="js/page_effects.js"></script><script>hljs.initHighlightingOnLoad();</script></head><body><div id="header"><h2>Generated by <a href="https://github.com/weavejester/codox">Codox</a></h2><h1><a href="index.html"><span class="project-title"><span class="project-name">Internationalisation</span> <span class="project-version">1.0.3-SNAPSHOT</span></span></a></h1></div><div class="sidebar primary"><h3 class="no-link"><span class="inner">Project</span></h3><ul class="index-link"><li class="depth-1 "><a href="index.html"><div class="inner">Index</div></a></li></ul><h3 class="no-link"><span class="inner">Topics</span></h3><ul><li class="depth-1 "><a href="intro.html"><div class="inner"><span>internationalisation</span></div></a></li></ul><h3 class="no-link"><span class="inner">Namespaces</span></h3><ul><li class="depth-1 current"><a href="scot.weft.i18n.core.html"><div class="inner"><span>scot.weft.i18n.core</span></div></a></li></ul></div><div class="sidebar secondary"><h3><a href="#top"><span class="inner">Public Vars</span></a></h3><ul><li class="depth-1"><a href="scot.weft.i18n.core.html#var-*default-language*"><div class="inner"><span>*default-language*</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-*resource-path*"><div class="inner"><span>*resource-path*</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-accept-language-grammar"><div class="inner"><span>accept-language-grammar</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-acceptable-languages"><div class="inner"><span>acceptable-languages</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-find-language-file-name"><div class="inner"><span>find-language-file-name</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-generate-accept-languages"><div class="inner"><span>generate-accept-languages</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-get-message"><div class="inner"><span>get-message</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-get-messages"><div class="inner"><span>get-messages</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-parse-accept-language-header"><div class="inner"><span>parse-accept-language-header</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-raw-get-messages"><div class="inner"><span>raw-get-messages</span></div></a></li><li class="depth-1"><a href="scot.weft.i18n.core.html#var-slurp-resource"><div class="inner"><span>slurp-resource</span></div></a></li></ul></div><div class="namespace-docs" id="content"><h1 class="anchor" id="top">scot.weft.i18n.core</h1><div class="doc"><div class="markdown"><p>Internationalisation.</p></div></div><div class="public anchor" id="var-*default-language*"><h3>*default-language*</h3><h4 class="dynamic">dynamic</h4><div class="usage"></div><div class="doc"><div class="markdown"><p>The default language to seek.</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L28">view source</a></div></div><div class="public anchor" id="var-*resource-path*"><h3>*resource-path*</h3><h4 class="dynamic">dynamic</h4><div class="usage"></div><div class="doc"><div class="markdown"><p>The default path within the resources space on which translation files will be sought.</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L23">view source</a></div></div><div class="public anchor" id="var-accept-language-grammar"><h3>accept-language-grammar</h3><div class="usage"></div><div class="doc"><div class="markdown"><p>Grammar for <code>Accept-Language</code> headers</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L32">view source</a></div></div><div class="public anchor" id="var-acceptable-languages"><h3>acceptable-languages</h3><div class="usage"><code>(acceptable-languages accept-language-header)</code></div><div class="doc"><div class="markdown"><p>Generate an ordered list of acceptable languages, most-preferred first.</p>
<ul>
<li><code>accept-language-header</code> should be the value of an RFC2616 <code>Accept-Language</code> header.</li>
</ul>
<p>Returns a list of maps as generated by <code>generate-accept-languages</code>, in descending order of preference.</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L96">view source</a></div></div><div class="public anchor" id="var-find-language-file-name"><h3>find-language-file-name</h3><div class="usage"><code>(find-language-file-name language-spec resource-path)</code></div><div class="doc"><div class="markdown"><p>Find the name of a messages file on this resource path which matches this <code>language-spec</code>.</p>
<ul>
<li><code>language-spec</code> should be either a map as generated by <code>generate-accept-languages</code>, or else a string;</li>
<li><code>resource-path</code> should be the path name of the directory in which message files are stored, within the resources on the classpath.</li>
</ul>
<p>Returns the name of an appropriate file if any is found, else nil.</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L126">view source</a></div></div><div class="public anchor" id="var-generate-accept-languages"><h3>generate-accept-languages</h3><div class="usage"><code>(generate-accept-languages parse-tree)</code></div><div class="doc"><div class="markdown"><p>From a <code>parse-tree</code> generated by the <code>language-specifier-grammar</code>, generate a list of maps each having a <code>:language</code> key, a <code>:preference</code> key and a <code>:qualifier</code> key.</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L49">view source</a></div></div><div class="public anchor" id="var-get-message"><h3>get-message</h3><div class="usage"></div><div class="doc"><div class="markdown"><p>Return the message keyed by this <code>token</code> from the most acceptable messages collection<br />we have given this <code>accept-language-header</code>.</p>
<ul>
<li><code>token</code> should be a clojure keyword identifying the message to be retrieved;</li>
<li><code>accept-language-header</code> should be the value of an RFC2616 <code>Accept-Language</code> header;</li>
<li><code>resource-path</code> should be the fully-qualified path name of the directory in which message files are stored;</li>
<li><code>default-locale</code> should be a locale specifier to use if no acceptable locale can be identified.</li>
</ul></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L201">view source</a></div></div><div class="public anchor" id="var-get-messages"><h3>get-messages</h3><div class="usage"></div><div class="doc"><div class="markdown"><p>Return the most acceptable messages collection we have given this <code>accept-language-header</code></p>
<ul>
<li><code>accept-language-header</code> should be the value of an RFC2616 <code>Accept-Language</code> header;</li>
<li><code>resource-path</code> should be the fully-qualified path name of the directory in which message files are stored;</li>
<li><code>default-locale</code> should be a locale specifier to use if no acceptable locale can be identified.</li>
</ul>
<p>Returns a map of message keys to strings.; if no useable file is found, returns nil.</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L189">view source</a></div></div><div class="public anchor" id="var-parse-accept-language-header"><h3>parse-accept-language-header</h3><div class="usage"></div><div class="doc"><div class="markdown"><p>Parse an <code>Accept-Language</code> header</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L45">view source</a></div></div><div class="public anchor" id="var-raw-get-messages"><h3>raw-get-messages</h3><div class="usage"><code>(raw-get-messages accept-language-header resource-path default-locale)</code></div><div class="doc"><div class="markdown"><p>Return the most acceptable messages collection we have given this <code>accept-language-header</code>. Do not use this function directly, use the memoized variant <code>get-messages</code>, as performance will be very much better.</p>
<ul>
<li><code>accept-language-header</code> should be the value of an RFC2616 <code>Accept-Language</code> header;</li>
<li><code>resource-path</code> should be the fully-qualified path name of the directory in which message files are stored;</li>
<li><code>default-locale</code> should be a locale specifier to use if no acceptable locale can be identified.</li>
</ul>
<p>Returns a map of message keys to strings; if no useable file is found, returns nil.</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L156">view source</a></div></div><div class="public anchor" id="var-slurp-resource"><h3>slurp-resource</h3><div class="usage"><code>(slurp-resource name)</code></div><div class="doc"><div class="markdown"><p>Slurp the resource of this name and return its contents as a string; but if it doesnt exist log the fact and return nil, rather than throwing an exception.</p></div></div><div class="src-link"><a href="https://github.com/simon-brooke/internationalisation/blob/master/src/scot/weft/i18n/core.clj#L115">view source</a></div></div></div></body></html>

17
docs/index.html Normal file
View file

@ -0,0 +1,17 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Internationalisation: Documentation</title>
<link rel="stylesheet" type="text/css" href="codox/css/default.css" />
</head>
<body>
<h1>Internationalisation: Documentation</h1>
<img src="MagrittePipe.jpeg">
<p>Illustration: <i><a href="https://en.wikipedia.org/wiki/The_Treachery_of_Images">La Trahison des Images</a> by
<a href="https://en.wikipedia.org/wiki/Ren%C3%A9_Magritte">Ren&eacute; Magritte</a></i></p>
<ul>
<li><a href="codox/index.html">Primary documentaion</a></li>
<li><a href="cloverage/index.html">Test coverage</a></li>
</ul>
</body>
</html>

View file

@ -1,13 +1,21 @@
(defproject org.clojars.simon_brooke/internationalisation "1.0.3"
(defproject org.clojars.simon_brooke/internationalisation "1.0.3-SNAPSHOT"
:cloverage {:output "docs/cloverage"
:codecov? true
:emma-xml? true}
:codox {:metadata {:doc "**TODO**: write docs"
:doc/format :markdown}
:output-path "docs/codox"
:source-uri "https://github.com/simon-brooke/internationalisation/blob/master/{filepath}#L{line}"}
:dependencies [[org.clojure/clojure "1.11.1"]
[com.taoensso/timbre "6.0.4"]
[instaparse "1.4.12"]
[trptr/java-wrapper "0.2.3"]]
:description "Internationalisation library for Clojure"
:url "https://github.com/simon-brooke/internationalisation"
:lein-release {:deploy-via :clojars}
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[com.taoensso/timbre "4.10.0"]
[instaparse "1.4.7"]]
:plugins [[lein-codox "0.10.3"]]
:plugins [[lein-cloverage "1.2.2"]
[lein-codox "0.10.7"]]
:profiles {:dev {:resource-paths ["resources"]}}
:lein-release {:deploy-via :clojars}
:signing {:gpg-key "Simon Brooke (Stultus in monte) <simon@journeyman.cc>"}
)
:url "https://github.com/simon-brooke/internationalisation")

3
resources/i18n/ru.edn Normal file
View file

@ -0,0 +1,3 @@
;;;; This is a Russian translation file.
{:pipe "это не труба."}

View file

@ -2,9 +2,12 @@
:author "Simon Brooke"}
scot.weft.i18n.core
(:require [clojure.java.io :as io]
[clojure.pprint :refer [pprint]]
[clojure.string :refer [join]]
[instaparse.core :as insta]
[taoensso.timbre :as timbre]))
[taoensso.timbre :as timbre]
[trptr.java-wrapper.locale :as locale])
(:import [clojure.lang Keyword]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -17,6 +20,14 @@
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:dynamic *resource-path*
"The default path within the resources space on which translation files
will be sought."
"i18n")
(def ^:dynamic *default-language*
"The default language to seek."
(-> (locale/get-default) locale/to-language-tag))
(def accept-language-grammar
"Grammar for `Accept-Language` headers"
@ -31,12 +42,10 @@
Q-SEP := #';\\s*q='
Q-VALUE := '1' | #'0.[0-9]+';")
(def parse-accept-language-header
"Parse an `Accept-Language` header"
(insta/parser accept-language-grammar))
(defn generate-accept-languages
"From a `parse-tree` generated by the `language-specifier-grammar`, generate
a list of maps each having a `:language` key, a `:preference` key and a
@ -51,7 +60,7 @@
:HEADER (generate-accept-languages (second parse-tree))
:SPECIFIERS (cons
(generate-accept-languages (second parse-tree))
(if (>= (count parse-tree) 3)
(when (>= (count parse-tree) 3)
(generate-accept-languages (nth parse-tree 3))))
:SPEC-SEP nil
:SPECIFIER (assoc
@ -78,8 +87,11 @@
(generate-accept-languages (second parse-tree)))
:SUB-TAG (second parse-tree)
:Q-SEP nil
:Q-VALUE (read-string (second parse-tree)))))
:Q-VALUE (read-string (second parse-tree))
;; default
(do
(timbre/error "Unable to parse header.")
nil))))
(defn acceptable-languages
"Generate an ordered list of acceptable languages, most-preferred first.
@ -90,11 +102,14 @@
of preference."
{:doc/format :markdown}
[accept-language-header]
(let [parse-tree (parse-accept-language-header accept-language-header)]
(if (vector? parse-tree)
(reverse
(sort-by
:preference
(generate-accept-languages
(parse-accept-language-header accept-language-header)))))
parse-tree)))
(timbre/error "Failed to parse Accept-Language header '" accept-language-header "':\n" (str parse-tree)))))
(defn slurp-resource
@ -103,7 +118,7 @@
[name]
(try
(slurp (io/resource name))
(catch Exception any
(catch Exception _
(timbre/error (str "Resource at " name " does not exist."))
nil)))
@ -119,12 +134,12 @@
Returns the name of an appropriate file if any is found, else nil."
{:doc/format :markdown}
[language-spec resource-path]
(let [file-path (if
(let [file-path (when
(string? language-spec)
(join
java.io.File/separator
[resource-path (str language-spec ".edn")]))
contents (if file-path (slurp-resource file-path))]
contents (when file-path (slurp-resource file-path))]
(cond
contents
file-path
@ -171,15 +186,31 @@
(timbre/error (str "Failed to load internationalisation because " (.getMessage any)))
nil))))
(def get-messages
"Return the most acceptable messages collection we have given this `accept-language-header`
`accept-language-header` should be the value of an RFC2616 `Accept-Language` header;
`resource-path` should be the fully-qualified path name of the directory in which
* `accept-language-header` should be the value of an RFC2616 `Accept-Language` header;
* `resource-path` should be the fully-qualified path name of the directory in which
message files are stored;
`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.
Returns a map of message keys to strings.; if no useable file is found, returns nil."
(memoize raw-get-messages))
(def get-message
"Return the message keyed by this `token` from the most acceptable messages collection
we have given this `accept-language-header`.
* `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;
* `resource-path` should be the fully-qualified path name of the directory in which
message files are stored;
* `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))
([^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*))))

View file

@ -1,7 +1,12 @@
(ns ^{:doc "Tests for Internationalisation."
:author "Simon Brooke"} scot.weft.i18n.test.core
(:require [clojure.test :refer :all]
[scot.weft.i18n.core :refer :all]))
(:require [clojure.test :refer [deftest is testing]]
[scot.weft.i18n.core :refer [*default-language*
acceptable-languages
generate-accept-languages
get-message
get-messages
parse-accept-language-header]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -14,7 +19,6 @@
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(deftest test-generator
(testing "Generating normalised maps from parse trees"
(is
@ -181,7 +185,11 @@
[:SPECIFIERS
[:SPECIFIER [:LANGUAGE-TAG [:PRIMARY-TAG "fr"]]]]]]
(parse-accept-language-header "en, fr"))
"Space after comma should be tolerated.")))
"Space after comma should be tolerated.")
(is (vector? (parse-accept-language-header "en, fr"))
"If the header is valid, we should get a (parse tree) vector")
(is (not (vector? (parse-accept-language-header "")))
"If the header is invalid, we should get a failure object not a vector")))
(deftest test-ordering
@ -206,4 +214,10 @@
(:pipe (get-messages "en-GB;q=0.9, fr-FR" "i18n" "en-GB"))))
(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.")))
"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)))
(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"))))))