@sveltevietnam/i18n
v0.2.3
Published
composable and minimal i18n solution for Svelte & SvelteKit
Maintainers
Readme
@sveltevietnam/i18n
A homebrew i18n solution for Svelte applications, built for type-safety, composability, and optimization, by leveraging:
- SvelteKit remote function,
- Vite Plugin API,
- tree-shakeable ES modules.
See more in the "Internationalisation with Remote Function — A Proof of Concept" blog post.
Changelog
See CHANGELOG.md.
Introduction
This package allows the following pattern:
messages:
just_string: Goodbye!
some_html: '<strong>Welcome!</strong>'
string_with_params: Hello {{name}}!
nested:
message: This is a nested message.
imports:
from_local:
'@import': ./path/to/another/locale.yaml
from_alias:
'@import': '$lib/components/my-component/locales/lang.yaml'
from_package:
'@import': '@design-system/another-component/locales/lang.yaml'<script>
import { T } from '@sveltevietnam/i18n';
</script>
<!-- type safe -->
<T key="some_html" />
<T key="string_with_params" params={{ name: 'world' }} />
<T key="nested.message" />and more...
Getting Started
1. Install Dependencies
npm install --save-dev @sveltevietnam/i18n
yarn add --dev @sveltevietnam/i18n
pnpm add -D @sveltevietnam/i18n2. Use the Vite Plugin
import { sveltekit } from '@sveltejs/kit/vite';
import { i18n } from '@sveltevietnam/i18n/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
i18n({
input: './src/lib/i18n/locales', // where your locale files live (en.yaml, vi.yaml, etc)
output: './src/lib/i18n/generated', // where generated modules live
}),
sveltekit(),
],
});3. Configure Svelte & SvelteKit
Turn on experimental Async Svelte and SvelteKit remote function, if your setup allows so
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
export default {
preprocess: vitePreprocess(),
compilerOptions: {
experimental: {
async: true,
},
},
kit: {
adapter: adapter(),
experimental: {
remoteFunctions: true,
},
},
};[!NOTE] If you cannot use above experimental features, you can still use the package in "static" mode. See Remote vs Static Mode for details.
4. Define Locales
Define your locale files as <lang>.yaml under the input folder you specified in the Vite config,
for example:
lib/
i18n/
locales/ <- input folder
en.yaml <- messages in English
vi.yaml <- messages in Vietnamese
generated/ <- output folder...with format as seen in Introduction:
messages: ...5. Configure I18N Provider
Configure i18n provider where appropriate, e.g., in src/routes/+layout.svelte:
<script lang="ts">
import { Provider } from '@sveltevietnam/i18n';
import { type Language } from '@sveltevietnam/i18n/generated';
let lang = $state<Language>('vi');
// manage lang state as you see fit
// change to lang will propagate to translations automatically
</script>
<Provider {lang} remote="prerender">... your app here ...</Provider>[!NOTE] See "Choose your Remote Function" for more details on how to specify the
remoteprop.
Translating Messages
There are several ways to translate your messages, and using T component is recommended whenever
possible.
Using T Component
<script lang="ts">
import { T } from '@sveltevietnam/i18n';
</script>
<T key="key.to.your.message" />[!NOTE] Typing should be inferred. If your message specs contains parameters, additional
paramsprop is expected.
Using t function from context
Internally, T component uses a t function from the i18n context, which you also have access to:
<script lang="ts">
import { Context } from '@sveltevietnam/i18n';
const { t } = Context.get();
</script>
{await t({ key: 'key.to.your.message' })}The interface of t input mirrors that of T prop:
[!IMPORTANT] A few things to note:
- In the default "remote" mode,
tis asynchronous (calls remote function internally).- You will need to handle html string yourself, i.e using
{@html ...}.
This is helpful when you are translating non-html messages for attributes, e.g.,
<dialog aria-label={await t({ key: 'key.to.your.message' })}></dialog>
<!-- or -->
<input placeholder={await t({ key: 'key.to.your.message' })} />In case of dialog in the above example, alternatively, you can still use T component with
aria-labelledby
T component can sill be used.
Overriding Lang
Both T component and t function accept an optional lang parameter in case you need to
override the current language from context.
<T key="key.to.your.message" lang="en" />
{await t({ key: 'key.to.your.message', lang: 'en' })}Using Static Messages
Sometimes translation happens outside of i18n context for T and t, in which case
you can import and use the generated static messages directly:
// in server, or some isolated modules
import * as m from '@sveltevietnam/i18n/generated/messages';
const message = m['key.to.your.message'];[!NOTE] When importing static messages, use the wildcard import syntax (
* as m) to better facilitate tree-shaking.
Using Remote Functions
In "remote" mode, t internally calls the generated remote function, which you can also import and
use directly:
import { query, prerender } from '@sveltevietnam/i18n/generated/t.remote';
const translated = await query({
// or prerender
lang: 'en',
key: 'string_with_params',
params: { name: 'world' }, // inferred from key
});Similar to t, you will need to handle html string yourself.
Choose your Remote Function
In "remote" mode, Provider, T, and t accept a remote parameter (required on
provider) that specifies which remote function to fetch translation from. remote can be:
prerender: uses SvelteKit prerender via the generatedprerenderfunction at<output-dir>/t.remote.js. This is usually what you want if you've turned onprerenderingfor your page(s),query: uses SvelteKit query.batch via the generatedqueryfunction at<output-dir>/t.remote.js. This can batch multiple translation requests but may not be able to utilize cache,- your own: import yours from some
.remote.{js,ts}and pass it here to provide an implementation that works for your setup. The generated modules are at your disposal.
Global Remote Function
<script lang="ts">
import { Provider } from '@sveltevietnam/i18n';
let lang = $state<Language>('vi');
</script>
<Provider {lang} remote="prerender">
<!--
unless specified otherwise, all children T and t will fetch from
import('@sveltevietnam/i18n/generated/t.remote').prerender
-->
</Provider>[!NOTE] Context may also be set programmatically via the imported Context class, i.e
Context.set.
Remote Function per Translation
<script lang="ts">
import { T, Context } from '@sveltevietnam/i18n';
const { t } = Context.get();
</script>
<!-- these use whatever remote function specified in provider -->
<T key="key.to.message" />
{await t({ key: 'key.to.message' })}
<!-- these overrides to use prerender function -->
<T key="key.to.message" remote="query" />
{await t({ key: 'key.to.message', remote: 'query' })}Remote vs Static Mode
Remote Mode
By default, the package builds in "remote" mode and assumes you have enabled experimental features
as discussed in Configure Svelte & SvelteKit. This leverages
Svelte & SvelteKit capabilities for optimization. i.e translations are lazily fetched only when and where needed.
Static Mode
In cases, however, you don't have access to those features, the package can still run in "static" mode.
This also helps if your project is not using SvelteKit (assuming Svelte+Vite).
Start by passing mode: 'static' to vite plugin config:
// ...truncated...
i18n({
mode: 'static',
});
// ...truncated...The interface of T component changes in static mode. You will need to pass in static messages
from the generated modules instead:
<script lang="ts">
import { T } from '@sveltevietnam/i18n';
import * as m from '@sveltevietnam/i18n/generated/messages'; // or where you defined as output
</script>
<T message={m['key.to.your.message']} />Similar restrictions apply to t function from context.
HTML Sanitization
Both T component and t function perform HTML sanitization by default,
using sanitize-html, and will strip all attributes
except id, class and data-*, or href, target, and rel for a anchor tags.
Customization is permissible via the sanitize parameter to Provider, T component, or t function.
<Provider sanitize={customSanitize}>...</Provider>
<T key="key.to.message" sanitize={customSanitize} />
{t({ key: 'key.to.message', sanitize: customSanitize })}Output Modules
When running the Vite plugin, the output option specifies the directory that will host the
following modules:
<output>/ <- e.g. ./src/lib/i18n/generated
messages/
index.js
[lang].js
i18n.d.ts
constants.js
t.remote.jsmessages/[lang].js: each contain language-specific "message targets".messages/index.js: contains static "messages". This is the module that@sveltevietnam/i18n/generated/messagesaliases to. See Using Static Messages for some more information.constants.js: contains some helpful constants like languages, keys, current mode, can be imported as@sveltevietnam/i18n/generated/constants. See Using Remote Functions for some more information.i18n.d.ts: module augmentation for on-demand typing support.t.remote.js: contains the remote functions for translation, can be imported from@sveltevietnam/i18n/generated/t.remote.
Although importing from @sveltevietnam/i18n/generated is most convenient, you can also opt to
import directly from the output directory. For example, in SvelteKit, that may look like:
import { langs } from '$lib/i18n/generated/constants';
import * as m from '$lib/i18n/generated/messages';
import { query } from '$lib/i18n/generated/t.remote';This is helpful if you are reusing code in non-vite context, e.g. in Playwright tests.
Composability
Locale files can import other locale files via the special @import directive, as seen in
Introduction. This allows you to break down your locale files into smaller,
manageable pieces, and even reuse locale files from other packages.
[!IMPORTANT] The
@sveltevietnam/i18nVite plugin, however, expects a single entry locale file per language. Using multiple instances of the plugin is feasible but not recommended as type augmentation may not work as expected. If you do this, proceed with caution.
Import aliases inherit from other vite plugins including $lib from SvelteKit or others you've
configured in svelte.config.js. Customization is possible via option to the i18n vite plugin.
Why YAML?
This package was originally built to facilitate internationalization for the
sveltevietnam.dev site, which has not yet require complex features
such as pluralization. As such, YAML was chosen for simplicity,
i.e easy to read, easy to parse, compared to more sophisticated formats such as
ICU or
GNU gettext.
This may change in future releases. Contributions and discussions are welcome.
Using JSON
You can customize the parser to accept JSON files instead:
i18n({
input: './src/lib/i18n/locales',
output: './src/lib/i18n/generated',
parseOptions: {
deserializer: {
parse: ({ content }) => JSON.parse(content),
},
},
});Feedback and contributions are welcome. Start at CONTRIBUTING.md.
