Building upon the criticism of my first version, I have a new translation API at
This allows you to translate interpolated strings in the native Crystal syntax. There is no need for you to write your code in a special string format for translation.
This will be checked in over BrucePerens/i18n, my older translation shard,
when it’s more stable. The paint is still wet on this software, and I got it done just in time to deliver a Raw Crystal talk about it tomorrow, but it’s available for you to review and criticize.
This addresses some issues with the first version:
Translations can change the order of interpolated expressions.
The programmer can add explanations of the meaning of the interpolated expressions, for the translator.
The programmer can add interpolated expressions that are not used in the native-language string, so that things that don’t appear in the native language, like gendered nouns, can be handled.
It is possible to modify the translation table at run-time, so that I can implement a version of http://github.com/BrucePerens/i18n-edit for Lucky, etc. This allowed the Rails programmer to click on strings in a running program and enter their translations directly!
I really like the concept of extracting translation keys at compile time using macros. I have been pondering about that myself. It should be more reliable than grepping the source code. The downside however is that it only considers reachable code. For libraries that could be a slight issue, but not if you’re aiming for ~100% test coverage.
I’m not too fond of the translation API itself, though. So I’ll leave a few thoughts here, maybe you’ve already considered that:
Using positional interpolation doesn’t make sense to me. Interpolated expressions should always have a named identifier to provide context for the translators. It can also easily break if the interpolations in the source translation are re-ordered.
The gettext-style translation model with source translations specified in a “base language” IMO isn’t a great solution. It installs one translation as superior which potentially makes other translations lacking. Especially problematic is that text modifications for this language need to happen in the source code directly, wheras every other language can be edited externally.
A key-based translation model (for examle like in Ruby’s i18n module) seems much better to me because it clearly conveys purpose to translation items. And it avoids the weird name: argument to disambiguate homonymous source translations. It essentialy means using name: everyhwere instead of a source translation.
fluent looks like a powerful translation model. I’d recommend looking into that. It’s implementation may become a bit more complex, but that seems absolutely justified in order to properly handle the complexities of natural languages.
It would be easy enough to add names of the interpolated expressions to the literal string portions of the interpolated string, and then let the macros strip them out and use them as named arguments rather than positional ones. For example “string #{1+1}$quantity string” would get to the translator as “string $quantity string”. I may be confused but it sounds like you are suggesting making the name the first positional argument. I can provide a macro other than t that offers that kind of interface. Which one to use would be a developer preference.
My surmise is that the most used internationalisation API will end up being the one that adds the very least to the program. Meaning t and the interpolated string. I think the biggest problem is resistance to internationalizing at all, and thus it pays to provide an ultra low resistance path. But it doesn’t have to be the only API.
My argument was mostly that translators shouldn’t have to deal with inexpressive numbered placeholders. I didn’t make any suggestions about the Crystal API.
But my suggestion for that would be to allow only calls or variables as interpolation values and map their names to the translation arguments.
Then you couldn’t interpolate arbitrary expressions but need to assign them to a variable first:
quantity = 1 + 1
t("string #{quantity} string")
And a translation format would be schnur $quantity schnur.
@straight-shoota I took a good look at fluent and I like the idea of intensively handling grammar differences across languages. I am not yet clear how that translates to Crystal, some sort of meta-string class that interprets its contents and retrieves strings from dictionaries according to what it finds might work best. For example:
numeric(name, 1+1) goes to the translation table for these: one: "There is just #{variable} coin left", two: "There are only #{variable} coins remaining.", many: "There are #{variable} coins.". This would come with gender() (which can be male/female but can also add neuter, nonbinary, etc.), and I have to figure out the names for other common branches on grammar.
So, potentially that can be used as an argument to the t macro I have already defined, but I agree that t should be expanded to name variables, either by extracting the name from #{variable} or from making an attribution of it as in "#{1+1}$coins remaining" using the space as both a boundary and part of the literal string, or "#{1+1}$coins$: you are running low!" with an explicit boundary.
And then I propose to add tr(name : String|Symbol, **named_variables) as the version with no native language.
I definitely appreciate your assistance in seeing this, since I am not the best suited to determine what the API should be, as an “ugly american”. I confess to mainly knowing how to order food and find the bathroom in languages other than English.
Unfortunately every time I’ve worked with translation teams they seem to want/prefer a giant .po/.pot file or have it uploaded to a service like Transifex. This way they can use their existing tools, like translation memory. However, they do like to see if the changes they’ve made actually work in the application and make sense in context.