Translations in Stimulus Controllers
This article was originally published on Rails Designer This is a sneak peek from the upcoming book, JavaScript for Rails Developers. For this blog post, I’ve simplified some of the more detailed sections to better fit the blog. Translations, or more broadly Internationalization (i18n; because there are 18 letters between “i” and “n”), is the process of preparing apps to support multiple languages and regional settings (i.e. “locale”) to accommodate a global user base. So it's not just about translating words, but also about the formatting of dates, numbers and phone numbers. It is one of those features product managers move back it to the backlog, because they know adding support for it is fairly easy (especially with Rails), but supporting it while the product is in development is a pain in the ass multiplied by the number of supported locales. But here we are! And luckily Rails has great support for i18n out-of-the-box. But how does that extend to JavaScript and specifically, Stimulus? I want to explore multiple ways. Let's assume you have a typical controller that has three static strings that are used to indicate a saved-state. saved; saving; failed. You can imagine them being used like this: export default class extends Controller { static targets = ["status"] update() { this.statusTarget.textContent = "Saving" const response = fetch( // logic to make a PATCH request ) response.ok ? this.statusTarget.textContent = "Saved" : this.statusTarget.textContent = "Failed" } } First make sure you add the keys to your locale config, e.g. config/locales/en.yml: en: hello: "Hello world" status: saved: Saved saving: Saving failed: Failed Use the HTML you already have Stimulus' tagline is “a modest JavaScript framework for the HTML you already have”. And with that you can render the, translated, content in the HTML like you normally do with Rails and then show/hide, inject and transform HTML-elements using Stimulus. So instead of one status target, you create a savedStatus, savingStatus and failedStatus. Easy and straight-forward. No extra steps needed! Using Stimulus' separate values API If you need to do more DOM wrangling in your JavaScript, you can choose to use the values API. export default class extends Controller { static values = { // … savedStatus: { type: String, default: "Saved" }, savingStatus: { type: String, default: "Saving" }, failedStatus: { type: String, default: "Failed" } } // … } And then instead of using the static strings, you can use the value like any other: this.savedStatusValue. Perfect solution if you only have a few keys! Using Stimulus' object values API But when you have many keys (even the above three status keys I find too much already), you could pass them as an object instead. Then you can pass them to the controller like so: And instead of three separate values, you have one i18n object: export default class extends Controller { static values = { // … i18n: Object } // … } Then use it in the controller as this.i18nValue.saved. Pretty sweet! This solution doesn't support default values, but if you need to pass multiple keys—not just status—you can do so with some Ruby logic. Custom JavaScript solution These solutions all work pretty fine, but are limited to Stimulus controllers. What if you need some translated strings in a imported class or method? Pass those strings along as well? What about using syntax like this: export default class extends Controller { update() { response.ok ? this.statusTarget.textContent = t("status.saved") : this.statusTarget.textContent = t("status.failed") } } I recognize that! That is just like the t (short for I18n.t) in Rails! It is not supported, like this, out-of-the-box in JavaScript. Let's create the t method in app/javascript/helpers/i18n.js (and import it where you need it): // app/javascript/helpers/i18n.js export function t(key) { if (!window.i18n) return key try { return key.split(".").reduce( (translation, part) => translation[part], window.i18n ) } catch { return key } } So what about window.i18n? Where is that coming from? It is something you need to add. It will hold all the defined i18n-keys. You can add it to your application layout or only the view you need it at. window.i18n = And there you have it. Multiple ways of using translated content in your JavaScript and Stimulus controllers. For more in-depth explanations, specifically on the JavaScript used in the the t() method, check out the upcoming book: JavaScript for Rails Developers.

This article was originally published on Rails Designer
This is a sneak peek from the upcoming book, JavaScript for Rails Developers. For this blog post, I’ve simplified some of the more detailed sections to better fit the blog.
Translations, or more broadly Internationalization (i18n; because there are 18 letters between “i” and “n”), is the process of preparing apps to support multiple languages and regional settings (i.e. “locale”) to accommodate a global user base. So it's not just about translating words, but also about the formatting of dates, numbers and phone numbers.
It is one of those features product managers move back it to the backlog, because they know adding support for it is fairly easy (especially with Rails), but supporting it while the product is in development is a pain in the ass multiplied by the number of supported locales.
But here we are! And luckily Rails has great support for i18n out-of-the-box. But how does that extend to JavaScript and specifically, Stimulus? I want to explore multiple ways.
Let's assume you have a typical controller that has three static strings that are used to indicate a saved-state.
- saved;
- saving;
- failed.
You can imagine them being used like this:
export default class extends Controller {
static targets = ["status"]
update() {
this.statusTarget.textContent = "Saving"
const response = fetch(
// logic to make a PATCH request
)
response.ok ?
this.statusTarget.textContent = "Saved" :
this.statusTarget.textContent = "Failed"
}
}
First make sure you add the keys to your locale config, e.g. config/locales/en.yml:
en:
hello: "Hello world"
status:
saved: Saved
saving: Saving
failed: Failed
Use the HTML you already have
Stimulus' tagline is “a modest JavaScript framework for the HTML you already have”. And with that you can render the, translated, content in the HTML like you normally do with Rails and then show/hide, inject and transform HTML-elements using Stimulus. So instead of one status target, you create a savedStatus, savingStatus and failedStatus. Easy and straight-forward. No extra steps needed!
Using Stimulus' separate values API
If you need to do more DOM wrangling in your JavaScript, you can choose to use the values API.
export default class extends Controller {
static values = {
// …
savedStatus: { type: String, default: "Saved" },
savingStatus: { type: String, default: "Saving" },
failedStatus: { type: String, default: "Failed" }
}
// …
}
And then instead of using the static strings, you can use the value like any other: this.savedStatusValue
. Perfect solution if you only have a few keys!
Using Stimulus' object values API
But when you have many keys (even the above three status keys I find too much already), you could pass them as an object instead.
Then you can pass them to the controller like so:
data-controller="editor"
data-editor-i18n-value="<%= t('status').to_json %>"
>
And instead of three separate values, you have one i18n
object:
export default class extends Controller {
static values = {
// …
i18n: Object
}
// …
}
Then use it in the controller as this.i18nValue.saved
. Pretty sweet! This solution doesn't support default values, but if you need to pass multiple keys—not just status
—you can do so with some Ruby logic.
Custom JavaScript solution
These solutions all work pretty fine, but are limited to Stimulus controllers. What if you need some translated strings in a imported class or method? Pass those strings along as well? What about using syntax like this:
export default class extends Controller {
update() {
response.ok ?
this.statusTarget.textContent = t("status.saved") :
this.statusTarget.textContent = t("status.failed")
}
}
I recognize that! That is just like the t (short for I18n.t) in Rails! It is not supported, like this, out-of-the-box in JavaScript. Let's create the t
method in app/javascript/helpers/i18n.js (and import it where you need it):
// app/javascript/helpers/i18n.js
export function t(key) {
if (!window.i18n) return key
try {
return key.split(".").reduce(
(translation, part) => translation[part], window.i18n
)
} catch {
return key
}
}
So what about window.i18n
? Where is that coming from? It is something you need to add. It will hold all the defined i18n-keys. You can add it to your application layout or only the view you need it at.
<script>
window.i18n = <%= raw I18n.backend.send(:translations)[I18n.locale].to_json %>
</script>
And there you have it. Multiple ways of using translated content in your JavaScript and Stimulus controllers.
For more in-depth explanations, specifically on the JavaScript used in the the t()
method, check out the upcoming book: JavaScript for Rails Developers.