i18n brainstorming
See original GitHub issueWe’ve somewhat glossed over the problem of internationalisation up till now. Frankly this is something SvelteKit isn’t currently very good at. I’m starting to think about how to internationalise/localise https://svelte.dev, to see which parts can be solved in userland and which can’t.
(For anyone unfamiliar: ‘Internationalisation’ or i18n refers to the process of making an app language agnostic; ‘localisation’ or l10n refers to the process of creating individual translations.)
This isn’t an area I have a lot of experience in, so if anyone wants to chime in — particularly non-native English speakers and people who have dealt with these problems! — please do.
Where we’re currently at: the best we can really do is put everything inside src/routes/[lang]
and use the lang
param in preload
to load localisations (an exercise left to the reader, albeit a fairly straightforward one). This works, but leaves a few problems unsolved.
I think we can do a lot better. I’m prepared to suggest that SvelteKit should be a little opinionated here rather than abdicating responsibility to things like i18next
, since we can make guarantees that a general-purpose framework can’t, and can potentially do interesting compile-time things that are out of reach for other projects. But I’m under no illusions about how complex i18n can be (I recently discovered that a file modified two days ago will be labeled ‘avant-hier’ on MacOS if your language is set to French; most languages don’t even have a comparable phrase. How on earth do you do that sort of thing programmatically?!) which is why I’m anxious for community input.
Language detection/URL structure
Some websites make the current language explicit in the pathname, e.g. https://example.com/es/foo or https://example.com/zh/foo. Sometimes the default is explicit (https://example.com/en/foo), sometimes it’s implicit (https://example.com/foo). Others (e.g. Wikipedia) use a subdomain, like https://cy.example.com. Still others (Amazon) don’t make the language visible, but store it in a cookie.
Having the language expressed in the URL seems like the best way to make the user’s preference unambiguous. I prefer /en/foo
to /foo
since it’s explicit, easier to implement, and doesn’t make other languages second-class citizens. If you’re using subdomains then you’re probably running separate instances of an app, which means it’s not SvelteKit’s problem.
There still needs to be a way to detect language if someone lands on /
. I believe the most reliable way to detect a user’s language preference on the server is the Accept-Language
header (please correct me if nec). Maybe this could automatically redirect to a supported localisation (see next section).
Supported localisations
It’s useful for SvelteKit to know at build time which localisations are supported. This could perhaps be achieved by having a locales
folder (configurable, obviously) in the project root:
locales
|- de.json
|- en.json
|- fr.json
|- ru.json
src
|- routes
|- ...
Single-language apps could simply omit this folder, and behave as they currently do.
lang attribute
The <html>
element should ideally have a lang
attribute. If SvelteKit has i18n built in, we could achieve this the same way we inject other variables into src/template.html
:
<html lang="%svelte.lang%">
Localised URLs
If we have localisations available at build time, we can localise URLs themselves. For example, you could have /en/meet-the-team
and /de/triff-das-team
without having to use a [parameter]
in the route filename. One way we could do this is by encasing localisation keys in curlies:
src
|- routes
|- index.svelte
|- {meet_the_team}.svelte
In theory, we could generate a different route manifest for each supported language, so that English-speaking users would get a manifest with this…
{
// index.svelte
pattern: /^\/en\/?$/,
parts: [...]
},
{
// {meet_the_team}.svelte
pattern: /^\/en/meet-the-team\/?$/,
parts: [...]
}
…while German-speaking users download this instead:
{
// index.svelte
pattern: /^\/de\/?$/,
parts: [...]
},
{
// {meet_the_team}.svelte
pattern: /^\/de/triff-das-team\/?$/,
parts: [...]
}
Localisation in components
I think the best way to make the translations themselves available inside components is to use a store:
<script>
import { t } from '$app/stores';
</script>
<h1>{$t.hello_world}</h1>
Then, if you’ve got files like these…
// locales/en.json
{ "hello_world": "Hello world" }
// locales/fr.json
{ "hello_world": "Bonjour le monde" }
…SvelteKit can load them as necessary and coordinate everything. There’s probably a commonly-used format for things like this as well — something like "Willkommen zurück, $1"
:
<p>{$t.welcome_back(name)}</p>
(In development, we could potentially do all sorts of fun stuff like making $t
be a proxy that warns us if a particular translation is missing, or tracks which translations are unused.)
Route-scoped localisations
We probably wouldn’t want to put all the localisations in locales/xx.json
— just the stuff that’s needed globally. Perhaps we could have something like this:
locales
|- de.json
|- en.json
|- fr.json
|- ru.json
src
|- routes
|- settings
|- _locales
|- de.json
|- en.json
|- fr.json
|- ru.json
|- index.svelte
Again, we’re in the fortunate position that SvelteKit can easily coordinate all the loading for us, including any necessary build-time preparation. Here, any keys in src/routes/settings/_locales/en.json
would take precedence over the global keys in locales/en.json
.
Translating content
It’s probably best if SvelteKit doesn’t have too many opinions about how content (like blog posts) should be translated, since this is an area where you’re far more likely to need to e.g. talk to a database, or otherwise do something that doesn’t fit neatly into the structure we’ve outlined. Here again, there’s an advantage to having the current language preference expressed in the URL, since userland middleware can easily extract that from req.path
and use that to fetch appropriate content. (I guess we could also set a req.lang
property or something if we wanted?)
Base URLs
Sapper (ab)used the <base>
element to make it easy to mount apps on a path other than /
. <base>
could also include the language prefix so that we don’t need to worry about it when creating links:
<!-- with <base href="de">, this would link to `/de/triff-das-team` -->
<a href={$t.meet_the_team}>{$t.text.meet_the_team}</a>
Base URLs haven’t been entirely pain-free though, so this might warrant further thought.
Having gone through this thought process I’m more convinced than ever that SvelteKit should have i18n built in. We can make it so much easier to do i18n than is currently possible with libraries, with zero boilerplate. But this could just be arrogance and naivety from someone who hasn’t really done this stuff before, so please do help fill in the missing pieces.
Issue Analytics
- State:
- Created 5 years ago
- Reactions:324
- Comments:178 (42 by maintainers)
Top GitHub Comments
Hello there! I’m a member of the Angular team, and I work on i18n there. I thought that I could share some of my knowledge to help you get started:
A little late to the party, but since you guys mentioned
svelte-i18n
, I think I should give some updates about it. I first created that lib as a POC for my previous job and kinda abandoned the project for a while after that. I’m currently working on av2.0.0
which add some new features and behaviours:stdout
or specified output file;locales
for easy{#each}
ing;This is currently a WIP and I’m definitely taking in consideration a lot of what’s said here. In no way I think I can handle every use case with just
svelte-i18n
. I’ve also thought about apreprocessor
to remove verbosity of some cases, but I’m reluctant about that for now.About creating a format specific for sapper/svelte: I’m not completely against it, but I think not using an established format is kind of reinventing the wheel. We already have great formats like
ICU
orFluent
, which already contemplate a bunch of quirks that a language can have.Edit:
Ended up deciding to have a queue of loader methods for each locale:
register(locale, loader)
: adds a loader method to the locale queue;waitLocale()
: executes all loaders and merges the result with the current locale dictionary;While not extremely ideal, the “verbosity” of this approach can be also reduced in the user-land by a preprocessor that adds those
register
andwaitLocale
calls, maybe even theformat/_
method import.Edit 2:
Just released
v2.0.0
🎉 Here’s a very crude sapper example: https://svelte-i18n.netlify.com/. You can check thenetwork
tab of your devtools too see how and when a locale messages are loaded. Hope it helps 😁