DEV Community

Stefan Wienert
Stefan Wienert

Posted on • Originally published at stefanwienert.de on

Integrating Javascript (Vue) I18n into Rails pipeline with missing/auto translate features

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:

  1. 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
  2. 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
  3. 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
  4. The Javascript initializer, in our case Vue-i18n can take that data from the window and load it as locale data
  5. During production asset building, we need to add the generation of those files to our build pipeline
  6. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Also, add the export path into gitignore, because that files change often and are auto generated anyways:

echo "vendor/assets/javascripts/locales" >> ~/.gitignore
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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}" %>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}" use somekey {argument}
  • Pluralization : instead of:
somekey:
  one: "1 car"
  other: "%{count} cars"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

And load that scanner in your config/i18n-tasks.js

echo "<% require './lib/vue_i18n_scanner' %>" >> config/i18n-tasks.js
Enter fullscreen mode Exit fullscreen mode

Now, i18n-tasks health/unused/missing will scan our javascripts too.


Used Open-Source:

GitHub logo glebm / i18n-tasks

Manage translation and localization with static analysis, for Ruby i18n

i18n-tasks Build Status Coverage Status Gitter

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'
Enter fullscreen mode Exit fullscreen mode

Copy the default configuration file:

$ cp $(i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

Or for minitest:

$ cp $(i18n-tasks gem-path)
Enter fullscreen mode Exit fullscreen mode

GitHub logo fnando / i18n-js

It's a small library to provide the I18n translations on the Javascript. It comes with Rails support.

i18n.js

It's a small library to provide the Rails I18n translations on the JavaScript

Tests Gem Version npm License: MIT Build Status Coverage Status Gitter


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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

GitHub logo kazupon / vue-i18n

🌐 Internationalization plugin for Vue.js

Vue I18n logo

vue-i18n

Build Status Coverage Status NPM version vue-i18n channel on Discord vue-i18n Dev Token

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

MIT






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)