Adding I18n features to a backend / "vanilla" Rails app is a common and well documented use case. Awesome tools, like i18n-tasks
helping manage to grow the translations by providing tasks like:
- Auto translate (via Google Translate),
- check for unused/missing translations,
- auto sorting translations,
- keeping all locales in sync and same structure/order.
Most of our apps nowadays are hybrid apps, though, that are using some amount of Javascript (Vue.js/Angular/React) on the frontside. A full I18n will also have to take that into account. When starting an app, I tend to keep the translations together with the components (like Vue i18n which adds a new block into the single file components). But using this approach, one loses the consistent tooling and translations are starting to sprinkle around the application, which makes adding another locale harder and harder.
For this purpose, I've build the following pipeline, which fulfills the following requirements:
- i. fast development cycle - reloading a page reloads the translations also for I18n
- ii. separate shipping of locale texts for each locale - e.g. a German user only has to download German locales and not download all
- iii. fingerprinting of said separate locales - so that those files can be cached efficiently and at the same time don't get stale
- iv. unified storage for all translations and support for the Rails tooling (i18n-tasks)
The high-level overview is the following:
- Using
i18n-js
gem, we can generate a Javascript file for each locale easily. That gem also helps us during development by providing a middleware that reloads those files - We use a namespace in our I18n locale tree, e.g.
js.
to ship only that tree to the client and not all of that keys - In every layout that requires Javascript translations, we put a little partial, that loads those files and put them into a global Javascript field onto
window
- The Javascript initializer, in our case Vue-i18n can take that data from the window and load it as locale data
- During production asset building, we need to add the generation of those files to our build pipeline
- We can add a custom
i18n-tasks scanner
that helps collecting ALL usages of our Javascript keys to sync which the actual translations (e.g. integrating that into "i18n-tasks unused", "i18n-tasks missing")
The whole process is described in the following flow chart:
i. Adding dependencies and configure I18n-js Gem
Add Gem, bundle, add configuration
# Gemfile
gem "i18n-js"
# config/i18n-js.yml
translations:
- file: 'vendor/assets/javascripts/locales/%{locale}.js'
namespace: "LocaleData"
only: '*.js'
export_i18n_js: "vendor/assets/javascripts/locales/"
js_extend: false # this will disable Javascript I18n.extend globally
Yes, that's right! We are using the old asset pipeline to handle the auto generated translations files, here under vendor/assets/javascript/locales
, because that provides a extremely simple fingerprinting and no-fuzz integration into our existing precompile step. To make that work, we need to add all those files to the asset-precompile list, e.g. when using sprockets 4+ in app/assets/config/manifest.js
:
//= link locales/en.js
//= link locales/de.js
Also, manually add the I18n-JS Middleware, to enable a seamless development reloading experience.
Rails.application.configure do
...
# config/environment/development.rb
config.middleware.use I18n::JS::Middleware
Also, add the export path into gitignore, because that files change often and are auto generated anyways:
echo "vendor/assets/javascripts/locales" >> ~/.gitignore
Don't forget, to run the i18n-js Rake export task before deployment, otherwise those files might be missing in your deployment server/ci whatever:
echo "task 'assets:precompile' => 'i18n:js:export'" >> Rakefile
ii. Loading the correct locale data into your Javascript
First, add some example locale. As stated in the introduction, we are using a namespace js.*
where all Javascript keys are kept inside. This has the advantage that we can very simply export only the necessary locale data to the client. There is an option in i18n-js, which can hide that "js." namespace in the client, e.g. a Rails i18n key of js.component.button
would be accessible as component.button
in the frontend. But I've decided against, because that namespace interferes when using I18n-tasks to check for missing/unused in several cases2.
# config/locales/js.en.yml'
en:
js:
button_text: "Save"
Now, make sure, every relevant layout, that has translated Javascript on it, loads the locale before everything else, e.g. using a layout partial which you require before loading any other Javascript.
<!-- app/views/layouts/_i18n_loader.html.erb -->
<script>
window.Locale = <%= raw I18n.locale.to_json %>
window.LocaleData = {}
</script>
<%= javascript_include_tag "locales/#{I18n.locale}" %>
Then, our pack / framework initializer can pick that up. In our case, we are using Vue-i18n:
// app/javascripts/utils/i18n.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: window.Locale,
fallbackLocale: 'de',
})
const rawData = window.LocaleData.translations[window.Locale]
i18n.setLocaleMessage(window.Locale, rawData)
export default i18n
Now we can include that i18n key when initializing every Vue root "app" on our site:
// app/javascripts/packs/app.js
import i18n from 'utils/i18n'
new Vue({
el,
i18n,
})
That's it! Every component can now access locale data, like that:
<template>
<!-- passing as efficient v-t handler -->
<button v-t="'js.button_text'"></button>
<!-- using i18n arguments -->
<button>{{ $t('js.text_with_arguments', { name: name }) }}
</template
Make sure to reference the documentation3 of Vue-i18n, as there are some differences to Rails built-in message style:
-
Passing arguments : Instead of
"somekey %{argument}"
usesomekey {argument}
- Pluralization : instead of:
somekey:
one: "1 car"
other: "%{count} cars"
use: somekey: 1 car | {n} cars
or with "nothing": somekey: no car | {n} car | {n} cars
iii. Adding I18n-Tasks Scanner Adapter
Translations should work find now. To make I18n-Tasks aware of our locales, use this scanner:
# lib/vue_i18n_scanner.rb
require 'i18n/tasks/scanners/file_scanner'
require 'pry'
# finds v-t="" and $t(c) usages
class VueI18nScanner < I18n::Tasks::Scanners::FileScanner
include I18n::Tasks::Scanners::OccurrenceFromPosition
KEY_IN_QUOTES = /(?["'])(?[\w\.]+)(?["'])/.freeze
WRAPPED_KEY_IN_QUOTES = /(?["'])#{KEY_IN_QUOTES}(?["'])/.freeze
# @return [Array]
def scan_file(path)
text = read_file(path)
# single file component translation used
return [] if text.include?('<i18n')
out = []
# v-t="'key'" v-t='"key"'
text.to_enum(:scan, /v-t=#{WRAPPED_KEY_IN_QUOTES}/).each do |_|
key = Regexp.last_match[:key]
occurrence = occurrence_from_position(
path, text, Regexp.last_match.offset(:key).first
)
out << [key, occurrence]
end
text.to_enum(:scan, /\$tc?\(#{KEY_IN_QUOTES}/).each do |_|
key = Regexp.last_match[:key]
occurrence = occurrence_from_position(
path, text, Regexp.last_match.offset(:key).first
)
out << [key, occurrence]
end
out
end
end
I18n::Tasks.add_scanner 'VueI18nScanner', only: %w(*.vue)
And load that scanner in your config/i18n-tasks.js
echo "<% require './lib/vue_i18n_scanner' %>" >> config/i18n-tasks.js
Now, i18n-tasks health/unused/missing will scan our javascripts too.
Used Open-Source:
glebm / i18n-tasks
Manage translation and localization with static analysis, for Ruby i18n
i18n-tasks
i18n-tasks helps you find and manage missing and unused translations.
This gem analyses code statically for key usages, such as I18n.t('some.key')
, in order to:
- Report keys that are missing or unused.
- Pre-fill missing keys, optionally from Google Translate or DeepL Pro.
- Remove unused keys.
Thus addressing the two main problems of i18n gem design:
- Missing keys only blow up at runtime.
- Keys no longer in use may accumulate and introduce overhead, without you knowing it.
Installation
i18n-tasks can be used with any project using the ruby i18n gem (default in Rails).
Add i18n-tasks to the Gemfile:
gem 'i18n-tasks', '~> 0.9.33'
Copy the default configuration file:
$ cp $(i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
Copy rspec test to test for missing and unused translations as part of the suite (optional):
$ cp $(i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/
Or for minitest:
$ cp $(i18n-tasks gem-path)
âĻfnando / i18n-js
It's a small library to provide the I18n translations on the Javascript. It comes with Rails support.
It's a small library to provide the Rails I18n translations on the JavaScript
Features:
- Pluralization
- Date/Time localization
- Number localization
- Locale fallback
- Asset pipeline support
- Lots more! :)
Version Notice
The main
branch (including this README) is for latest 3.0.0
instead of
2.x
.
Usage
Installation
Rails app
Add the gem to your Gemfile.
gem "i18n-js"
Rails with webpacker
If you're using webpacker
, you may need to add the dependencies to your client
with:
yarn add i18n-js
# or, if you're using npm
npm install i18n-js
For more details, see this gist.
Rails app with Asset Pipeline
If you're using the
asset pipeline, then you
must add the following line to your app/assets/javascripts/application.js
.
//
// This is optional (in case you have `I18n is not defined` error)
// If you want to put this line, you must put it BEFORE `i18n/translations`
//= require i18n
// Some people
âĻvue-i18n
Internationalization plugin for Vue.js
đĨ Gold Sponsors
đĨ Silver Sponsors
đĨ Bronze Sponsors
đĸ Notice
vue-i18n will soon be transferred to intlify organization. After that, it will be developed and maintained on intlify.
The vue-i18n
that has been released on npm will be released as @intlify/vue-i18n
in near future.
vue-i18n
next major version repo is here
Intlify is a new i18n project kickoff by @kazupon
.
đ Documentation
See here
đ Changelog
Detailed changes for each release are documented in the CHANGELOG.md.
â Issues
Please make sure to read the Issue Reporting Checklist before opening an issue. Issues not conforming to the guidelines may be closed immediately.
đĒ Contribution
Please make sure to read the Contributing Guide before making a pull request.
Šī¸ License
2. Adding the prefix in our custom scanner while scanning the files was a way that worked, BUT the default I18n-scanner already picked up some of the keys by it's own regex scanner ($t(..)
-usages) which then would then be marked as missed. There seemed to be no way to ignore the app/javascript/ for only the default scanner but only for all.
3. https://kazupon.github.io/vue-i18n/guide/formatting.html#named-formatting
Top comments (0)