Use your gettext translations in your React components
… and check why 5600+ Rails engineers read also this
Use your gettext translations in your React components
In one of our projects, we are using gettext for i18n. We were putting Handlebars x-handlebars-template
templates directly in Haml templates to provide translated views for frontend part - all translations were made on backend. Recently we have rewritten our frontend to React and decided not to use ruby for translations anymore.
Transitioning from backend to frontend
During rewrite, we created an simple API endpoint on backend that was returning translation for given key and locale and mixed it with React component that was asynchronously getting translations. The code was pretty simple and was using jQuery promises:
React = require('react')
{span} = React.DOM
cache = {}
lookupCache = (key, locale) ->
cache[locale] ||= {}
cache[locale][key]
updateCache = (key, locale, translation) ->
cache[locale] ||= {}
cache[locale][key] = translation
translate = (key, locale) ->
if translation = lookupCache(key, locale)
new $.Deferred()
.resolve(translation)
.promise()
else
$.ajax
url: '/api/gettext'
data:
key: key
locale: locale
dataType: 'JSON'
type: 'GET'
.then (response) ->
updateCache(key, locale, response.translation)
.fail ->
key
Translation = React.createClass
displayName: 'translation'
getInitialState: ->
translation: null
componentDidMount: ->
translate(@props.key, @props.locale)
.always (translation) =>
if @isMounted()
@setState(translation: translation)
render: ->
if @state.translation
span(null, @state.translation)
else
span(null, '...')
module.exports = (key, locale) ->
React.createElement(Translation, key: key, locale: locale)
i18next - move your gettext to frontend
This approach was good for quick start but did not scale - it required multiple ajax calls to backend on each page render, so we decided to find something better. After some research we have chosen i18next - full-featured i18n JS library that have pretty good compatibility with gettext (including pluralization rules). With i18next you can easily return translations using almost the same API as in gettext:
# {"key": "translation"}
i18n.t('key') # => translation for key
This library also supports variables inside translation keys:
# {"key with __variable__": "translation with __variable__"}
i18n.t('key with __variable__', {variable: 'value'}) # => translation with value
It has also sprintf support:
# {"Some text with string %s and number %d": "Hello %s! You're number %d!"}
i18n.t('Some text with string %s and number %d', {postProcess: 'sprintf', sprintf: ['world', 1]}) # => Hello world! You're number 1!
And supports plurar forms (even for languages with multiple plural forms):
# {"key": "__count__ banana", "key_plural": "__count__ bananas"}
i18n.t("key", {count: 0}) # => 0 bananas
i18n.t("key", {count: 1}) # => 1 banana
i18n.t("key", {count: 5}) # => 5 bananas
There are much more configuration options and features in i18next library so you’d better look at their docs.
To convert our gettext .po
files to json format readable by i18next, we’re using i18next-conv tool and store generated json in public/locale
directory of our Rails app. Here’s a simple script we’re using during deploy to compile JS translations (script/compile_js_i18n
):
#!/bin/bash
npm install .
for locale in de en fr pl; do
for file in acme.po support.po tags.po; do
./node_modules/.bin/i18next-conv -l $locale -s locale/$locale/$file -t public/locale/$locale/${file/.po/.json}
done
done
To use it, just run script/compile_js_i18n
in your app’s root directory (but make sure you have node & npm installed and "i18next-conv": "~> 0.1.4"
line in your package.json
file before). What’s great about i18next-conv, it has built-in plural forms for many languages.
i18next has also a bunch of initialization options. Here’s our setup that works in our app:
i18n.init
ns:
defaultNs: 'acme'
namespaces: ['acme', 'support', 'tags']
lngWhiteList: ['de', 'en', 'fr', 'pl']
fallbackLng: 'en'
resGetPath: '/locale/%{lng}/%{ns}.json'
interpolationPrefix: '%{'
interpolationSuffix: '}'
keyseparator: '<'
nsseparator: '>'
Some of those initialization options need more explanation. First, we’re using variable interpolation in our gettext translations. They have format different than i18next defaults (%{variable_name}
instead of __variable_name__
) so we had to set interpolationPrefix
and interpolationSuffix
. Second, since we’re using english translations as gettext msgids (usually full sentences), we need to change key and namespace separator (keyseparator
and nsseparator
options). The default key separator in i18next is a dot (.
) and namespace separator is a colon (:
) and that was making most of our translations useless, since they were not translated at all when translation key contained .
or :
. We also had to change resGetPath
since we decided to store our json in public/locale
(e.g. public/locale/en/acme.json
for acme namespace).
In our app, we wrapped initialization code in i18n
CommonJS module for easier use:
i18n = require('i18next')
i18n.init(
ns:
defaultNs: 'acme'
namespaces: ['acme', 'support', 'tags']
lngWhiteList: ['de', 'en', 'fr', 'pl']
fallbackLng: 'en'
resGetPath: '/locale/%{lng}/%{ns}.json'
interpolationPrefix: '%{'
interpolationSuffix: '}'
keyseparator: '<'
nsseparator: '>'
})
module.exports = i18n
With this helper, you don’t need to initialize library each time you use it, it would be initialized only once, on first use.
By default, i18next retrieves translations asynchronously, using ajax get requests to the endpoint set in resGetPath
when you set locale using i18n.setLng
method. setLng
method accepts locale as first parameter and optional callback that would be fired after loading translations. You can make use of it in your’s app bootstrap code:
React = require('react')
i18n = require('i18n')
Gui = React.createFactory(require('gui'))
class App
constructor: (locale, node) ->
i18n.setLng locale, =>
# ...
React.render(Gui(), node)
# ...
# ...
new App(window.locale, document.body)
Having this setup we can just use regular i18next API in our React components:
i18n = require('i18n')
module.exports = React.createClass
displayName: 'Foo'
render: ->
React.DOM.span(null, i18n.t('Hello world!'))
i18next has much more features and integrations, including localStorage caching, jQuery integration and ruby gem that can automatically rebuild your javascript translations from YAML files. Have a look at their docs for further information.