As a Developer
When building a new feature that needs labels, it’s not clear what those labels will be during the beginning, and it’s always a pain to go hunt them down after the fact to properly labelize them. That’s why cr-i18n
allows you to pass in basic strings from the getgo:
require "cr-i18n"
CrI18n.compiler_load_labels("./path/to/labels")
label("This is a label") # => "This is a label"
label("Hello #{name}!") # => ...
label("You have #{count} apples") # doesn't make sense yet if count == 1
At some point you’ll need to set what language / locale the intended user is using, and there are two approachs to doing that. One is by passing in the locale directly to locale
as the second parameter, or alternatively (and more easily), you can set the locale for the request. The below are equivalent:
NOTE: Free form strings as labels shouldn’t contain the semicolon (‘;’) character, as this is used under the hood as a delimiter when enforcing labels with the compiler check. Semicolons can be used freely within label files though.
label(some.label.path) # no locale set, will resolve from root
CrI18n.with_locale("en-Us") do
# The below will try and resolve the label from "en-Us" locale first, then
# "en" language if not found. If the label isn't in either "en" or "en-Us",
# and `CrI18n.resolve_to_root` is true (default), then it will look in the root labels.
label(some.label.path) # Search in "en-Us", then "en", then root
label(some.label.path, "en-Uk") # Search in "en-Uk", then "en", then root. Overrides context locale
end
CrI18n.root_locale = "en-Us"
label(some.label.path) # Search in "en-Us", then "en", then root
CrI18n.resolve_to_root = false
label(some.label.path) # Search in "en-Us", then "en"
If a label target doesn’t exist, it will record that label as missing and then return the passed in label target as is (such as a String, verbatim). To get a list of all labels that are missing and not using the compiler flag to check for them, you can check them via CrI18n.missed
.
Writing and Running Tests
Chances are you’ll want to verify that content is appearing as it’s intended to users, and labels will be a part of that. Since labels can change for any number of reasons, and translation often doesn’t occur until after feature development is complete, cr-i18n
has a running_tests
mode that changes how labels are resolved.
Spec.before_suite do
CrI18n.running_tests = true
end
...
it "renders correctly"
# While running_tests is true, label resolving will be consistent regardless of what labels are loaded, making
# for less brittle test writing
label(my.label.target, parma1: "with params").should eq "my.label.target param1='with params'"
end
After Feature Development Completes
After the feature is complete, it’s time to transfer all of these labels to a proper root.yml
(or adjacent) file. You can structure these files however you want. To discover all label
invocations that don’t use valid label path, you can build your application with the -Denforce_labels
flag:
> crystal build -Denforce_labels src/my_app.cr
Which will throw a compiler error for any missing labels:
Showing last frame. Use --error-trace for full trace.
In src/cr-i18n/enforce_labels_check.cr:3:5
3 | {% begin %}
^
Error: Found errors in compiled labels under "./src":
Missing label 'Hello #{name}!' at ./src/my_app.cr:7 wasn't found in labels loaded from c./path/to/labels
Missing label 'This is a label' at ./src/my_app.cr:5 wasn't found in labels loaded from c./path/to/labels
Missing label 'You have #{count} apples' at ./src/my_app.cr:9 wasn't found in labels loaded from c./path/to/labels
Let’s say you create or add these labels to a new root label file like so:
labels:
intro: This is a label
greeting: Hello %{name}!
apple:
# Note: the root locale only supports the "other" plural target. See Pluralization for more details
other: You have %{count} apples
Then update the above label
calls to:
...
label("labels.intro") # Can use a String to for the label path
label(labels.greeting, name: "Troy") # Or no String (macro will convert it to one)
# You can use string interpolation for the label path as well
fruit = "apple"
label("labels.#{fruit}", count: 1) # => "You have one apple"
label("labels.#{fruit}", count: 42) # => "You have 42 apples"
Translating the Label Files
Through human effort or robotic assistance, all labels from root label files should be translated to any desired supported languages and locales. To ensure that all languages and locales do have all labels from the root, you can build with the -Denforce_label_parity
which will cause cr-i18n
to inspect all labels in all languages and locales, and make sure that all labels are present as defined in root.