i8 is an internationalization library for TypeScript-based apps.
- Small: i8 is under 500 lines and has only two dependencies.
- Extensible: i8 is designed to be extended with custom features. For example, you can add support for custom pluralization rules.
- Type-safe: i8 uses TypeScript to ensure that your translations are valid and that you use them correctly. This also enables IDE autocompletion, everywhere.
- Framework-agnostic: i8 is not tied to any specific framework. It can be used with Svelte, React, Vue, Angular, etc.
- Lazy-loaded: i8 only loads the translations that are actually needed. If you have 100 languages, i8 will only load the translations for the language that the user has selected.
- On-the-fly language switching: i8 can switch the language on the fly, without reloading the page in any reactive framework.
This is not an ordinary TS library (why?). You need to copy this folder directly into your source tree to enable TS support.
To install i8, install the following dependencies into your project:
npm i --save-dev @sozialhelden/ietf-language-tags
npm i node-inspect-extracted resolve-accept-language
Then clone this repository into a folder in your source tree. You can use any name, but I
recommend i18n
:
cd project/src
git clone https://github.com/lxhom/i8.git i18n
I prefer short names for library imports like this, so I add the following to my tsconfig.json
:
{
"compilerOptions": {
"paths": {
"$i8": [
"./src/i18n/internal/i8"
]
}
}
}
Then I don't have to use relative imports and can use $i8
instead:
import {i8} from '$i8';
The configuration is done in settings.ts. You can change a lot about how i8 works there. The options are documented in the file and have sane defaults.
The TypeScript type of the i8 store gets derived from the default language. The default language should be the most complete language you have (usually English or the native language of the dev).
Options for making i8 work with your favorite framework. i8 was primarily designed for SvelteKit or Svelte with
Vite because it offers nice features like import.meta.glob
, but if you want to use another framework (I don't judge),
you can override the store type & function and the importer function.
You can add adapters to i8 here. An adapter is run after a variable is resolved, and can be used to modify the result,
like
converting status: (n: number) => n + " language(s) loaded"
called with $i8.i8.status(2)
to "2 languages loaded"
.
The
plurality adapter is provided as an example by default.
You can make i8 automatically update the URL and/or the cookie when the language changes. This can be customized there.
You can control how variables are converted to strings ("inspected", e.g. $i8.doesnt_exist("a", 1, {b: 2})
becomes
"doesnt_exist(a, 1, {b: 2})"
). By default, this uses node-inspect-extracted
to get the same output as in the Node
console, but it can be changed.
Function to log messages if stuff goes wrong (function error, missing language etc.).
You can add translations by creating a file in lang/
with a valid IETF language tag as the name, e.g. lang/de-DE.ts
.
To get full TypeScript support, your language file should follow this boilerplate:
import type {LanguageDeclaration} from "$i";
export default {
// Your translations here
} satisfies LanguageDeclaration;
This will give you autocompletion for your translations and will ensure that you use the same names as in the default language.
Note that this is NOT necessary for the default language, you just export default { /* ... */ }
there, because that
would be a circular dependency and TypeScript doesn't like that.
You also need to run node i18n/internal/gen_lang.mjs
to generate language metadata. This is necessary because loading
metadata from all languages would be pretty slow, so we do this at build time to generate a static list of languages.
This is why you need to run this command every time you add a new language.
i8 works best with a reactive framework like Svelte or React to enable automatic updates when the language changes.
This is why the i8
export is a store. In Svelte prefixing a variable with $
will "dereference" it, so you can use
$i8
to get the current language in Svelte. In React, you have to follow the Writable
interface you set
in settings.ts
. I'll just assume the $
notation here.
Essentially, $i8 is just a regular object. For example:
// Language file
export default {
foo: {
bar: {
baz: "Hello world!"
}
},
function: (a: number, b: number) => a + b
}
// Usage
console.log($i8.foo.bar.baz.toString()); // "Hello world!"
console.log($i8.function(1, 2).toString()); // 3
You will just get "i18n not initialized! You need to set a language first!" if you execute it like this, because i8
can't
magically know which language you want to use. To properly initialize i8, you need to call setLang()
. i8 also comes
with
a few tools to help you to determine the language:
import {resolveLangCode, getBrowserLang, getCookie, getUrlParam} from "$i8";
// Server-side:
let lang_s = resolveLangCode([
ev.url.searchParams.get("lang"),
ev.cookies.get("lang"),
ev.request.headers.get("accept-language"),
]);
// or Client-side:
let lang_c = resolveLangCode([
getUrlParam("lang"),
getCookie("lang"),
getBrowserLang(),
]);
// Then:
await setLang(lang_s);
// or with .then():
setLang(lang_c).then(() => {
// Do something
});
You might have already noticed the toString()
calls. This is because i8 uses a proxy to make the tree safe to use,
even
with missing translations or even entire missing keys. If you try to access a key that doesn't exist, you will just get
the
path to the key as a string: $i8.this.really.does.not.exist
will return "this.really.does.not.exist"
. To make this
work,
every key in the tree has to be an object (or a function, to allow the same thing with function calls!), so for example
calling
console.log
will log you the object, and not the actual string. i8 automatically sets the toString
and valueOf
properties,
so in places where values are cast to strings, you can just use the variable directly, like in Svelte:
<p>{$i8.foo.bar.baz}</p>
You should generally use the store & proxy mode, but if you know what you're doing, you can use the i8.static
and i8.noProxy
properties on the i8
store. i8.static
is the proxied version, but as a static property which is not
reactive. i8.noProxy
is the raw object, without the proxy. You need to be really careful with this, because missing translations might
throw errors.
For example, i8.noProxy.foo
is undefined
, but i8.noProxy.foo.bar
will throw an error, because foo
is undefined
and
you can't access properties of undefined
. Just don't do that please. Avoid these properties if you can.
The language
store stores the current language, and it is writable, so you can set it to a new language, and i8 will
load the requested language. You can also subscribe to it to get notified when the language changes. Note: language
's
subscribers will be called before the language is actually loaded, and i8
's subscribers will be called after the
language is loaded.
i8 generally should never throw any errors if you use it correctly. Accessing a missing key will just return the path to
the
key as a string, and the same works with function calls, so for
example $i8.foo.bar.baz("a").a.b(2).c({d: 3}).e.toString()
will return "foo.bar.baz(a).a.b(2).c({d: 3}).e"
and throw no errors. Even if one of your adapters fails, i8 will just
replace the string with the error message and log it to the console, but it will never throw an error.
This library heavily relies on types out of your own project. I could hard-code the types to a location
like ../../src/i18n
,
but that would make it impossible to use i8 in a monorepo, because it relies on the location of the src
folder. i8
should be
as flexible as possible, so I decided to not publish it on NPM.
To use i8 in SK, add these snippets:
import type {LayoutServerLoad} from "./$types";
import {resolveLangCode} from "$i8";
export const load: LayoutServerLoad = (async ev => {
let lang = resolveLangCode([
ev.url.searchParams.get("lang"),
ev.cookies.get("lang"),
ev.request.headers.get("accept-language"),
]);
return {lang};
});
import type {LayoutLoad} from "./$types";
import {setLang} from "$i8";
export const load: LayoutLoad = (async ({data}) => {
await setLang(data.lang);
});
MIT.