In the previous blog post we learned on a simple way how we can instrumented our Remix app to be ready for localization by using remix-i18next.
In this blog post we will try to unleash the full power of i18next and focus on a continuous localization workflow.
In the previous blog post there was a voluntary part. This already was the first step.
By sending the translations to some translators or translator agency you have more control and a direct contact with them. But this also means more work for you.
This is a traditional way. But be aware sending files around creates always an overhead.
Does a better option exist?
For sure!
i18next helps to get the application translated, and this is great - but there is more to it.
How do you integrate any translation services / agency?
How do you keep track of new or removed content?
How do you handle proper versioning?
How do you deploy translation changes without deploying your complete application?
Done so, we're going change the way the translations are loaded on server side and on client side.
Currently the translations are downloaded from locize via CLI and are then served on server side in the public/locales folder. Thanks to remix-i18next then the translations are downloaded by the client.
We now would like the client side to directly consume the translations provided by the locize CDN.
Instead on server side we'll continue to "bundle" the translations first.
See downloadLocales script in package.json.
We're doing so to prevent an elevated amount of downloads generated on server side. Read this for more information about this topic about serverless environments.
const locizeOptions = { projectId: 'f6d74b76-9677-4a0d-b400-86e1507397ab', apiKey: '1c2bbc21-027d-4f41-995a-e8beb451cdef', // YOU should not expose your apps API key to production!!! version: 'latest' }
// initialize i18next using initReactI18next and configuring it if (!i18next.isInitialized) { // prevent i18next to be initialized multiple times i18next // pass the i18n instance to react-i18next. .use(initReactI18next) // i18next-locize-backend // loads translations from your project, saves new keys to it (saveMissing: true) // https://github.com/locize/i18next-locize-backend .use(Backend) // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(LanguageDetector) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ ...i18nextOptions, // This function detects the namespaces your routes rendered while SSR use // and pass them here to load the translations ns: getInitialNamespaces(), detection: { // Here only enable htmlTag detection, we'll detect the language only // server-side with remix-i18next, by using the `<html lang>` attribute // we can communicate to the client the language detected server-side order: ['htmlTag'], // Because we only use htmlTag, there's no reason to cache the language // on the browser, so we disable it caches: [], }, backend: locizeOptions }) .then(() => { // then hydrate your app return hydrate( <I18nextProvideri18n={i18next}> <RemixBrowser /> </I18nextProvider>, document ) }) }
The entry.server.jsx file, the root.jsx and the i18nextOptions.js file should still look the same:
exportdefaultasyncfunctionhandleRequest( request, statusCode, headers, context ) { // First, we create a new instance of i18next so every request will have a // completely unique instance and not share any state const instance = createInstance()
// Then we could detect locale from the request const lng = await i18n.getLocale(request) // And here we detect what namespaces the routes about to render want to use const ns = i18n.getRouteNamespaces(context)
// First, we create a new instance of i18next so every request will have a // completely unique instance and not share any state. await instance .use(initReactI18next) // Tell our instance to use react-i18next .use(Backend) // Setup our backend.init({ .init({ ...i18nextOptions, // use the same configuration as in your client side. lng, // The locale we detected above ns, // The namespaces the routes about to render want to use backend: { loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'), } })
// Then you can render your app wrapped in the I18nextProvider as in the // entry.client file const markup = renderToString( <I18nextProvideri18n={instance}> <RemixServercontext={context}url={request.url} /> </I18nextProvider> );
exportconst handle = { // In the handle export, we could add a i18n key with namespaces our route // will need to load. This key can be a single string or an array of strings. i18n: ['common'] };
exportfunctionmeta({ data }) { return { title: data.title } }
exportdefaultfunctionApp() { const { i18n } = useTranslation() const { locale } = useLoaderData() // This hook will change the i18n instance language to the current locale // detected by the loader, this way, when we do something to change the // language, this locale will change and i18next will load the correct // translation files useChangeLanguage(locale)
The app looks more or less the same, but on client side the translations are fetched directly from the locize CDN.
This means if you change translations in locize they will be available to your Remix app, without having to change or redeploy your app.
Only to have the newest translations on server side (i.e. for SEO optimizations) a new npm run downloadLocales and rebuild is needed.
save missing translations
Thanks to the use of the saveMissing functionality, new keys gets added to locize automatically, while developing the app.
Just pass saveMissing: true in the i18next options:
const locizeOptions = { projectId: 'f6d74b76-9677-4a0d-b400-86e1507397ab', apiKey: '1c2bbc21-027d-4f41-995a-e8beb451cdef', // YOU should not expose your apps API key to production!!! version: 'latest' }
// initialize i18next using initReactI18next and configuring it if (!i18next.isInitialized) { // prevent i18next to be initialized multiple times i18next // pass the i18n instance to react-i18next. .use(initReactI18next) // i18next-locize-backend // loads translations from your project, saves new keys to it (saveMissing: true) // https://github.com/locize/i18next-locize-backend .use(Backend) // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(LanguageDetector) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ ...i18nextOptions, // This function detects the namespaces your routes rendered while SSR use // and pass them here to load the translations ns: getInitialNamespaces(), detection: { // Here only enable htmlTag detection, we'll detect the language only // server-side with remix-i18next, by using the `<html lang>` attribute // we can communicate to the client the language detected server-side order: ['htmlTag'], // Because we only use htmlTag, there's no reason to cache the language // on the browser, so we disable it caches: [], }, backend: locizeOptions, saveMissing: true }) .then(() => { // then hydrate your app return hydrate( <I18nextProvideri18n={i18next}> <RemixBrowser /> </I18nextProvider>, document ) }) }
Each time you'll use a new key, it will be sent to locize, i.e.:
1
<div>{t('new.key', 'this will be added automatically')}</div>
With the help of the locize plugin, you'll be able to use your app within the locize InContext Editor.
Lastly, with the help of the auto-machinetranslation workflow and the use of the saveMissing functionality, new keys not only gets added to locize automatically, while developing the app, but are also automatically translated into the target languages using machine translation.
Check out this video to see how the automatic machine translation workflow looks like!
const locizeOptions = { projectId: 'f6d74b76-9677-4a0d-b400-86e1507397ab', apiKey: '1c2bbc21-027d-4f41-995a-e8beb451cdef', // YOU should not expose your apps API key to production!!! version: 'latest' }
// initialize i18next using initReactI18next and configuring it if (!i18next.isInitialized) { // prevent i18next to be initialized multiple times i18next // pass the i18n instance to react-i18next. .use(initReactI18next) // i18next-locize-backend // loads translations from your project, saves new keys to it (saveMissing: true) // https://github.com/locize/i18next-locize-backend .use(Backend) // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(LanguageDetector) // locize-lastused // sets a timestamp of last access on every translation segment on locize // -> safely remove the ones not being touched for weeks/months // https://github.com/locize/locize-lastused .use(LastUsed) // locize-editor // InContext Editor of locize .use(locizePlugin) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ ...i18nextOptions, // This function detects the namespaces your routes rendered while SSR use // and pass them here to load the translations ns: getInitialNamespaces(), detection: { // Here only enable htmlTag detection, we'll detect the language only // server-side with remix-i18next, by using the `<html lang>` attribute // we can communicate to the client the language detected server-side order: ['htmlTag'], // Because we only use htmlTag, there's no reason to cache the language // on the browser, so we disable it caches: [], }, backend: locizeOptions, locizeLastUsed: locizeOptions, saveMissing: true }) .then(() => { // then hydrate your app return hydrate( <I18nextProvideri18n={i18next}> <RemixBrowser /> </I18nextProvider>, document ) }) }
First in locize, create a dedicated version for production. Do not enable auto publish for that version but publish manually or via API or via CLI.
Lastly, enable Cache-Control max-age for that production version.
const locizeOptions = { projectId: 'f6d74b76-9677-4a0d-b400-86e1507397ab', apiKey: !isProduction ? '1c2bbc21-027d-4f41-995a-e8beb451cdef' : undefined, // YOU should not expose your apps API key to production!!! version: isProduction ? 'production' : 'latest' }
if (!isProduction) { // locize-lastused // sets a timestamp of last access on every translation segment on locize // -> safely remove the ones not being touched for weeks/months // https://github.com/locize/locize-lastused i18next.use(LastUsed) }
// initialize i18next using initReactI18next and configuring it if (!i18next.isInitialized) { // prevent i18next to be initialized multiple times i18next // locize-editor // InContext Editor of locize .use(locizePlugin) // i18next-locize-backend // loads translations from your project, saves new keys to it (saveMissing: true) // https://github.com/locize/i18next-locize-backend .use(Backend) // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(LanguageDetector) // pass the i18n instance to react-i18next. .use(initReactI18next) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ ...i18nextOptions, // This function detects the namespaces your routes rendered while SSR use // and pass them here to load the translations ns: getInitialNamespaces(), detection: { // Here only enable htmlTag detection, we'll detect the language only // server-side with remix-i18next, by using the `<html lang>` attribute // we can communicate to the client the language detected server-side order: ['htmlTag'], // Because we only use htmlTag, there's no reason to cache the language // on the browser, so we disable it caches: [], }, backend: locizeOptions, locizeLastUsed: locizeOptions, saveMissing: !isProduction // you should not use saveMissing in production }) .then(() => { // then hydrate your app return hydrate( <I18nextProvideri18n={i18next}> <RemixBrowser /> </I18nextProvider>, document ) }) }
Now, during development, you'll continue to save missing keys and to make use of lastused feature. => npm run dev
And in production environment, saveMissing and lastused are disabled. => npm run build && npm start