HenryLie / svelte-i18n-lingui

Use lingui for i18n in Svelte/Sveltekit projects
https://www.npmjs.com/package/svelte-i18n-lingui
MIT License
23 stars 2 forks source link
i18n internationalization lingui svelte

svelte-i18n-lingui

Add i18n to Svelte/Sveltekit projects using Lingui, with gettext-style message as catalog id.

Features

Why this library?

Motivations I created this package since I couldn't find any svelte i18n library that allows me to add i18n with a gettext-style approach, which works by: - Writing the base language text as-is in the codebase, and mark them for extraction. - A cli command will scan the codebase and extract the marked messages as a message catalog, usually .po files. - We add a translations to the po files for every supported locale. - Run another cli command to compile the catalog files into highly optimized message dictionary to be loaded during runtime. - Use the provided functions to translate the message during runtime, reacting to the user's language preference updates and updating the text on the UI in real time. Most of the i18n libraries for Svelte works by manually defining the message catalog, adding a short descriptive key name manually, and then use the keys in the code. I think this comes with a few drawbacks: - We can't see the real text in the codebase, only a short summary of it in the form of the keys. This makes the code feel detached from the rendered result. - We need to decide on the key names by ourselves depending on the context, which adds an additional mental burden every time we add text. - The manual name we pick for the key are created by us, so it can be inconsistent when there are many people working on the project unless there is a very strict naming rule enforced in the team. - To keep the name descriptive for the text, we might need to pick a long key name, which adds to the catalog's file size that the users need to download. - When we write the keys manually, it could be hard to track for duplicate keys defined more than once in the catalog. This is especially true when the messages are grouped based on the page where they are used. While looking for i18n libraries that support this requirement, I found [Lingui](https://lingui.dev/) which seems to fit this need, and it supports usage in both React and plain JS. While the plain JS version seem to work, I found some drawback when implementing them to my Svelte projects: - The plain syntax without macro usage is quite verbose. - It provides a macro that work for JS files to make the syntax much more succinct, but it doesn't work with modern Svelte projects that uses Vite as the bundler, since both Vite and Sveltekit expects libraries in ESM format. Lingui's macro depends on `babel-plugin-macros` to work, which doesn't seem to work well with Vite. - Svelte and Vite don't use Babel to transpile code, so even if `babel-plugin-macros` work we'll need to add an extra tool to do extra transpilation when compiling. - Lingui doesn't come with an extractor for Svelte files yet. - The i18n function provided is not reactive since it's just a plain js function. Due to the above challenges, I decided to build this library to replicate Lingui's macro functionality on Svelte projects. The basic syntax looks similar to Lingui's macro version, but there are some changes added on top to make it reactive on Svelte. This library also comes with an extractor for both Svelte files and js/ts files to support its customized syntax.

How it works

Messages that should be translated to different languages need to be marked for extraction. There are two ways to do this:

Installation

Firstly, install this package along with the Lingui packages.

npm install --save @lingui/core svelte-i18n-lingui
npm install --save-dev @lingui/cli

Configure Lingui

After the packages are installed, create a lingui.config.ts (or js depending on your project config) and add a basic configuration. An example of a simple project with English base language and Japanese localization:

import { jstsExtractor, svelteExtractor } from 'svelte-i18n-lingui/extractor';

export default {
    locales: ['en', 'ja'],
    sourceLocale: 'en',
    catalogs: [
        {
            path: 'src/locales/{locale}',
            include: ['src/lib', 'src/routes']
        }
    ],
    extractors: [jstsExtractor, svelteExtractor]
};

Here we are using svelte-i18n-lingui's extractors to allow for extracting messages with customized tags from both svelte components and plain js/ts files.

NPM Commands

To extract messages as catalogs and compile them for production, @lingui/cli provides cli commands that we can integrate to our project's workflow. Inside package.json, add these two commands:

{
    "scripts": {
        // Add these commands
        "extract": "lingui extract",
        "compile": "lingui compile --typescript"
    }
}

These can be added to pre-commit hooks to make sure that new text are properly extracted and compiled.

Usage

Importing the store for the first time will initiate Lingui's i18n instance with a default language and an empty message catalog.

Change the Active Locale

Locale can be changed by accessing the set method of the locale store, passing in the desired locale and message catalog. To make sure that only the necessary message catalog is downloaded, use dynamic import, e.g.:

<script lang="ts">
    import { locale } from 'svelte-i18n-lingui';

    async function setLocale(lang) {
        const { messages } = await import(`../locales/${lang}.ts`);
        locale.set(lang, messages);
    }
</script>

<button on:click={() => setLocale('en')}>Switch to English</button>
<button on:click={() => setLocale('ja')}>Switch to Japanese</button>

Basic Translations

To start translating in Svelte files, import the t store and auto-subscribe to it by prefixing with $:

<script lang="ts">
    import { t } from 'svelte-i18n-lingui';
</script>

<!-- Use directly as text element -->
{$t`hello`}

<!-- Use as attribute or prop -->
<ComponentA propName={$t`hello`} />

<!-- Supports parameterized text, for cases where different language has different order -->
{$t`Proceed to ${$t`cart`}`}

Predefined Messages

Since the extractor does a static parse of the code, messages must be in plain string to be extractable. Template literals or variables names won't work.

<script lang="ts">
    import { t } from 'svelte-i18n-lingui';

    const text = 'message';
</script>

{$t`${text}`}<!-- !!! Will not work, extractor cannot find the message to extract !!! -->

Instead, mark the string as extractable first with the provided msg function, and pass it to the store later on as a plain string (not as tagged template literal)

<script lang="ts">
    import { t, msg } from 'svelte-i18n-lingui';

    const text = msg`message`;
</script>

{$t(text)}

Interpolate Elements/Components

To include components or elements in the middle of the message, use the provided <T> component and use # characters to add slots in the middle of the text to insert elements or components. The first parameter is supplied through the default children snippet (as the content of the component), while subsequent parameters use snippets (second, third, fourth, fifth corresponding to the index of the parameter, starting from 1 for the default children snippet).

<script lang="ts">
    import { T } from 'svelte-i18n-lingui';
</script>

<T msg="Click # for #">
    <a href="https://github.com/HenryLie/svelte-i18n-lingui/blob/main/about">{$t`here`}</a>
    {#snippet second()}{$t`more information`}{/snippet}
</T>

MessageDescriptor Format

Sometimes we'll need to add a context info for messages that are exactly the same in the base language, but has different meanings in different places (e.g. in English right can either refer to direction or correctness). We can add a context by passing a message descriptor instead of plain string or literal string:

{$t({
    message: 'right',
    context: 'direction'
})}

The context will be added on top of the message in the catalog, both of them combined are treated as a unique key for the message. Therefore, the same message with a different context will be extracted as separate entries in the catalog and can have its own translations.

We can also add a comment for the translators reading the message catalog e.g. to let them know how the message is going to be used. This is added as a pure comment on the message catalog and won't affect how the message is extracted or compiled.

{$t({
    message: 'text',
    context: 'message for translator'
})}

Plurals

To provide different messages for text with numbers, use the provided $plural store:

{$plural(count, {
    one: '# item',
    other: '# items'
})}

Refer to Lingui's pluralization guide for more information on what keys are accepted on different locales.

Usage Outside of Svelte Components

Since Svelte's stores are meant to be used in Svelte components, using them inside plain js/ts files is a bit awkward since you need to use the get helper to subscribe, get the value, and then immediately unsubscribe afterwards. svelte-i18n-lingui provides another way to access Lingui's i18n object through plain strings:

Migration guide

v0.1.x to v0.2.x - Update Node version to at least v20 - If the project is still on Svelte 4: - You'll need to change all usages of the `` component to use the new `` component instead. Otherwise the syntax stays exactly the same with slots. - If the project is on Svelte 5: - If you're not ready to use runes mode yet, you can keep using the slots syntax by modifying the usages of `` components to use the `` component instead. Other than the component name and import changes, the syntax stays the same. - If you're migrating to runes mode, use the new `` component and switch to using snippets instead of slots if you have more than one parameter in the sentence. If there is only one parameter, then there is no need to change anything since default slot and children snippet has the same syntax.

Known issues

Contributing

Found a bug? Feel free to open an issue or PR to fix it!