Fast I18n backend to keep things running smoothly.
I18nema is a drop-in replacement for I18n::Backend::Simple, for faster lookups (15-20%) and quicker GC runs (ymmv). Translations are stored outside of the ruby heap, and lookups happen in C (rather than the usual inject on nested ruby hashes).
How do I use it?
gem 'i18nema' # or 'i18nema-19' if you're on Ruby 1.9
Then do something like this in an initializer:
I18n.backend = ::.
You can pull in additional features, e.g.
As with regular I18n, you should probably load translations before you
fork, so that all processes can use the same translations in memory. In
an initializer, just do
What sort of improvements will I see?
Loading all the translations into memory is dramatically faster with I18nema (about 4x). While this is just a one time hit, it's pretty noticeable when you're waiting on it (e.g. console, specs). In canvas-lms, I18nema brings it down to just over half a second (from almost 2.5).
Faster GC Runs
Because there are fewer ruby objects, the periodic GC runs will be proportionally faster. How much faster is a question of how many translations you have versus how many other ruby objects. Applications that are localized in more languages should see a bigger boost (since the translations make up a bigger share of the original ObjectSpace).
For example, canvas-lms is translated into seven other languages, and I18nema reduces (startup) ObjectSpace by about 18% and GC runtime by about 11%.
I18nema also moves I18n's normalized_key_cache into C structs. This key cache grows over time (it eventually holds a key/value for every translation key used in the app), so that's another area where I18nema is nicer on ObjectSpace than vanilla I18n.
Faster Translation Lookups
Simple lookups (i.e. no options or interpolation) take a bit over 15% less time.
Lookups with options see slightly bigger gains (over 20% less time), in
part due to some speedups on the ruby side of things (I18n uses
Hash#except, which is quite slow when you have a long list of
Show me the benchmarks
Here are some basic ones done with
Benchmark.bmbm (edited for brevity)
I18n.translate 100000 times on 4 different translation keys.
translate(n) denotes how many parts there are in the key,
I18n.t('foo') -> 1,
I18n.t('foo.bar') -> 2
translate (no options)
user system total real translate(1): 0.900000 0.010000 0.910000 ( 0.910228) translate(2): 1.010000 0.010000 1.020000 ( 1.009545) translate(3): 1.020000 0.010000 1.030000 ( 1.028098) translate(4): 1.210000 0.000000 1.210000 ( 1.214737)
user system total real translate(1): 1.000000 0.000000 1.000000 ( 1.007367) translate(2): 1.260000 0.000000 1.260000 ( 1.268323) translate(3): 1.320000 0.000000 1.320000 ( 1.315132) translate(4): 1.390000 0.010000 1.400000 ( 1.393478)
translate with options (locale, interpolation)
user system total real translate(1): 0.950000 0.000000 0.950000 ( 0.943904) translate(2): 1.040000 0.000000 1.040000 ( 1.036595) translate(3): 1.060000 0.010000 1.070000 ( 1.059588) translate(4): 1.240000 0.000000 1.240000 ( 1.237322)
user system total real translate(1): 1.090000 0.000000 1.090000 ( 1.099866) translate(2): 1.360000 0.000000 1.360000 ( 1.364869) translate(3): 1.430000 0.000000 1.430000 ( 1.425103) translate(4): 1.500000 0.010000 1.510000 ( 1.500952)
OK, so what's the catch?
I18nema is still a work in progress, so there are some compatibility notes you should be aware of:
I18nema requires ruby 1.9.3 or later.
I18nema only supports
.yml translation files (no
I18nema requires UTF-8
.yml files. That means that your translations
should actually be in their UTF-8 form (e.g. "Contraseña"), not some
escaped representation. I18nema uses a simplified syck implementation
and does not support many optional yml types (e.g.
I18nema doesn't yet support symbols as translation values (note that
and defaults work
just fine). Symbol values in your
.yml file can be used in the same
way that symbol defaults can, i.e. they tell I18n to find the
translation under some other key.