# i18n ![Build Status](https://github.com/crimson-knight/i18n.cr/actions/workflows/ci.yml/badge.svg) Internationalization API ## Changes from 0.4.0 to 0.4.1 - Supported Crystal versions of >= 0.35 - add Backend::Yaml#exists? to check whether given translation key exists - add I18n.exists? - add docs to I18n module public methods (most of wording was taken from the ruby I18n repo) - Fixed the iter argument in `translate` to properly return the correct index Example ```crystal # Array we are looking into ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] I18n.translate("__formats__.date.day_names", iter: 2) # Returns "Wednesday" ``` ## Breaking changes from 0.3 to 0.4 - Pluralization rules are now fully suites [CLDR convention](http://cldr.unicode.org/index/cldr-spec/plural-rules). Specifically `en` pluralization no more returns `zero` ## Breaking changes from 0.2 to 0.3 - The first day of the week is now Monday according to ISO 8601. - The nil value in `month_names` and `abbr_month_names` was removed. ## Installation Add this to your application's `shard.yml`: ```yaml dependencies: i18n: github: crimson-knight/i18n.cr ``` ## Usage ``` crystal I18n.translate( "some.dot.separated.path", # key : String {attr_to_interpolate: "a"}, # options : Hash | NamedTuple? = nil "pt", # force_locale : String = nil 2, # count : Numeric? = nil "default translation", # default : String? = nil nil # iter : Int? = nil ) I18n.localize( Time.utc_now, # object : _ "pt", # force_locale : String = I18n.config.locale :time, # scope : Symbol? = :number "long" # format : String? = nil ) ``` ### Arguments interpolation Translation may include argument interpolation. For doing this use regular crystal named interpolation placeholder and pass hash or named tuple as a `options` argument: ```yaml message: new: "New message: %{text}" ``` ```crystal # New message: hello I18n.translate("message.new", {text: "hello"}) I18n.translate("message.new", {:text => "hello"}) I18n.translate("message.new", {"text" => "hello"}) ``` Also any extra key-value pair will be ignored and missing one will not cause any exception: ```crystal I18n.translate("message.new", {message: "hello"}) # New message: %{text} ``` ### Configuration ```crystal require "i18n" I18n.load_path += ["spec/locales"] I18n.init # This will load locales from all specified locations I18n.default_locale = "pt" # default can be set after loading translations ``` There is a [handler](https://github.com/TechMagister/kemalyst-i18n) for Kemalyst that bring I18n configuration. ### Note on YAML Backend Putting translations for all parts of your application in one file per locale could be hard to manage. You can store these files in a hierarchy which makes sense to you. For example, your config/locales directory could look like this: ```console locales |--defaults |----en.yml |----pt.yml |--models |----en.yml |----pt.yml |--views |----users |------en.yml |------pt.yml ``` This way you can separate model related translations from the view ones. To require all described subfolders at once use `**` - `I18n.load_path += ["locals/**/"]` #### Date/Time Formats To localize the time (or date) format you should pass `Time` object to the `I18n.localize`. To pick a specific format path `format` argument: ```crystal I18n.localize(Time.local, scope: :date, format: :long) ``` > By default `Time` will be localized with `:time` scope. To specify formats and all need localization information (like day or month names) fill your file in following way: > NOTE: According to ISO 8601, Monday is the first day of the week ```yaml __formats__: date: formats: default: '%Y-%m-%d' # is used by default long: '%A, %d de %B %Y' month_names: # long month names - Janeiro - Fevereiro - Março - Abril - Maio - Junho - Julho - Agosto - Setembro - Outubro - Novembro - Dezembro abbr_month_names: # month abbreviations - Jan - Fev # ... day_names: # fool day names - Segunda # ... abbr_day_names: # short day names - Seg # ... ``` Format accepts any crystal `Time::Format` directives. Also following directives will be automatically localized: | Directive | Description | Key | |---|---|---| | `%a` | short day name | `date.abbr_day_names` | | `%A` | day name | `date.day_names` | | `%b` | short month name | `date.abbr_month_names` | | `%B` | month name | `date.month_names` | | `%p` | am-pm (lowercase) | `time.am`/`time.pm` | | `%P` | AM-PM (uppercase) | `time.am`/`time.pm` | #### Pluralization In many languages — including English — there are only two forms, a singular and a plural, for a given string, e.g. "1 message" and "2 messages". Other languages (Arabic, Japanese, Russian and many more) have different grammars that have additional or fewer plural forms. The `count` interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules defined by CLDR: ```yaml message: one: "%{count} message" other: "%{count} messages" ``` ```crystal I18n.translate("message", count: 1) # 1 message I18n.translate("message", count: 2) # 2 messages I18n.translate("message", count: 0) # 0 messages ``` > `count` should be passed as argument - not inside of `options`. Otherwise regular translation lookup will be applied. I18n defines default [CLDR rules](http://cldr.unicode.org/index/cldr-spec/plural-rules) for many locales (see `src/i18n/config/plural_rules`), however they can be overwritten: ```crystal I18n.plural_rules["ru"] = ->(n : Int32) { if n == 0 :zero elsif ((n % 10) == 1) && ((n % 100 != 11)) # 1, 21, 31, 41, 51, 61... :one elsif ([2, 3, 4].includes?(n % 10) && ![12, 13, 14].includes?(n % 100)) # 2-4, 22-24, 32-34... :few elsif ((n % 10) == 0 || ![5, 6, 7, 8, 9].includes?(n % 10) || ![11, 12, 13, 14].includes?(n % 100)) # 0, 5-20, 25-30, 35-40... :many else :other end } ``` ```yaml kid: zero: 'нет детей' one: '%{count} ребенок' few: '%{count} ребенка' many: '%{count} детей' other: '%{count} детей' ``` ```crystal I18n.locale = "ru" I18n.translate("kid", count: 0) # нет детей I18n.translate("kid", count: 1) # 1 ребенок I18n.translate("kid", count: 2) # 2 ребенка I18n.translate("kid", count: 6) # 6 детей ``` #### Iteration To store several alternative objects under one localization key they could be just listed in the file and later retrieved using `iter` argument: > NOTE : The first index is `0` ```yaml __formats__: date: day_names: [Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday] ``` ```crystal I18n.translate("__formats__.date.day_names", iter: 2) # >>> "Wednesday" ``` ### Embedding translations inside your binary You can embed translations inside your binary by using the following macro call: ```crystal I18n::Backend::Yaml.embed(["some/locale/directory", "some/other/locale/directory"]) ``` ## Development TODO : - [ ] Add more backends ( Database, json based, ruby based ( why not ? )) - [ ] others ( there is always something to add ... or remove ) ## Contributing 1. Fork it ( https://github.com/crimson-knight/i18n/fork ) 2. Create your feature branch (git checkout -b my-new-feature) 3. Commit your changes (git commit -am 'Add some feature') 4. Push to the branch (git push origin my-new-feature) 5. Create a new Pull Request ## Contributors - [[crimson-knight]](https://github.com/crimson-knight) Seth Tucker (New owner as of July 2021) - [[imdrasil]](https://github.com/imdrasil) Roman Kalnytskyi Inspiration taken from: - https://github.com/whity/crystal-i18n - https://github.com/mattetti/i18n Special thank you to [[TechMagister]](https://github.com/Techmagister) for being the original owner and creator of this shard.