pax_global_header00006660000000000000000000000064130645344040014515gustar00rootroot0000000000000052 comment=18e17e41a7b262c358874757bb1fea9372e45b8b clj-i18n-0.8.0/000077500000000000000000000000001306453440400130475ustar00rootroot00000000000000clj-i18n-0.8.0/.gitignore000066400000000000000000000001721306453440400150370ustar00rootroot00000000000000/target /classes /checkouts pom.xml pom.xml.asc *.jar *.class /.lein-* /.nrepl-port .hgignore .hg/ /resources/locales.clj clj-i18n-0.8.0/.travis.yml000066400000000000000000000001731306453440400151610ustar00rootroot00000000000000language: clojure lein: 2.7.1 jdk: - openjdk7 - oraclejdk8 script: "./ext/travisci/test.sh" branches: only: - master clj-i18n-0.8.0/LICENSE000066400000000000000000000261361306453440400140640ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. clj-i18n-0.8.0/Makefile000066400000000000000000000000441306453440400145050ustar00rootroot00000000000000include src/leiningen/i18n/Makefile clj-i18n-0.8.0/README.md000066400000000000000000000270021306453440400143270ustar00rootroot00000000000000# i18n A Clojure library and leiningen plugin to make i18n easier. Provides convenience functions to access the JVM's localization facilities and automates managing messages and resource bundles. The tooling for translators uses [GNU gettext](http://www.gnu.org/software/gettext/), so that translators can work with `.po` files which are widely used and for which a huge amount of tooling exists. The `main.clj` and `example/program` in this repo contain some simple code that demonstrates how to use the translation functions. Before you can use it, you need to run `make` to generate the necessary `ResourceBundles`. After that, you can use `lein run` or `LANG=de_DE lein run` to look at English and German output. ## Developer usage Any Clojure code that needs to generate human-readable text must use the functions `puppetlabs.i18n.core/trs` and `puppetlabs.i18n.core/tru` to do so. Use `trs` for messages that should be formatted in the system's locale, for example log messages, and `tru` for messages that will be shown to the current user, for example an error that happened processing a web request. When you require `puppetlabs.i18n.core` into your namespace, you *must* call it either `trs`/`tru`/`trun`/`trsn` or `i18n/trs`/`i18n/tru`/`i18n/trun`/`i18n/trsn` (these are the names that `xgettext` will look for when it extracts strings) Typically, you would have this in your namespace declaration (ns puppetlabs.myproject (:require [puppetlabs.i18n.core :as i18n :refer [trs trsn tru trun]])) You use `trs`/`tru` very similar to how you use `format`, except that the format string must be a valid [`java.text.MessageFormat`](https://docs.oracle.com/javase/8/docs/api/java/text/MessageFormat.html) pattern. Note that these patterns offer support for localized formatting; see the Javadocs for details. For example, you would write (println (trs "It takes {0} software engineers {1} hours to change a light bulb" 3 9)) `trsn`/`trun` are similar to `trs`/`tru` except that they support pluralization of strings. The first argument is the singular version of the string, the second argument must be the plural form of the string. The third argument is the count value to determine the level of pluralization. Any additional arguments will be used for additional formatting (println (trsn "We found one cute puppy" "We found {0} cute puppies" 5)) ### How to find the Strings Here is a crappy Ruby script that you can point at a Clojure source tree to find *most* of the strings that will need to be translated: https://github.com/cprice404/stringtracker/blob/master/getstrings.rb ### Comments for translators It is sometimes useful to tell the translator something about the message; you can do that by preceding the message string in the`trs`/`tru` invocation with a comment; in the above example you might want to say ;; This is really just a silly example message. It gets the following ;; arguments: ;; 0 : number of software engineers (an integer) ;; 1 : number of hours (also an integer) (println (trs "It takes {0} software engineers {1} hours to change a light bulb" 3 9)) The comment will be copied to `.pot` together with the actual message so that translators have some context on what they are working on. Note that such comments must be immediately preceding the string that is the message. When you write ;; No translator will see this (trs "A message on another line") the comments do *not* get extracted into `.pot`. ### Single quotes in messages Single quotes have a special meaning in [`java.text.MessageFormat`](https://docs.oracle.com/javase/8/docs/api/java/text/MessageFormat.html) patterns and need to be escaped with another single quote: ;; The following will produce "Hes going to the store" (trs "He's going to the store") ;; You may want to supply a comment for devs and ;; translators to make sure the quoting is preserved. ;; The following will produce "He's going to the store" (trs "He''s going to the store") ### Separating message extraction from translation In some cases, messages need to be generated separately from when they're translated; this is common in specialized `def` forms or when defining a constant for reuse. In that case, use the `mark` macro to mark strings for xgettext extraction, and the standard `trs`/`tru` at the translation site. ### Development tools Extracting messages and building ResourceBundles requires the command line tools from [GNU gettext](https://www.gnu.org/software/gettext/) which you will have to install manually. If you are using Homebrew on OSX, run `brew install gettext`. OSX provides the BSD gettext library by default and because of that the Homebrew formula for `gettext` is keg-only. keg-only formulas are not symlinked. This can be remedied by running `brew link gettext --force`. On Red Hat-based operating systems, including Fedora, install gettext via `yum install gettext` ### Project setup [![Clojars Project](https://img.shields.io/clojars/v/puppetlabs/i18n.svg)](https://clojars.org/puppetlabs/i18n) 1. In your `project.clj`, add `[puppetlabs/i18n "0.5.0"]` to your project's :plugins and :dependencies vectors (without the version number in :dependencies if your project uses clj-parent). Also add ``` :uberjar-merge-with {"locales.clj" [(comp read-string slurp) (fn [new prev] (if (map? prev) [new prev] (conj prev new))) #(spit %1 (pr-str %2))]} ``` to merge in the translation locales.clj from upstream projects. 2. Run `lein i18n init`. This will * put a `Makefile.i18n` into `dev-resources/` in your project and include it into an existing toplevel `Makefile` resp. create a new one that does that. You should check these files into you source control system. * put scripts for comparing and updating PO & POT files in `dev-resources/i18n/bin`. (These scripts and the Makefile.i18n are updated to include your project name, so that the POT file will be named after your project.) These are used by [the clj-i18n CI job][ci-job] and can be ignored (they are added to the project's .gitignore file). * add hooks to the `compile` task that will rebuild the resource bundles (equivalent of running `make i18n`). 3. **If there are namespaces/packages in your project with names which do not start with a prefix derived from the project name:** you'll need to list all of your namespaces/package name prefixes in the `PACKAGES` variable in the top level `Makefile` before the inclusion of the `dev-resources/Makefile.i18n` 4. Add a job using [CI job configs' i18n-clj template][ci-job] to your project's CI pipelines. This job will automatically update the POT file when externalized strings are added or changed in the project. [ci-job]: https://github.com/puppetlabs/ci-job-configs/blob/master/resources/job-templates/i18n-clj.yaml This setup will ensure that compiling your project will also regenerate the Java `ResourceBundle` classes that your code needs to do translations. You can manually regenerate these files by running `make i18n`. Additional information about the Make targets is available through running `make help`. **Note: `make i18n` will fail if you don't have at least one string wrapped with a translation function, i.e. trs or tru.** The i18n tools maintain files in three directories: * message catalogs in `locales/` * compiled translations in `resources/` * temporary files in the project root `/`, for example `/mp-e` You should check the files in `locales/` into source control, but not the ones in `resources/` or the `mp-*` files. A sample `.gitignore` for a project might look something like: ``` # Ignore these files for clj-i18n /resources/example/*.class /resources/locales.clj /mp-* ``` ### Web service changes If you are working on an HTTP service, you will also need to make sure that we properly handle the locale that the user requests via the `Accept-Language` header. The library contains the function `locale-negotiator` that you should use as a Ring middleware. It stores the negotiated locale in the `*locale*` binding - ultimately, that's the locale that the `tru` macro will use. ### Testing and pseudo-localization For testing, it is often useful to introduce translations that are maintained separately from the generally used locales, and whose change is controlled by developers rather than translators. The `i18n` library uses the file `resources/locales.clj`, which is generated and maintained by the `make` targets, to track for which locales translations are available. Additional locales can be made available by putting one or more `locales.clj` files on the class path whose `:package` entry is the same as the one in `resources/locales.clj` but that mentions additional `:locales`. That makes it possible to introduce additional locales for testing by doing the following: 1. Create a file `test/locales.clj` by copying `resources/locales.clj` and edit the copy by changing the `:locales` entry to the languages that should be used for testing 1. For each of the additional locales, create a message catalog. It will generally be easiest to base that message catalog on properties files rather than on `.po` files. If you added the `eo` locale, you need to create a file `test//Messages_eo.properties`. Note that pluralization is not currently supported in properties files. 1. Use those additional locales in your tests. The `test/` directory of this library has an example of that in the `test-tru` test in `core_test.clj`. The macro `with-user-locale` can be used to change the locale under which a certain test should run, for example, with ```clojure (let [eo (string-as-locale "eo")] (with-user-locale eo (testing "user-locale is Esperanto" (is (= eo (user-locale)))))) ``` ## Translator usage Translators for Puppet, don't use this workflow. In the Puppet workflow POs are generated in our translation tool, from an up to date POT. We don't, as developers, update or commit POs. So this may only be relevant should a developer want to test or generate a test language. ### Generate a test .po file Prior to generating a po file, make sure the POT is up to date by running `make i18n`. This will put new msgids from the app, into the POT. To create a `.po` file for the language eo: make locales/eo.po Note this will just take the contents of the current POT and write the PO from it. Subsequent runs will not keep that file up to date. ### Update a test .po file Prior to updating a po file, make sure the POT is up to date by running `make i18n`. This will put new msgids from the app, into the POT. To update the po: msgmerge -U locales/eo.po locales/.pot This uses the contents of the current POT to update msgids in the target po (eo.po). ## Release usage When it comes time to make a release, or if you want to use your code in a different locale before then, you need to generate Java `ResourceBundle` classes that contain the localized messages. This is done by running `make msgfmt` on your project. # Hacking The code is set up as an ordinary leiningen project, with the one exception that you need to run `make` before running `lein test` or `lein run`, as there are messages that need to be turned into a message bundle. # Maintenance Maintainers: David Lutterkort and Libby Molina Tickets: File bug tickets at https://tickets.puppetlabs.com/browse/INTL, and add the `clj` component to the ticket. clj-i18n-0.8.0/dev-resources/000077500000000000000000000000001306453440400156355ustar00rootroot00000000000000clj-i18n-0.8.0/dev-resources/test-locales/000077500000000000000000000000001306453440400202345ustar00rootroot00000000000000clj-i18n-0.8.0/dev-resources/test-locales/conflicting-de.clj000066400000000000000000000003211306453440400236070ustar00rootroot00000000000000;; This locales.clj sample is deliberately conflicting with the de-ru-es.clj. ;; It is used by the tests in core_test.clj. { :locales #{"de"} :package "example.i18n" :bundle "alternate.i18n.Messages" } clj-i18n-0.8.0/dev-resources/test-locales/de-ru-es.clj000066400000000000000000000003611306453440400223470ustar00rootroot00000000000000;; This is a sample of the locales.clj file that the old version of ;; the Makefile used to generate. ;; It is used by the tests in core_test.clj. { :locales #{"de" "es" "ru"} :package "example.i18n" :bundle "example.i18n.Messages" } clj-i18n-0.8.0/dev-resources/test-locales/fr-it.clj000066400000000000000000000003501306453440400217450ustar00rootroot00000000000000;; This is a sample of the locales.clj file that the old version of ;; the Makefile used to generate. ;; It is used by the tests in core_test.clj. { :locales #{"it" "fr"} :package "other.i18n" :bundle "other.i18n.Messages" } clj-i18n-0.8.0/dev-resources/test-locales/merge-eo.clj000066400000000000000000000003051306453440400224240ustar00rootroot00000000000000;; This has the same package as de-es-ru.clj which means we will merge the ;; locales from both files. ;; It is used by the tests in core_test.clj. { :locales #{"eo"} :package "example.i18n" } clj-i18n-0.8.0/dev-resources/test-locales/multi-pacakge-es.clj000066400000000000000000000004001306453440400240500ustar00rootroot00000000000000;; This is a sample of the locales.clj file that the current version fo the ;; Makefile generates. ;; It is used by the tests in core_test.clj. { :locales #{"es"} :packages ["example.i18n" "alternate.i18n"] :bundle "multi_package.i18n.Messages" } clj-i18n-0.8.0/dev-resources/test-locales/overlapping-packages.clj000066400000000000000000000006631306453440400250350ustar00rootroot00000000000000;; This is a sample of the locales.clj file that the current version fo the ;; Makefile generates. ;; It is used by the tests in core_test.clj. { :locales #{"es"} ;; alt3rnate3.i18n has the same length as multi-package-es.clj, dexample.i18n.* overlaps with multi-package-es.clj :packages ["example.i18n.another_package" "example" "example.i18n.another_package" "alt3rnat3.i18n"] :bundle "overlapped_package.i18n.Messages" } clj-i18n-0.8.0/dev-resources/test-locales/uberjar-en-de.clj000066400000000000000000000005471306453440400233540ustar00rootroot00000000000000;; This is an example locales.clj that we expect to be generated by projects ;; which generate uberjars and need to build a list of the locales.clj maps [ { :locales #{"en" "de"} :packages ["puppetlabs.i18n"] :bundle "puppetlabs.i18n.Messages" } { :locales #{"en" "fr"} :packages ["puppetlabs.foo"] :bundle "puppetlabs.foo.Messages" } ] clj-i18n-0.8.0/dev-resources/test-pos/000077500000000000000000000000001306453440400174135ustar00rootroot00000000000000clj-i18n-0.8.0/dev-resources/test-pos/diff-location.pot000066400000000000000000000022321306453440400226540ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "X-Git-Ref: deadb33f\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/default.clj msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj, src/puppetlabs/classifier/application/default.clj msgid "create or delete children" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj:666 msgid "edit parameter values" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj:123, src/puppetlabs/classifier/application/permissioned.clj:456 msgid "edit classes, variables, and parameters" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/diff-order.pot000066400000000000000000000020511306453440400221560ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "X-Git-Ref: deadb33f\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "create or delete children" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit classes, variables, and parameters" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit parameter values" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/diff-ref.pot000066400000000000000000000020501306453440400216160ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "X-Git-Ref: deadb33f\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "create or delete children" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit parameter values" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit classes, variables, and parameters" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/diff-string-edit.pot000066400000000000000000000020511306453440400232740ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "X-Git-Ref: deadb33f\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "create and delete children" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit parameter values" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit classes, variables, and parameters" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/diff-translated.pot000066400000000000000000000021011306453440400232000ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "X-Git-Ref: deadb33f\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj msgid "access all groups" msgstr "Accedar todas las groupas" #: src/puppetlabs/classifier/application/permissioned.clj msgid "create or delete children" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit parameter values" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit classes, variables, and parameters" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/diff-whole-string.pot000066400000000000000000000022101306453440400234620ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "X-Git-Ref: deadb33f\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "create or delete children" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit parameter values" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit classes, variables, and parameters" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "change environment" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/git-ref.po000066400000000000000000000015201306453440400213060ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "X-Git-Ref: deadb33f\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "create or delete children" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/line-numbers.po000066400000000000000000000021241306453440400223520ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "X-Git-Ref: deadb33f\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj:123 msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj:124, src/puppetlabs/classifier/application/permissioned.clj:836 msgid "create or delete children" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit parameter values" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "src/foo.clj:340" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/no-git-ref.po000066400000000000000000000014701306453440400217240ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "create or delete children" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/no-line-numbers.po000066400000000000000000000021101306453440400227570ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.classifier \n" "X-Git-Ref: deadb33f\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj, src/puppetlabs/classifier/application/permissioned.clj msgid "create or delete children" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "edit parameter values" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "src/foo.clj:340" msgstr "" clj-i18n-0.8.0/dev-resources/test-pos/no-project-id-version.po000066400000000000000000000014111306453440400241050ustar00rootroot00000000000000# Esperanto test translations for puppetlabs.classifier package. # Copyright (C) 2016 Puppet Labs # This file is distributed under the same license as the puppetlabs.classifier package. # Automatically generated, 2016. # msgid "" msgstr "" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/puppetlabs/classifier/application/permissioned.clj msgid "access all groups" msgstr "" #: src/puppetlabs/classifier/application/permissioned.clj msgid "create or delete children" msgstr "" clj-i18n-0.8.0/dev-setup000077500000000000000000000001261306453440400147100ustar00rootroot00000000000000#!/usr/bin/env bash # This bootstraps the resources so that tests can pass make i18n clj-i18n-0.8.0/doc/000077500000000000000000000000001306453440400136145ustar00rootroot00000000000000clj-i18n-0.8.0/doc/intro.md000066400000000000000000000001461306453440400152720ustar00rootroot00000000000000# Introduction to i18n TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) clj-i18n-0.8.0/example/000077500000000000000000000000001306453440400145025ustar00rootroot00000000000000clj-i18n-0.8.0/example/program/000077500000000000000000000000001306453440400161515ustar00rootroot00000000000000clj-i18n-0.8.0/example/program/.gitignore000066400000000000000000000001721306453440400201410ustar00rootroot00000000000000/target /classes /checkouts pom.xml pom.xml.asc *.jar *.class /.lein-* /.nrepl-port .hgignore .hg/ /resources/locales.clj clj-i18n-0.8.0/example/program/README.md000066400000000000000000000043521306453440400174340ustar00rootroot00000000000000# Example program This little leiningen project is meant as a simple sandbox for experimenting with the i18n library. If you do not have the i18n library installed, or you want to use the lates from the git checkout, simply run (in this directory) > mkdir checkouts > ln -s ../../.. checkouts/i18n Let's see what a German user of this program would be told: > LANG=de_DE lein run Before we can bring them any joy, we need to initialize the project for using i18n: > lein i18n init This will put a `Makefile` in place; run `make help` to learn more about what that `Makefile` can do for you. You should run `lein i18n init` every time the i18n library is updated to make sure you have the most recent `Makefile`. **Prior to running `make i18n`, you will need to have at least one string wrapped with a translation function, i.e. trs or tru.** Until you are comfortable with how `.po` files are handled, it is safest to only run `make i18n`, which is also run automtically every time `lein compile` is run: > make i18n There is now a file `locales/messages.pot` which is the 'template' file for all translations. Translators will generate language-specific versions of this file by running: > make locales/de.po They will run this command (which is a thin wrapper aroung `msgmerge`/`msginit`) every time the `messages.pot` has changed and translations need to be updated. If you don't feel like translating the messages for this program into German yourself, you can cheat and use the file `dev-resources/de.po.premade` which I included for convenience: > cp dev-resources/de.po.premade locales/de.po We are now at the point where a translator has given you updated translations, and we want to try out our program in different languages: > make i18n > lein run > LANG=de_DE lein run If this weren't an example program, but a 'real' project, you would check the following files into git: * `Makefile` and `dev-resources/Makefile.i18n`. You can freely edit `Makefile`, but `dev-resources/Makefile.i18n` will get clobbered every time you run `lein i18n init`, e.g., when you update to a newer version of the i18n library * `locales/messages.pot` and `locales/*.po` as they contain the raw messages and the translations clj-i18n-0.8.0/example/program/dev-resources/000077500000000000000000000000001306453440400207375ustar00rootroot00000000000000clj-i18n-0.8.0/example/program/dev-resources/.gitkeep000066400000000000000000000000001306453440400223560ustar00rootroot00000000000000clj-i18n-0.8.0/example/program/dev-resources/de.po.premade000066400000000000000000000017771306453440400233170ustar00rootroot00000000000000# German translations for PACKAGE package. # Copyright (C) 2015 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2015. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2015-06-05 12:45-0700\n" "PO-Revision-Date: 2015-06-04 07:40-0700\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: src/puppetlabs/i18n_example_program/main.clj:14 msgid "Hello, World" msgstr "Hallo, draußen nur Kännchen" #: src/puppetlabs/i18n_example_program/main.clj:17 msgid "{0} bottles on the wall" msgstr "An der Wand sind {0} Flaschen" #: src/puppetlabs/i18n_example_program/main.clj:22 msgid "If we take {0} bottles away, we have {1} left" msgstr "Wenn wir {0} Flaschen wegnehmen, haben wir noch {1} übrig" clj-i18n-0.8.0/example/program/project.clj000066400000000000000000000007461306453440400203200ustar00rootroot00000000000000(defproject puppetlabs/i18n-example-program "0.1.0" :description "A sample use of the i18n library" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.6.0"] [puppetlabs/i18n "0.1.0-SNAPSHOT"]] :plugins [[puppetlabs/i18n "0.1.0-SNAPSHOT"]] :main puppetlabs.i18n-example-program.main :aot [puppetlabs.i18n-example-program.main]) clj-i18n-0.8.0/example/program/resources/000077500000000000000000000000001306453440400201635ustar00rootroot00000000000000clj-i18n-0.8.0/example/program/resources/.gitkeep000066400000000000000000000000001306453440400216020ustar00rootroot00000000000000clj-i18n-0.8.0/example/program/src/000077500000000000000000000000001306453440400167405ustar00rootroot00000000000000clj-i18n-0.8.0/example/program/src/puppetlabs/000077500000000000000000000000001306453440400211175ustar00rootroot00000000000000clj-i18n-0.8.0/example/program/src/puppetlabs/i18n_example_program/000077500000000000000000000000001306453440400251405ustar00rootroot00000000000000clj-i18n-0.8.0/example/program/src/puppetlabs/i18n_example_program/main.clj000066400000000000000000000015041306453440400265560ustar00rootroot00000000000000(ns puppetlabs.i18n-example-program.main (:gen-class) (:require [puppetlabs.i18n.core :as i18n :refer [trs tru]])) (defn -main "I don't do a whole lot." [] ;; The bundle to use is based on the namespace we are working in, from ;; which we derive the name of the Leiningen project. We have one catalog ;; per Leiningen project (println "Using bundle" (i18n/bundle-name)) ;; Simple system message (println (trs "Hello, World")) ;; Interpolate using java.text.MessageFormat (println (trs "{0} bottles on the wall" 99)) ;; You can also translate a message first, and later interpolate values ;; into the translation. Note that i18n/fmt takes the values to ;; interpolate as a seq (let [msg (trs "If we take {0} bottles away, we have {1} left")] (println (i18n/fmt (i18n/system-locale) msg [7 92])))) clj-i18n-0.8.0/ext/000077500000000000000000000000001306453440400136475ustar00rootroot00000000000000clj-i18n-0.8.0/ext/travisci/000077500000000000000000000000001306453440400154735ustar00rootroot00000000000000clj-i18n-0.8.0/ext/travisci/test.sh000077500000000000000000000000531306453440400170070ustar00rootroot00000000000000#!/usr/bin/env bash ./dev-setup lein test clj-i18n-0.8.0/locales/000077500000000000000000000000001306453440400144715ustar00rootroot00000000000000clj-i18n-0.8.0/locales/de.po000066400000000000000000000027631306453440400154310ustar00rootroot00000000000000# German translations for PACKAGE package. # Copyright (C) 2015 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # David Lutterkort , 2015. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: 2015-03-26 17:16-0700\n" "Last-Translator: David Lutterkort \n" "Language-Team: German\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: src/puppetlabs/i18n/main.clj msgid "I do not speak German" msgstr "Ich spreche kein Deutsch" #. Very simple localization #: src/puppetlabs/i18n/main.clj msgid "Welcome! This is localized" msgstr "Willkommen ! Draußen nur Kännchen" #: src/puppetlabs/i18n/main.clj msgid "There is one bottle of beer on the wall." msgid_plural "There are {0} bottles of beer on the wall." msgstr[0] "Es gibt eine Flasche Bier an der Wand." msgstr[1] "Es gibt {0} Flaschen Bier an der Wand." #: src/puppetlabs/i18n/main.clj msgid "It took {0} programmers {1} months to implement this" msgstr "{0} Programmierer brauchten {1} Monat(e) um das zu implementieren" #: src/puppetlabs/i18n/main.clj msgid "There are {0,number,integer} bicycles in Beijing" msgstr "In Peking gibt es {0,number,integer} Fahrräder" #~ msgid "This is another test string" #~ msgstr "german german german german german" clj-i18n-0.8.0/locales/messages.pot000066400000000000000000000022351306453440400170260ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Puppet # This file is distributed under the same license as the puppetlabs.i18n package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: puppetlabs.i18n \n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" #: src/puppetlabs/i18n/main.clj msgid "I do not speak German" msgstr "" #. Very simple localization #: src/puppetlabs/i18n/main.clj msgid "Welcome! This is localized" msgstr "" #: src/puppetlabs/i18n/main.clj msgid "There is one bottle of beer on the wall." msgid_plural "There are {0} bottles of beer on the wall." msgstr[0] "" msgstr[1] "" #: src/puppetlabs/i18n/main.clj msgid "It took {0} programmers {1} months to implement this" msgstr "" #: src/puppetlabs/i18n/main.clj msgid "There are {0,number,integer} bicycles in Beijing" msgstr "" clj-i18n-0.8.0/project.clj000066400000000000000000000015541306453440400152140ustar00rootroot00000000000000(defproject puppetlabs/i18n "0.8.0" :description "Clojure i18n library" :url "http://github.com/puppetlabs/clj-i18n" :license {:name "Apache License, Version 2.0" :url "http://www.apache.org/licenses/LICENSE-2.0"} :pedantic? :abort :dependencies [[org.clojure/clojure "1.6.0"] [cpath-clj "0.1.2"] [org.gnu.gettext/libintl "0.18.3"]] :profiles {:dev {:dependencies [[puppetlabs/kitchensink "2.1.0" :exclusions [org.clojure/clojure]]]}} :main puppetlabs.i18n.main :aot [puppetlabs.i18n.main] :deploy-repositories [["releases" {:url "https://clojars.org/repo" :username :env/clojars_jenkins_username :password :env/clojars_jenkins_password :sign-releases false}]]) clj-i18n-0.8.0/resources/000077500000000000000000000000001306453440400150615ustar00rootroot00000000000000clj-i18n-0.8.0/resources/.gitkeep000066400000000000000000000000001306453440400165000ustar00rootroot00000000000000clj-i18n-0.8.0/src/000077500000000000000000000000001306453440400136365ustar00rootroot00000000000000clj-i18n-0.8.0/src/i18n/000077500000000000000000000000001306453440400144155ustar00rootroot00000000000000clj-i18n-0.8.0/src/i18n/plugin.clj000066400000000000000000000005601306453440400164060ustar00rootroot00000000000000(ns i18n.plugin (:require [leiningen.core.main :as l] [robert.hooke :as rh] [leiningen.compile] [clojure.java.shell :as sh :refer [sh]])) (defn compile-hook [task project] (l/debug "i18n: running 'make i18n'") (sh "make" "i18n") (task project)) (defn hooks [] (rh/add-hook #'leiningen.compile/compile #'compile-hook)) clj-i18n-0.8.0/src/leiningen/000077500000000000000000000000001306453440400156065ustar00rootroot00000000000000clj-i18n-0.8.0/src/leiningen/i18n.clj000066400000000000000000000125411306453440400170620ustar00rootroot00000000000000(ns leiningen.i18n "Plugin for i18n tasks. Start by using i18n init" (:require [leiningen.core.main :as l] [leiningen.core.eval :as e] [leiningen.i18n.utils :as utils] [clojure.java.io :as io] [clojure.pprint :as pprint] [clojure.string :as cstr] [clojure.java.shell :as sh :refer [sh]] [cpath-clj.core :as cp])) (defn help [] " The i18n tooling expects that you have GNU make and the gettext tools installed. The following subtasks are supported: init - add i18n tool support to the project, then run 'make help' make - invoke 'make i18n' ") (defn path-join [ & args ] (apply str (cons (first args) (map #(str java.io.File/separator %) (rest args))))) (defn dev-resources-path "Return the first path in the project's resource-paths that ends in dev-resources or create dev-resources in the first :source-paths" [project] (or (first (filter #(.endsWith % "dev-resources") (:resource-paths project))) (l/abort "You must have a dev-resources directory in your project's resource-paths"))) (defn dev-resources-dir "Return the dev-resources-path as a file. Create the directory if it does not exist" [project] (let [dir (io/as-file (dev-resources-path project))] (if (or (.isDirectory dir) (.mkdirs dir)) dir (l/abort (str "Could not create directory " dir))))) (defn bundle-package-name "Return the Java package name in which our resource bundles should live. This is the project's group and name in dotted notation. If the project has no group, we use 'nogroup'" [project] (namespace-munge (str (or (:group project) "nogroup") "." (cstr/replace (:name project) "/" ".")))) (defn copy-makefile-to-dev-resources [project] (let [dest (path-join (dev-resources-dir project) "Makefile.i18n") makefile (io/resource "leiningen/i18n/Makefile")] (spit (io/as-file dest) (-> (utils/replace-first-in-multiline (slurp makefile) #"BUNDLE=.*" (str "BUNDLE=" (bundle-package-name project))) (utils/replace-first-in-multiline #"POT_NAME=.*" (str "POT_NAME=" (:name project) ".pot")))))) (defn copy-scripts-to-dev-resources [project] (let [dest-dir (io/as-file (path-join (dev-resources-dir project) "i18n" "bin")) scripts (cp/resources (io/resource "leiningen/i18n/bin"))] (when-not (.exists dest-dir) (.mkdirs dest-dir)) (doseq [[basename [script-uri]] scripts] (let [dest-file (io/as-file (path-join (.getPath dest-dir) basename))] (io/copy (io/input-stream script-uri) dest-file) (.setExecutable dest-file true false))))) ; second false means set it for everyone (defn project-file "Construct a path in the project's root by appending rest to it and return a file" [project & rest] (let [root (:root project)] (io/as-file (apply path-join (cons root rest))))) (defn update-scripts-with-pot-name "CI script task needs to know the name of the POT file to be modified and committed. This will insert the correct name of the POT in the /bin files." [project] (let [dest (path-join (dev-resources-dir project) "i18n" "bin" "update-pot.sh") pot-script (io/resource "leiningen/i18n/bin/update-pot.sh")] (spit (io/as-file dest) (utils/replace-first-in-multiline (slurp pot-script) #"POT_NAME=.*" (str "POT_NAME=" (:name project) ".pot"))))) (defn ensure-contains-line "Make sure that file contains the given line, if not append it. If file does not exist yet, create it and put line into it" [file line] (if (.isFile file) (let [contents (slurp file)] (if-not (.contains contents line) (do (if-not (.endsWith contents "\n") (spit file "\n" :append true)) (spit file (str line "\n") :append true)))) (spit file (str line "\n")))) (defn edit-toplevel-makefile "Add a line to include Makefile.i18n to an existing Makefile or create a new one with just the include statement" [project] (let [include-line "include dev-resources/Makefile.i18n" makefile (project-file project "Makefile")] (ensure-contains-line makefile include-line))) (defn edit-gitignore "Add generated i18n files that should not be checked in to .gitignore" [project] (let [lines ["/resources/locales.clj" "/dev-resources/i18n/bin"] gitignore (project-file project ".gitignore")] (doseq [line lines] (ensure-contains-line gitignore line)))) (defn i18n-init [project] (l/info "Setting up Makefile; don't forget to check it in") (copy-makefile-to-dev-resources project) (l/info "Adding i18n scripts in `dev-resources/i18n/bin`") (copy-scripts-to-dev-resources project) (update-scripts-with-pot-name project) (edit-toplevel-makefile project) (edit-gitignore project)) (defn i18n-make [project] (l/info "Running 'make i18n'") (sh "make" "i18n")) (defn abort [& rest] (apply l/abort (concat '("Error:") rest (list "\n\n" (help))))) (defn i18n [project command] (if-not (:root project) (abort "The i18n plugin can only be run inside a project")) (condp = command nil (abort "You need to provide a subcommand") "init" (i18n-init project) "make" (i18n-make project) (abort "Unexpected command:" command))) clj-i18n-0.8.0/src/leiningen/i18n/000077500000000000000000000000001306453440400163655ustar00rootroot00000000000000clj-i18n-0.8.0/src/leiningen/i18n/Makefile000066400000000000000000000140671306453440400200350ustar00rootroot00000000000000# -*- Makefile -*- # This file was generated by the i18n leiningen plugin # Do not edit this file; it will be overwritten the next time you run # lein i18n init # # The name of the package into which the translations bundle will be placed BUNDLE=puppetlabs.i18n # The name of the POT file into which the gettext code strings (msgid) will be placed POT_NAME=messages.pot # The list of names of packages covered by the translation bundle; # by default it contains a single package - the same where the translations # bundle itself is placed - but this can be overridden - preferably in # the top level Makefile PACKAGES?=$(BUNDLE) LOCALES=$(basename $(notdir $(wildcard locales/*.po))) BUNDLE_DIR=$(subst .,/,$(BUNDLE)) BUNDLE_FILES=$(patsubst %,resources/$(BUNDLE_DIR)/Messages_%.class,$(LOCALES)) FIND_SOURCES=find src -name \*.clj # xgettext before 0.19 does not understand --add-location=file. Even CentOS # 7 ships with an older gettext. We will therefore generate full location # info on those systems, and only file names where xgettext supports it LOC_OPT=$(shell xgettext --add-location=file -f - /dev/null 2>&1 && echo --add-location=file || echo --add-location) LOCALES_CLJ=resources/locales.clj define LOCALES_CLJ_CONTENTS { :locales #{$(patsubst %,"%",$(LOCALES))} :packages [$(patsubst %,"%",$(PACKAGES))] :bundle $(patsubst %,"%",$(BUNDLE).Messages) } endef export LOCALES_CLJ_CONTENTS i18n: msgfmt # Update locales/.pot update-pot: locales/$(POT_NAME) locales/$(POT_NAME): $(shell $(FIND_SOURCES)) | locales @tmp=$$(mktemp $@.tmp.XXXX); \ $(FIND_SOURCES) \ | xgettext --from-code=UTF-8 --language=lisp \ --copyright-holder='Puppet ' \ --package-name="$(BUNDLE)" \ --package-version="$(BUNDLE_VERSION)" \ --msgid-bugs-address="docs@puppet.com" \ -k \ -kmark:1 -ki18n/mark:1 \ -ktrs:1 -ki18n/trs:1 \ -ktru:1 -ki18n/tru:1 \ -ktrun:1,2 -ki18n/trun:1,2 \ -ktrsn:1,2 -ki18n/trsn:1,2 \ $(LOC_OPT) \ --add-comments --sort-by-file \ -o $$tmp -f -; \ sed -i.bak -e 's/charset=CHARSET/charset=UTF-8/' $$tmp; \ sed -i.bak -e 's/POT-Creation-Date: [^\\]*/POT-Creation-Date: /' $$tmp; \ rm -f $$tmp.bak; \ if ! diff -q -I POT-Creation-Date $$tmp $@ >/dev/null 2>&1; then \ mv $$tmp $@; \ else \ rm $$tmp; touch $@; \ fi # Run msgfmt over all .po files to generate Java resource bundles # and create the locales.clj file msgfmt: $(BUNDLE_FILES) $(LOCALES_CLJ) clean-orphaned-bundles # Force rebuild of locales.clj if its contents is not the the desired one. The # shell echo is used to add a trailing newline to match the one from `cat` ifneq ($(shell cat $(LOCALES_CLJ) 2> /dev/null),$(shell echo '$(LOCALES_CLJ_CONTENTS)')) .PHONY: $(LOCALES_CLJ) endif $(LOCALES_CLJ): | resources @echo "Writing $@" @echo "$$LOCALES_CLJ_CONTENTS" > $@ # Remove every resource bundle that wasn't generated from a PO file. # We do this because we used to generate the english bundle directly from the POT. .PHONY: clean-orphaned-bundles clean-orphaned-bundles: @for bundle in resources/$(BUNDLE_DIR)/Messages_*.class; do \ locale=$$(basename "$$bundle" | sed -E -e 's/\$$?1?\.class$$/_class/' | cut -d '_' -f 2;); \ if [ ! -f "locales/$$locale.po" ]; then \ rm "$$bundle"; \ fi \ done resources/$(BUNDLE_DIR)/Messages_%.class: locales/%.po | resources msgfmt --java2 -d resources -r $(BUNDLE).Messages -l $(*F) $< # Use this to initialize translations. Updating the PO files is done # automatically through a CI job that utilizes the scripts in the project's # `bin` file, which themselves come from the `clj-i18n` project. locales/%.po: | locales @if [ ! -f $@ ]; then \ touch $@ && msginit --no-translator -l $(*F) -o $@ -i locales/$(POT_NAME); \ fi resources locales: @mkdir $@ help: $(info $(HELP)) @echo .PHONY: help define HELP This Makefile assists in handling i18n related tasks during development. Files that need to be checked into source control are put into the locales/ directory. They are locales/$(POT_NAME) - the POT file generated by 'make update-pot' locales/$$LANG.po - the translations for $$LANG Only the $$LANG.po files should be edited manually; this is usually done by translators. You can use the following targets: i18n: refresh all the files in locales/ and recompile resources update-pot: extract strings and update locales/$(POT_NAME) locales/LANG.po: create translations for LANG msgfmt: compile the translations into Java classes; this step is needed to make translations available to the Clojure code and produces Java class files in resources/ endef # @todo lutter 2015-04-20: for projects that use libraries with their own # translation, we need to combine all their translations into one big po # file and then run msgfmt over that so that we only have to deal with one # resource bundle clj-i18n-0.8.0/src/leiningen/i18n/bin/000077500000000000000000000000001306453440400171355ustar00rootroot00000000000000clj-i18n-0.8.0/src/leiningen/i18n/bin/add-gitref.sh000077500000000000000000000037461306453440400215140ustar00rootroot00000000000000#!/bin/bash # This script does one thing and one thing only: given a PO or POT filename, it # adds a 'X-Git-Ref' header (or updates the existing header) with the current # git HEAD's SHA. If it can't add the header for any reason, it prints a message # on stderr and exits nonzero. # # All of the real work is just a few lines in the addAfterLine and addGitSha # functions; the rest of the script is checking for error conditions. FIELD_NAME='X-Git-Ref' PREVIOUS_FIELD='Project-Id-Version' COMMIT_HEADER_REGEX="^\"$FIELD_NAME:[ \t]*[a-zA-Z0-9]*[ \t]*\\\\n\"[ \t]*$" function addAfterLine { # filename target_line new_line local file="$1" target_line="$2" new_line="$3" sed -i.bak -e "/^$target_line\s*$/ {" -e "a\\ $new_line" -e "}" "$file" rm "$file.bak" } function addGitSha { # POx_file local pot_file="$1" sha=$(git rev-parse HEAD) addAfterLine "$pot_file" "\\\"$PREVIOUS_FIELD:.*\\\"" "\"$FIELD_NAME: $sha\\\\n\"" } ## Main ## ==== set -eo pipefail pot_file="$1" set -u # there should be no more undefined variables if [[ -z "$pot_file" ]]; then echo "ERROR: no PO or POT file given" 1>&2 echo "usage: $0 " 1>&2 exit 1 fi if [[ ! -e "$pot_file" ]]; then echo "ERROR: the file '$pot_file' does not exist" 1>&2 exit 1 fi if [[ ! -r "$pot_file" ]]; then echo "ERROR: don't have permission to open file '$pot_file'" 1>&2 exit 1 fi ## if there's a commit header in there already, remove it if [[ ! -z "$(grep "$COMMIT_HEADER_REGEX" "$pot_file")" ]]; then sed -i.bak -e "/$COMMIT_HEADER_REGEX/ {" -e d -e "}" "$pot_file" rm "$pot_file.bak" fi if ! grep "$PREVIOUS_FIELD" "$pot_file"; then echo "ERROR: the $FIELD_NAME header must follow the $PREVIOUS_FIELD header, but no" 1>&2; echo "$PREVIOUS_FIELD header was found in '$pot_file'" 1>&2; exit 1 fi addGitSha "$pot_file" if ! grep "$FIELD_NAME" "$pot_file"; then echo "ERROR: unable to add $FIELD_NAME header to '$pot_file' for unknown reasons." 1>&2; echo "Please file a bug report." 1>&2; exit 1 fi clj-i18n-0.8.0/src/leiningen/i18n/bin/compare-POTs.sh000077500000000000000000000031261306453440400217470ustar00rootroot00000000000000#!/bin/bash # This script's only function is to determine whether there are any differences # in the strings of the two gettext PO or POT files it's given as arguments. If # there are no differences, a message to that effect is printed and the script # exits zero; if there are, a different message is printed and the exit status # is nonzero (specifically, 1), # # Differences are defined as only the addition or removal of entire strings # and changes to the content of any one string; any differences in the # translations available between the two files does not count. # # Most of this scritp is just checking error conditions; the real work happens # in the last 10 lines. set -eo pipefail usage="usage: $0 old.pot new.pot" old="$1" new="$2" set -u # there should be no more undefined variables if [[ -z "$old" ]]; then echo "ERROR: no 'old.pot' file given" 1>&2 echo "$usage" 1>&2 exit 1 fi if [[ -z "$new" ]]; then echo "ERROR: no 'new.pot' file given" 1>&2 echo "$usage" 1>&2 exit 1 fi for f in "$old" "$new"; do if [[ ! -e "$f" ]]; then echo "ERROR: the file '$f' does not exist" 1>&2; exit 1 fi if [[ ! -r "$f" ]]; then echo "ERROR: don't have permission to open $f" 1>&2; exit 1 fi done set +e # msgcmp will return 1 if there are any differences msgcmp_warnings=$(msgcmp --use-untranslated "$old" "$new" 2>&1) msgcmp_status=$? set -e set -x if [[ "$msgcmp_status" -ne 0 \ || "$msgcmp_warnings" =~ "warning: this message is not used" ]]; then echo "Found differences between the POTs" exit 1 else echo "The POTs contain identical strings" exit 0 fi clj-i18n-0.8.0/src/leiningen/i18n/bin/remove-line-numbers.sh000077500000000000000000000014321306453440400233670ustar00rootroot00000000000000#!/bin/bash # This script's only function is to strip the line numbers from the location # comments in a PO or POT file. It does this using a sed one-liner at the # bottom. The preceding lines are largely about checking for error conditions. set -eo pipefail gettext_file="$1" set -u # there should be no more undefined variables if [[ -z "$gettext_file" ]]; then echo "ERROR: no PO or POT file given" 1>&2 echo "usage: $0 " 1>&2 exit 1 fi if [[ ! -e "$gettext_file" ]]; then echo "ERROR: the file '$gettext_file' does not exist" 1>&2 exit 1 fi if [[ ! -r "$gettext_file" ]]; then echo "ERROR: don't have permission to open '$gettext_file'" 1>&2 exit 1 fi sed -i.bak -e '/^#:.*[0-9]/ {' -e 's/:[0-9][0-9]*//g' -e '}' "$gettext_file" rm "$gettext_file.bak" clj-i18n-0.8.0/src/leiningen/i18n/bin/update-pot.sh000077500000000000000000000033201306453440400215540ustar00rootroot00000000000000#!/bin/bash set -eo pipefail POT_NAME="" repo_dir="$(dirname "$0")/../../.." pot_file="$repo_dir/locales/$POT_NAME" compare_pots="$repo_dir/dev-resources/i18n/bin/compare-POTs.sh" git_branch="$1"; set -u if [[ -z "$git_branch" ]]; then echo "ERROR: no git branch argument was supplied, so the updated POT can't be pushed" 1>&2 echo "usage: $0 remote_branch_name" 1>&2 exit 1 fi if [[ ! -e "$pot_file" ]]; then echo "ERROR: POT file '$pot_file' doesn't exist" 1>&2 echo 'Have you run `make i18n update-pot`?' exit 1 fi if [[ ! -r "$pot_file" ]]; then echo "ERROR: don't have permission to open '$pot_file'" 1>&2 exit 1 fi if [[ ! -e "$compare_pots" ]]; then echo "ERROR: the file '$compare_pots' does not exist" 1>&2 echo 'Have you run 'lein i18n init'?' 1>&2 exit 1 fi if [[ ! -x "$compare_pots" ]]; then echo "ERROR: don't have permission to execute '$compare_pots'" 1>&2 exit 1 fi # move current POT out of the way old_pot="$(mktemp "/tmp/i18n-POT-XXXXXXXX.po")" mv "$pot_file" "$old_pot" # regenerate the POT make update-pot new_pot="$pot_file" echo "" echo "Comparing checked-in POT file with fresh POT file" set +e # see if there are new strings in the new POT if ! "$compare_pots" "$old_pot" "$new_pot"; then set -e echo "" echo "String changes found in fresh POT file; committing" ./dev-resources/i18n/bin/remove-line-numbers.sh "$new_pot" ./dev-resources/i18n/bin/add-gitref.sh "$new_pot" git add "$new_pot" git commit -m "(i18n) Update strings in $POT_NAME file" echo "" echo "Pushing updated POT file to GitHub" git push origin "HEAD:$git_branch" rm "$old_pot" else echo "" echo "No string changes found; restoring old POT file" mv "$old_pot" "$pot_file" fi clj-i18n-0.8.0/src/leiningen/i18n/utils.clj000066400000000000000000000007321306453440400202210ustar00rootroot00000000000000(ns leiningen.i18n.utils "Plugin for i18n tasks. Start by using i18n init" (:require [clojure.string :as cstr])) (defn replace-first-in-multiline "Replace first instance of regex-match in the passed in string with the replacement. Uses RegExp's m flag (?m) so that ^/$ matches beginning of line." [s regex-match replacement] (cstr/replace-first s (re-pattern (str "(?m)^" (.toString regex-match) "$")) replacement)) clj-i18n-0.8.0/src/puppetlabs/000077500000000000000000000000001306453440400160155ustar00rootroot00000000000000clj-i18n-0.8.0/src/puppetlabs/i18n/000077500000000000000000000000001306453440400165745ustar00rootroot00000000000000clj-i18n-0.8.0/src/puppetlabs/i18n/core.clj000066400000000000000000000327241306453440400202260ustar00rootroot00000000000000(ns puppetlabs.i18n.core (:gen-class) (:require [clojure.java.io :as io] [clojure.set] [clojure.edn :as edn] [clojure.string :as str])) ;;; General setup/info (defn info-files "Find all locales.clj files on the context class path and return them as a seq of URLs" [] (-> (Thread/currentThread) .getContextClassLoader (.getResources "locales.clj") enumeration-seq)) (defn parse-info-file "This function will handle the normal locales.clj file the Makefile builds in a project or a list of the the locales.clj maps which we construct when we build the single locales.clj that goes into an uberjar." [info-file-path] (let [data-structure (-> info-file-path slurp edn/read-string)] (if (map? data-structure) [(assoc data-structure :source info-file-path)] (map #(assoc % :source info-file-path) data-structure)))) (defn infos "Read all the locales.clj files on the classpath and return them as an array of maps. There is one locales.clj file per Clojure project. Each entry in the map uses the following keys: :locales - set of the locale names in which translations are available :packages - the list of pacakges covered by the associted resouce bundle :bundle - the name of the resource bundle :source - the path to the file from which we read this entry (added automatically)" [] ;; this will not change over the lifetime of the program and should be ;; memoized; there are a few thunks involving infos that could be ;; precomputed in a similar manner (letfn [(check-locales [item] (if-let [locales (:locales item)] (if (and (set? locales) (seq locales)) item (throw (Exception. (format "Invalid locales info: %s: :locales must be a nonempty set" (:source item))))) (throw (Exception. (format "Invalid locales info: %s: missing :locales" (:source item)))))) (check-and-normalize-packages [item] (if-let [packages (:packages item)] (if (coll? packages) (-> item (dissoc :package)) ; just to make sure we don't have both: :packages & :package (throw (Exception. (format "Invalid locales info: %s: :packages must be a collection" (:source item))))) (if-let [package (:package item)] ; for backwards compatibility: transform :package to :packages (-> item (dissoc :package) (assoc :packages [package])) (throw (Exception. (format "Invalid locales info: %s: missing :packages" (:source item)))))))] (->> (info-files) (mapcat parse-info-file) (map (comp check-and-normalize-packages check-locales))))) (def string-length-comparator (reify java.util.Comparator (compare [_ lhs rhs] (clojure.core/compare (count rhs) (count lhs))))) (defn info-map' "Forces the parsing of the `locale.clj` file and creation of the bundle `info-map`. This information doesn't change after startup and thus the cached value `info-map` should be used rather than calling this function directly" [] (letfn [(merge-entry [old new package] (if (some? old) (let [bundle-old (:bundle old) bundle-new (:bundle new)] (if (or (nil? bundle-old) (nil? bundle-new) (= bundle-old bundle-new)) {:locales (clojure.set/union (:locales old) (:locales new)) :bundle (or bundle-old bundle-new) :source (let [source-old (:source old) source-new (:source new)] (clojure.set/union (if (set? source-old) source-old #{source-old}) (if (set? source-new) source-new #{source-new})))} (throw (Exception. (format "Invalid locales info: %s and %s are both for package %s but set different bundles %s and %s" (:source old) (:source new) package bundle-old bundle-new))))) new))] (reduce (fn [map item] (let [packages (:packages item) item (dissoc item :packages)] (reduce (fn [map package] (update-in map [package] merge-entry item package)) map packages))) {} (infos)))) (def info-map "Turn the result of infos into a map mapping the package name to locales and bundle name. To facilitate testing, we allow multiple infos with the same :package as long as they agree on the :bundle. The result of such a setup is that the :locales for such a package are the union of all the locales from those info files" (delay (info-map'))) (defn available-locales "Return a list of all the locales for which we have translations based on the information in locales.clj generated at compile time" [] ;; intersection would be another option; in a well-managed code base, the ;; assumption is that all bundles are available in the same locales. ;; If there are differences, we make a best effort to give users as much ;; in their desired language as we can (apply clojure.set/union (map :locales (infos)))) ;;; Handling the current locale (defn system-locale "Get the globally set locale" [] (. java.util.Locale getDefault)) (def ^:dynamic *locale* "The current user locale. You should not modify this variable directly. Instead, call user-locale to read its value and use with-user-locale to evaluate forms with the user locale set to a specific value" nil) (defmacro with-user-locale "Evaluate body with the user locale set to locale" [locale & body] `(let [locale# ~locale] (if (instance? java.util.Locale locale#) (binding [*locale* locale#] ~@body) (throw (IllegalArgumentException. (str "Expected java.util.Locale but got " (.getName (.getClass locale#)))))))) (defn user-locale [] "Return the user's preferred locale. If none is set, return the system locale" (or *locale* (system-locale))) ;; @todo lutter 2015-04-21: there are various formats of string locales ;; we need to make sure we have the right one. For example, "en_US" leads ;; to a bad locale, whereas "en-us" works (defn string-as-locale [loc] (java.util.Locale/forLanguageTag loc)) (defn message-locale "The locale of the untranslated messages. This is used as a fallback if we don't have translations for any of the locales that the user would like to have. If you change this, it also needs to be changed in the Makefile that generates resource bundles" [] (string-as-locale "en")) ;;; ResourceBundles (defn bundle-for-namespace "Find the name of the ResourceBundle for the given namespace name" ([namespace] (bundle-for-namespace @info-map namespace)) ([i18n-info-map namespace] (:bundle (get i18n-info-map (first (filter #(.startsWith namespace %) (reverse (sort-by count (keys i18n-info-map))))))))) (defmacro bundle-name "Return the name of the ResourceBundle that the trs and tru macros will use" [] `(bundle-for-namespace ~(namespace-munge *ns*))) (defn get-bundle "Get the java.util.ResourceBundle for the given locale (a string)" [namespace loc] (try (let [base-name (bundle-for-namespace namespace)] (and base-name (gnu.gettext.GettextResource/getBundle base-name loc))) (catch java.lang.NullPointerException e ;; base-name or loc were nil nil) (catch java.util.MissingResourceException e ;; no bundle for the base-name and/or locale nil))) ;;; Message lookup/formatting (defn lookup "Look msg up in the resource bundle for namespace in the locale loc. If there is no resource bundle for it, or the resource bundle does not contain an entry for msg, return msg itself" [namespace loc msg] (let [bundle (get-bundle namespace loc)] (if (and bundle (not-empty msg)) (try (gnu.gettext.GettextResource/gettext bundle msg) (catch java.util.MissingResourceException e ;; no key for msg msg)) msg))) (defn lookup-plural "Look msg up in the resource bundle for namespace in the locale loc. If there is no resource bundle for it, or the resource bundle does not contain an entry for msg, return msg itself" [namespace loc msgid msgid-plural count] (let [bundle (get-bundle namespace loc)] (if (and bundle (not-empty msgid)) (try (gnu.gettext.GettextResource/ngettext bundle msgid msgid-plural count) (catch java.util.MissingResourceException e ;; no key for msg (if (= count 1) msgid msgid-plural))) (if (= count 1) msgid msgid-plural)))) (defn fmt "Use msg as a java.text.MessageFormat and interpolate the args into it according to locale loc. See the documentation for java.text.MessageFormat for the details of what patterns are available." ([msg args] (fmt (user-locale) msg args)) ([loc msg args] ;; we might want to cache these MessageFormat's in some way ;; maybe in a size-bounded LRU cache (.format (new java.text.MessageFormat msg loc) (to-array args)))) (defn translate "Translate a message into the given locale, interpolating as needed. Messages are looked up in the resource bundle associated with the given namespace" [namespace loc msg & args] (fmt loc (lookup namespace loc msg) (to-array args))) (defn translate-plural "Translate a message into the given locale, interpolating as needed. The count argument can be interpolated as {0}. Messages are looked up in the resource bundle associated with the given namespace" [namespace loc msgid msgid-plural count & args] (fmt loc (lookup-plural namespace loc msgid msgid-plural count) (to-array (cons count args)))) (defmacro tru "Translate a message into the user's locale, interpolating as needed" [& args] `(translate ~(namespace-munge *ns*) (user-locale) ~@args)) (defmacro trun "Translate a message into the user's locale observing pluralization, interpolating as needed" [& args] `(translate-plural ~(namespace-munge *ns*) (user-locale) ~@args)) (defmacro trs "Translate a message into the system locale, interpolating as needed" [& args] `(translate ~(namespace-munge *ns*) (system-locale) ~@args)) (defmacro trsn "Translate a message into the system locale observing pluralization, interpolating as needed" [& args] `(translate-plural ~(namespace-munge *ns*) (system-locale) ~@args)) ;; Mark a message for extraction, without translation. This is useful when ;; strings are defined at compile time but need to be translated at run time. (def mark identity) ;; ;; Ring middleware for language negotiation ;; (defn as-number "Parse a string into a float. If the string is not a valid number, return 0" [s] (cond (nil? s) 0 (number? s) s (string? s) (try (Double/parseDouble s) (catch NumberFormatException _ 0)) :else 0)) (defn parse-http-accept-header "Parses HTTP Accept header and returns sequence of [choice weight] pairs sorted by weight." [header] (sort-by second #(compare %2 %1) (remove ;; q values can only have three decimal places; we need to ;; remove all q values that are 0 (fn [[lang q]] (< q 0.0001)) (for [choice (remove str/blank? (str/split (str header) #","))] (let [[lang q] (str/split choice #";")] [(str/trim lang) (or (when q (as-number (get (str/split q #"=") 1))) 1)]))))) (defn negotiate-locale "Given a string sequence of wanted locale (sorted by preference) and a set of available locales, all expressed as strings, find the first string in wanted that is available, and return the corresponding Locale object. This function will always return a locale. If we can't negotiate a suitable locale, we fall back to the system-locale" [wanted available] ;; @todo lutter 2015-05-20: if wanted contains only a country-specific ;; variant, and we have the general variant, we might want to match those ;; up if we don't find a better match. This is not what the HTTP spec ;; says, but helps work around broken browsers. ;; ;; For example, if we have locales #{"de" "es"} available, and the user ;; asks for ["de_AT" "fr"], we should probably return "de" rather than ;; falling back to the message locale (if-let [loc (some available wanted)] (string-as-locale loc) (system-locale))) (defn locale-negotiator "Ring middleware that performs locale negotiation. It parses the Accept-Language header and selects the best available locale according to the user's preference. That locale is set as the user locale while evaluating handler." [handler] (fn [request] ;; @todo lutter 2015-06-03: remove our hand-crafted language ;; negotiation and use java.util.Locale/filterTags instead; this would ;; remove the gnarly parse-http-accept-header business. Requires Java 8 (let [headers (:headers request) parsed (parse-http-accept-header (get headers "accept-language")) wanted (mapv first parsed) negotiated (negotiate-locale wanted (available-locales))] (with-user-locale negotiated (handler request))))) clj-i18n-0.8.0/src/puppetlabs/i18n/main.clj000066400000000000000000000037521306453440400202210ustar00rootroot00000000000000(ns puppetlabs.i18n.main "Some I18N examples" (:gen-class) (:require [puppetlabs.i18n.core :as i18n :refer [tru trs trun trsn mark]])) (def ^:const const-string (mark "I do not speak German")) ;; Some simple examples of using tru/trs ;; The unit tests rely on the message catalog and translation generated for ;; the messages in this file. If you make changes to the messages here, you ;; might also have to change the tests. (defn -main [] ;; You have to use a literal string as the first argument to i18n/tr ;; This code ensures that translators never see this message since ;; xgettext doesn't see the string (let [dont-do-this "Current Locale: {0}"] (println (tru dont-do-this (.toString (i18n/user-locale))))) ;; Very simple localization (println (trs "Welcome! This is localized")) ;; Localizing a previously-extracted string (println (tru const-string)) ;; Localizing an empty string (println (tru "")) (println "-----") (println (trs "")) (println "-----") (println (trun "" "" 1)) (println "-----") (println (trun "" "" 6)) (println "-----") (println (trun "" "non empty string" 1)) (println "-----") (println (trun "non empty string" "" 6)) (println "-----") (println (trsn "" "" 1)) (println "-----") (println (trsn "" "" 6)) (println "-----") (println (trsn "" "non empty string" 1)) (println "-----") (println (trsn "non empty string" "" 6)) (println "-----") ;; Very simple plural system localization (doseq [beers (range 5 0 -1)] (println (trsn "There is one bottle of beer on the wall." "There are {0} bottles of beer on the wall." beers))) ;; String with arguments (let [nprog 3 nmonths 5] (println (i18n/tru "It took {0} programmers {1} months to implement this" nprog nmonths))) ;; String with special formatting (let [nbikes 9000000] (println (i18n/tru "There are {0,number,integer} bicycles in Beijing" nbikes)))) clj-i18n-0.8.0/test/000077500000000000000000000000001306453440400140265ustar00rootroot00000000000000clj-i18n-0.8.0/test/locales.clj000066400000000000000000000004451306453440400161450ustar00rootroot00000000000000;; This file can go anywhere on the class path ;; It is used to add additional locales for testing ;; to the ones that are available for 'normal' use { ;; we use Esperanto for testing :locales #{"eo"} ;; this should be the same as in resources/locales.clj :package "puppetlabs.i18n" } clj-i18n-0.8.0/test/puppetlabs/000077500000000000000000000000001306453440400162055ustar00rootroot00000000000000clj-i18n-0.8.0/test/puppetlabs/i18n/000077500000000000000000000000001306453440400167645ustar00rootroot00000000000000clj-i18n-0.8.0/test/puppetlabs/i18n/Messages_eo.properties000066400000000000000000000004501306453440400233330ustar00rootroot00000000000000# An Esperanto message catalog for testing. # # We use a Java properties file for this, even though the format is a bit # hostile for normal English strings as whitespace and lots of special # characters need to be escaped with a backslash Welcome\!\ This\ is\ localized=Welcome_pseudo_localized clj-i18n-0.8.0/test/puppetlabs/i18n/bin_test.clj000066400000000000000000000120251306453440400212650ustar00rootroot00000000000000(ns puppetlabs.i18n.bin-test (:require [clojure.java.io :as io] [clojure.string :as str] [clojure.test :refer :all] [clojure.java.shell :refer [sh]] [puppetlabs.kitchensink.core :as ks])) (defn git-head-sha [] (-> (sh "git" "rev-parse" "HEAD") :out str/trim)) (defn temp-file-from-resource "Given a prefix & suffix for a temp filename, and the classpath of a Java resource, create a new temp file with the resource's content that will be deleted when the JVM shuts down. Returns a File object for the temp file." [prefix suffix resource-path] (let [temp-file (ks/temp-file prefix suffix) resource (io/resource resource-path)] (io/copy (io/file resource) temp-file) temp-file)) (defn- path [f] (.getPath f)) (deftest add-gitref-test (testing "the src/leiningen/i18n/bin/add-gitref.sh script" (let [current-git-ref-line (re-pattern (str "\n\"X-Git-Ref: " (git-head-sha) "\\\\n\"\\s*\n"))] (testing "adds a git ref header when none is present" (let [po (temp-file-from-resource "i18n-add-gitref-test" ".po" "test-pos/no-git-ref.po") proc (sh "src/leiningen/i18n/bin/add-gitref.sh" (path po))] (is (zero? (:exit proc))) (is (re-find current-git-ref-line (slurp po))))) (testing "replaces git ref header when one is already present" (let [fake-git-ref-line #"\n\"X-Git-Ref: deadb33f\\n\"" po (temp-file-from-resource "i18n-add-gitref-test" ".po" "test-pos/git-ref.po") _ (is (re-find fake-git-ref-line (slurp po))) proc (sh "src/leiningen/i18n/bin/add-gitref.sh" (path po))] (is (zero? (:exit proc))) (let [post-script-contents (slurp po)] (is (re-find current-git-ref-line post-script-contents)) (is (nil? (re-find fake-git-ref-line post-script-contents))))))) (testing "when given a PO file without a Project-Id-Version header" (let [po (temp-file-from-resource "i18n-add-gitref-test" ".po" "test-pos/no-project-id-version.po") {:keys [exit err]} (sh "src/leiningen/i18n/bin/add-gitref.sh" (path po))] (is (= 1 exit)) (is (re-find #"X-Git-Ref\s+header\s+must\s+follow\s+the\s+Project-Id-Version" err)) (is (re-find #"no\s+Project-Id-Version\s+header\s+was\s+found" err)) (is (re-find (re-pattern (path po)) err)))))) (deftest remove-line-numbers-test (testing "the remove-line-numbers.sh script behaves as expected" (let [numbered-po (temp-file-from-resource "i18n-rm-line-nos-test" ".po" "test-pos/line-numbers.po") proc (sh "src/leiningen/i18n/bin/remove-line-numbers.sh" (path numbered-po))] (is (zero? (:exit proc))) (let [post-script-contents (slurp numbered-po)] (is (= post-script-contents (-> (io/resource "test-pos/no-line-numbers.po") io/file slurp))))))) (deftest compare-POTs-test (testing "the compare-POTs.sh script" (let [ref-pot (temp-file-from-resource "i18n-ref" ".pot" "test-pos/diff-ref.pot") cmp-script "src/leiningen/i18n/bin/compare-POTs.sh" cmp-with (fn [resource-path] (let [tmp-prefix (-> resource-path (str/replace #"^test-pos/" "") (str/replace #"\.pot$" "")) diff-pot (temp-file-from-resource tmp-prefix ".pot" resource-path)] (sh cmp-script (path ref-pot) (path diff-pot))))] (testing "finds differences when" (let [diff-pot (temp-file-from-resource "i18n-whole-string" ".pot" "test-pos/diff-whole-string.pot")] (testing "a string is added" (let [cmp (sh cmp-script (path ref-pot) (path diff-pot))] (is (not (zero? (:exit cmp)))))) (testing "a string is removed" (let [cmp (sh cmp-script (path diff-pot) (path ref-pot))] (is (not (zero? (:exit cmp))))))) (testing "a string is changed only slightly" (is (not (zero? (:exit (cmp-with "test-pos/diff-string-edit.pot"))))))) (testing "considers the POTs identical when only" (testing "the order of the strings changes" (is (zero? (:exit (cmp-with "test-pos/diff-order.pot"))))) (testing "the location comment of a string changes" (is (zero? (:exit (cmp-with "test-pos/diff-location.pot"))))) (testing "a translation is added for a string" (is (zero? (:exit (cmp-with "test-pos/diff-translated.pot"))))))))) clj-i18n-0.8.0/test/puppetlabs/i18n/core_test.clj000066400000000000000000000252601306453440400214520ustar00rootroot00000000000000(ns puppetlabs.i18n.core-test (:require [clojure.test :refer :all] [puppetlabs.i18n.core :refer :all] [clojure.java.io :as io] [clojure.pprint :refer [pprint]])) ;; Set the JVM's default locale so we run in a known environment (. java.util.Locale setDefault (string-as-locale "en-US")) (def de (string-as-locale "de-DE")) (def eo (string-as-locale "eo")) (def en (string-as-locale "en-US")) (def welcome_en "Welcome! This is localized") (def welcome_de "Willkommen ! Draußen nur Kännchen") (def welcome_eo "Welcome_pseudo_localized") (def one_bottle "There is one bottle of beer on the wall.") (def n_bottles "There are {0} bottles of beer on the wall.") (def one_bottle_en "There is one bottle of beer on the wall.") (def one_bottle_de "Es gibt eine Flasche Bier an der Wand.") (def six_bottle_en "There are 6 bottles of beer on the wall.") (def six_bottle_de "Es gibt 6 Flaschen Bier an der Wand.") (deftest handling-of-user-locale (testing "user-locale defaults to system-locale" (is (= (system-locale) (user-locale)))) (testing "with-user-locale changes user-locale" (with-user-locale de (is (= de (user-locale))))) (testing "user-locale is conveyed to future" (with-user-locale de (is (= de @(future (user-locale)))))) (testing "with-user-locale fails when not passed a java.util.Locale" (is (thrown? IllegalArgumentException (with-user-locale "de" nil))))) (deftest test-tru (testing "tru with no user locale" (is (= welcome_en (tru welcome_en)))) (testing "tru in German" (with-user-locale de (is (= welcome_de (tru welcome_en))))) (testing "tru in Esperanto" ;; We use Esperanto as our test locale (with-user-locale eo (is (= welcome_eo (tru welcome_en)))))) (deftest test-trun (testing "trun with no user locale" (is (= one_bottle_en (trun one_bottle n_bottles 1))) (is (= six_bottle_en (trun one_bottle n_bottles 6)))) (testing "trun in German" (with-user-locale de (is (= one_bottle_de (trun one_bottle n_bottles 1)))) (with-user-locale de (is (= six_bottle_de (trun one_bottle n_bottles 6)))))) (deftest test-trs (testing "trs with no user locale" (is (= welcome_en (trs welcome_en)))) (testing "trs with a user locale" (with-user-locale de (is (= welcome_en (trs welcome_en)))))) (deftest test-trsn (testing "trsn with no user locale" (is (= one_bottle_en (trsn one_bottle n_bottles 1))) (is (= six_bottle_en (trsn one_bottle n_bottles 6)))) (testing "trsn with a user locale" (with-user-locale de (is (= one_bottle_en (trsn one_bottle n_bottles 1)))) (with-user-locale de (is (= six_bottle_en (trsn one_bottle n_bottles 6)))))) (deftest test-empty-string-msgid-fallback-to-pot-no-header (testing "trsn with no user locale" (is (= "" (trsn "" "" 1))) (is (= "" (trsn "" "" 6))) (is (= "" (trsn "" "fred" 1))) (is (= "fred" (trsn "" "fred" 6))) ; msgid/msgstr not in po/bundle render correctly (is (= "fred" (trsn "fred" "" 1))) (is (= "" (trsn "fred" "" 6)))) (testing "trsn with a user locale" (with-user-locale de (is (= "" (trsn "" "" 1))) (is (= "" (trsn "" "" 6))) (is (= "" (trsn "" "fred" 1))) (is (= "fred" (trsn "" "fred" 6))) ; msgid/msgstr not in po/bundle render correctly (is (= "fred" (trsn "fred" "" 1))) (is (= "" (trsn "fred" "" 6))))) (testing "trun can display an empty string" (with-user-locale de (is (= "" (trun "" "" 1))) (is (= "" (trun "" "" 6))) (is (= "" (trun "" "fred" 1))) (is (= "fred" (trun "" "fred" 6))) ; msgid/msgstr not in po/bundle render correctly (is (= "fred" (trun "fred" "" 1))) (is (= "" (trun "fred" "" 6))))) (testing "trs can display an empty string" (with-user-locale de (is (= "" (trs ""))) (is (= " " (trs " "))))) (testing "tru can display an empty string" (with-user-locale de (is (= "" (tru ""))) (is (= " " (tru " ")))))) ;; ;; Helper files in dev-resources; they have the same format as the ;; locales.clj file that the Makefile generates ;; (defn one-locale-file [] [(io/resource "test-locales/de-ru-es.clj")]) (defn two-locale-files [] [(io/resource "test-locales/de-ru-es.clj") (io/resource "test-locales/fr-it.clj")]) (defn conflicting-locale-files [] [(io/resource "test-locales/de-ru-es.clj") (io/resource "test-locales/merge-eo.clj") (io/resource "test-locales/conflicting-de.clj")]) (defn uberjar-locale-file [] [(io/resource "test-locales/uberjar-en-de.clj")]) (defn merge-locale-files [] [(io/resource "test-locales/de-ru-es.clj") (io/resource "test-locales/merge-eo.clj")]) (defn multi-pacakge-locale-file [] [(io/resource "test-locales/multi-pacakge-es.clj")]) (defn overlapping-pacakge-locale-file [] [(io/resource "test-locales/overlapping-packages.clj") (io/resource "test-locales/multi-pacakge-es.clj")]) (deftest test-infos (with-redefs [puppetlabs.i18n.core/info-files one-locale-file] (testing "info-map" (is (= ["example.i18n"] (keys (info-map')))) (is (= "example.i18n.Messages" (:bundle (get (info-map') "example.i18n"))))) (testing "available-locales" (is (= #{"de" "ru" "es"} (available-locales))))) (with-redefs [puppetlabs.i18n.core/info-files overlapping-pacakge-locale-file] (testing "bundle-for-namespace ordering with overlapping packages and same length namespaces" (are [bundle i18n-ns] (= bundle (bundle-for-namespace (info-map') i18n-ns)) "overlapped_package.i18n.Messages" "example" "multi_package.i18n.Messages" "example.i18n" "multi_package.i18n.Messages" "alternate.i18n" "overlapped_package.i18n.Messages" "alt3rnat3.i18n" "overlapped_package.i18n.Messages" "example.i18n.another_package"))) (with-redefs [puppetlabs.i18n.core/info-files two-locale-files] (testing "info-map #2" (is (= #{"example.i18n" "other.i18n"} (into #{} (keys (info-map'))))) (is (= "example.i18n.Messages" (:bundle (get (info-map') "example.i18n"))))) (testing "available-locales #2" (is (= #{"it" "fr" "de" "ru" "es"} (available-locales)))) (testing "bundle-for-namespace" (is (= "example.i18n.Messages" (bundle-for-namespace (info-map') "example.i18n"))) (is (= "example.i18n.Messages" (bundle-for-namespace (info-map') "example.i18n.dog.cat"))) (is (nil? (bundle-for-namespace (info-map') "example"))) (is (= "other.i18n.Messages" (bundle-for-namespace (info-map') "other.i18n.abbott.costello"))))) (with-redefs [puppetlabs.i18n.core/info-files conflicting-locale-files] (testing "conflicting locales" (is (thrown-with-msg? Exception #"Invalid locales info: .* are both for package .* but set different bundles" (info-map'))))) (with-redefs [puppetlabs.i18n.core/info-files uberjar-locale-file] (testing "info-map" (is (= #{"puppetlabs.i18n" "puppetlabs.foo"} (set (keys (info-map'))))) (is (= "puppetlabs.foo.Messages" (:bundle (get (info-map') "puppetlabs.foo")))) (is (= "puppetlabs.i18n.Messages" (:bundle (get (info-map') "puppetlabs.i18n"))))) (testing "available-locales" (is (= #{"de" "en" "fr"} (available-locales))))) (with-redefs [puppetlabs.i18n.core/info-files merge-locale-files] (testing "merged langauges" (is (= #{"de" "ru" "es" "eo"} (available-locales))) (is (= 1 (count (info-map')))))) (with-redefs [puppetlabs.i18n.core/info-files multi-pacakge-locale-file] (testing "multi package locales" (is (= "multi_package.i18n.Messages" (bundle-for-namespace (info-map') "example.i18n.abbott.costello"))) (is (= "multi_package.i18n.Messages" (bundle-for-namespace (info-map') "alternate.i18n.abbott.costello")))))) (deftest test-as-number (testing "convert number strings properly" (is (= 0.1 (as-number "0.1"))) (is (= 1.0 (as-number "1.0"))) (is (= 0.5 (as-number 0.5)))) (testing "turns garbage into 0" (is (= 0 (as-number "xyz"))) (is (= 0 (as-number true))) (is (= 0 (as-number nil))))) (deftest test-parse-http-accept-header (let [p #(parse-http-accept-header %)] (testing "parses q-values properly" (is (= '(["de-DE" 1]) (p "de-DE"))) (is (= '(["de-DE" 0.5]) (p "de-DE;q=0.5"))) (is (= '() (p "de-DE;q=0.0"))) (is (= '() (p "de-DE;q=garbage")))) (testing "sorts locales properly" (is (= '(["de-DE" 1] ["de" 1]) (p "de-DE, de"))) (is (= '(["de-DE" 1.0] ["de" 1]) (p "de-DE;q=1, de"))) (is (= '(["de-DE" 1] ["de" 0.9] ) (p "de;q=0.9, de-DE"))) (is (= '(["de-DE" 0.8] ["de" 0.7]) (p "de;q=0.7, de-DE;q=0.8"))) (is (= '(["de-DE" 0.8] ["de" 0.7]) (p "de-DE;q=0.8 , de;q=0.7, "))) (is (= '(["de" 1] ["en-gb" 0.8] ["en" 0.7]) (p "de, en-gb;q=0.8, en;q=0.7")))))) (deftest test-negotiate-locale (with-redefs [puppetlabs.i18n.core/system-locale #(string-as-locale "oc")] (let [check (fn [exp wanted] (is (= (string-as-locale exp) (negotiate-locale wanted #{"de" "fr-FR" "en"}))))] (testing "works" (check "de" ["it" "de" "en"]) (check "en" ["it" "en" "de"]) (check "en" ["en_US" "en" "de"]) (check "oc" ["da" "no"])) ;; The next two tests are here to document current behavior, not ;; because that behavior is nevessarily a great idea (testing "country variants for a locale we have are ignored" (check "oc" ["de-CH" "it"])) (testing "generic locale when we have country variant is ignored" (check "fr-FR" ["fr" "fr-FR"]))))) (deftest test-locale-negotiator (with-redefs [puppetlabs.i18n.core/available-locales (fn [] #{"de" "fr-FR" "en"}) puppetlabs.i18n.core/system-locale #(string-as-locale "oc")] (let [neg (locale-negotiator (fn [request] (user-locale))) mk-request (fn [accept] {:headers {"accept-language" accept}}) check (fn [exp accept] (is (= (string-as-locale exp) (neg (mk-request accept)))))] (testing "works for valid headers" (check "de" "de") (check "de" "de_DE, de;q=0.9, en;q=0.8") (check "oc" "it, fr")) (testing "falls back to system-locale for empty/invalid headers" (map #(check "oc" %) ["" nil "en;q=garbage" ",,," ",;," "xyz" "de-US"]) (is (= (system-locale) (neg {:headers {}}))) (is (= (system-locale) (neg {})))) (testing "conveys the locale" (is (= (string-as-locale "de") ((locale-negotiator (fn [request] @(future (user-locale)))) (mk-request "de")))))))) clj-i18n-0.8.0/test/puppetlabs/i18n/utils_test.clj000066400000000000000000000031471306453440400216620ustar00rootroot00000000000000(ns puppetlabs.i18n.utils-test (:require [clojure.test :refer :all] [leiningen.i18n.utils :as utils])) (deftest replace-first-in-multiline (testing "replace-first-in-multiline" (testing "only changes lines that begin with pattern" (is (= "asdf ABC-DEF stuff\nOther stuff" (utils/replace-first-in-multiline "asdf ABC-DEF stuff\nOther stuff" #"ABC-DEF" "New stuff")))) (testing "only changes first instance of pattern" (is (= "New stuff\nOther stuff\nABC-DEF" (utils/replace-first-in-multiline "ABC-DEF\nOther stuff\nABC-DEF" #"ABC-DEF" "New stuff")))) (testing "can pass regex to select to the EOL" (is (= "New stuff\nOther stuff" (utils/replace-first-in-multiline "ABC-DEF stuff\nOther stuff" #"ABC-DEF.*" "New stuff")))) (testing "can pass regex to select EOL on a single line" (is (= "New stuff" (utils/replace-first-in-multiline "ABC-DEF stuff" #"ABC-DEF.*" "New stuff")))) (testing "can pass regex to select EOL on last line" (is (= "\nNew stuff" (utils/replace-first-in-multiline "\nABC-DEF stuff" #"ABC-DEF.*" "New stuff"))))))