Adding basic internationalization (i18n) to your Svelte/SvelteKit app
Published
đ 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 âď¸
Step 1: 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).
Step 2: 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!
Step 3: 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.
Step 4: 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
Step 5: 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!
Step 6: Wrapping up
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!
Like this post?
Why don't you let me know on Twitter:
@danawoodman