π Hola, Mundo!
Read this article en EspaΓ±ol (via Chema of Ideas Worth Tranlsating, thanks!)
I just recently stumbled upon a great video by Dr. Matthias Stahl (tweet here, YouTube video here, code here) on Svelte Society's YouTube channel who came up with a simple approach to adding basic i18n translations to a Svelte app.
I thought it would be fun and informative to recreate it while also making some minor optimizations and enhancements along the way. π€
We're going to be creating something like this:
Most of the credit in this post goes to Matthias here, so make sure to check him out and give him a follow! π
π Note: this isn't a full featured internationalization solution like i18next, so this might not be the exact right solution for you!
Impatient? Checkout the Svelte REPL with all the code βοΈ
The translations object
In Matthias's example, he uses a deeply nested object to store translations strings. This works, but it is slightly inefficient since you'll have to traverse the object, especially if you have multiple layers of nested keys (think app => page => section => component => label
).
I've instead opted for a flat object with the key being the internationalization locale subtag (eg en
and not en-US
) and a string representing a dot-separated namespace for a translation value. When we're dealing with many translations, this should have a slight performance benefit.
In addition, we will support embedded variables and HTML in our translation strings:
// translations.js
export default {
en: {
"homepage.title": "Hello, World!",
"homepage.welcome": "Hi <strong>{{name}}</strong>, how are you?",
"homepage.time": "The current time is: {{time}}",
},
es: {
"homepage.title": "Β‘Hola Mundo!",
"homepage.welcome": "Hola, <strong>{{name}}</strong>, ΒΏcΓ³mo estΓ‘s?",
"homepage.time": "La hora actual es: {{time}}",
},
};
This will allow us to have namespaced keys as well as supporting rich formatting and injecting values (e.g. strings, numbers, dates, etc).
The component
We will now create our Svelte component, huzzah! π
This component is pretty simple and will consist of a select dropdown to choose the language the user wants to use as well as displaying some translation text including one with HTML and custom variables!
<!-- App.svelte -->
<script>
import { t, locale, locales } from "./i18n";
// Create a locale specific timestamp
$: time = new Date().toLocaleDateString($locale, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
</script>
<main>
<p>
<select bind:value={$locale}>
{#each locales as l}
<option value={l}>{l}</option>
{/each}
</select>
</p>
<h1>{$t("homepage.title")}!</h1>
<p>{@html $t("homepage.welcome", { name: "Jane Doe" })}!</p>
<p>{$t("homepage.time", { time })}!</p>
</main>
What we're doing here is connecting a <select>
element to a Svelte store (which we will create in a second) and also using a magic $t()
method which will allow us to do translation lookups.
You'll also notice we're creating a locale specific timestamp to show the user using toLocaleDateString
which we pass the $locale
store value to.
If this doesn't make sense yet, that's ok, keep reading!
The store
Now for the fun part, let's create our Svelte store! π―ββοΈ
The store itself is quite simple, basically we just store the locale value (e.g. en
, es
, etc) in one store and then create a derived
store from the locale and the translations object we created earlier.
import { derived, writable } from "svelte/store";
import translations from "./translations";
export const locale = writable("en");
export const locales = Object.keys(translations);
function translate(locale, key, vars) {
// Let's throw some errors if we're trying to use keys/locales that don't exist.
// We could improve this by using Typescript and/or fallback values.
if (!key) throw new Error("no key provided to $t()");
if (!locale) throw new Error(`no translation for key "${key}"`);
// Grab the translation from the translations object.
let text = translations[locale][key];
if (!text) throw new Error(`no translation found for ${locale}.${key}`);
// Replace any passed in variables in the translation string.
Object.keys(vars).map((k) => {
const regex = new RegExp(`{{${k}}}`, "g");
text = text.replace(regex, vars[k]);
});
return text;
}
export const t = derived(locale, ($locale) => (key, vars = {}) =>
translate($locale, key, vars)
);
The majority of the logic is in the translate
method which looks up the keys and injects the variables, if present.
The derived store will stay in sync with the current locale and thus our translate
method will always received the current locale when being called. When the locale is updated, the $t()
calls will be re-computed and thus update all our text in our Svelte component when the user changes their locale. Cool! π
This departs a bit from Matthias's version as it doesn't require creating an extra store for the translation which isn't strictly necessary and is a bit more efficient if we omit it.
Putting it together
Now that we have our store, we have all the pieces to create a basic internationalization system in Svelte, congrats π
If you want to see this code in action, have a look at the Svelte REPL
π° Going further
Now, this option isn't right for everyone. If you're building a large, robust, content-heavy application with many translations, then maybe you'll want to consider something like Locize in combination with i18next. You can always integrate their JS libraries with Svelte in a similar way.
We are also not sanitizing any of the HTML content, so if you're injecting user supplied data in your translation strings, you'll need to make sure to sanitize/strip the input so as not to create an XSS vulnerability! π
Another issue with this approach is there is no real fallback behavior for a missing translation (right now we're just throwing errors which is probably not what you want).
That said, a solution like this can be helpful when you don't need a full-blown translation platform and just need relatively basic string translations.
You could extend this example by persisting the locale value in local storage and defaulting to the browser's preferred language by, for example, using the navigator.languages
property. This is a subject in its own right!
π¬ Fin
Checkout the Svelte REPL for all the code in a live editing environment you can mess around with! π€
I think this example shows us a few interesting properties of Svelte, including:
1οΈβ£ How to implement a functional but basic i18n implementation in very little code
2οΈβ£ How to use a derived
store which returns a function
3οΈβ£ How to use global stores and how to set those values in our components
4οΈβ£ How to use toLocaleDateString
to get locale-specific date formatting
Hopefully this was entertaining for you and don't forget to give Matthias a shoutout for his original post!
Thanks for reading! Consider giving this post a β€οΈ, π¦ or π to bookmark it for later. π
Have other tips, ideas, feedback or corrections? Let me know in the comments! πββοΈ
Don't forget to follow me on Dev.to (danawoodman), Twitter (@danawoodman) and/or Github (danawoodman)!
Photo by Joshua Aragon on Unsplash
Top comments (1)
Works like a charm. Very nice concept. Thank you very much.