8.1.0 • Published 3 days ago

@288-toolkit/i18n v8.1.0

Weekly downloads
-
License
MIT
Repository
-
Last release
3 days ago

i18n

i18n handles

You can create three i18n handles with the createI18nHandles function:

  • langInfo: Sets the locale, language, and region in the event locals.
  • langRedirect: Redirects to the user's preferred language if the site is localized (has more than one supported locale) and the request is for the root
  • langAttribute: Renders the correct html lang attribute
import { createI18nHandles } from '@288-toolkit/i18n/handles';

const { langInfo, langRedirect, langAttribute } = createI18nHandles({
	supportedLocales: ['en-ca', 'fr-ca'],
	defaultLocale: ['en-ca']
});

export const handle = sequence(i18n(langInfo, langRedirect, langAttribute));

Make sure the langInfo is always placed before the other two.

Next, you need to change the html lang attribute inside app.html to %lang% so that it be replaced by the current language.

Finally, to get type safety for the locals, you need to extend the App.Locals interface.

In app.d.ts:

import type { LangInfo } from '@288-toolkit/i18n/types';

declare global {
	namespace App {
		interface Locals extends LangInfo<typeof ['en-ca', 'fr-ca']> {
			// ...
		}
	}
}

Translation files

You must write translations in a .ts file named after the language. For example, if your supported languages are 'en' and 'fr', you will have en.ts dans fr.ts.

In en.ts:

export const en = {
	hello: 'world',
	color: 'green'
};

Typescript can help you making sure that your translation objects all have the same form. You can generate a type from the default translations and use it to cast the other objects. Typescript will indicate an error if properties don't match.

In en.ts:

export type GlobalTranslations = typeof en;

In fr.ts:

import type { GlobalTranslations } from './en';

export const fr: GlobalTranslations = {
	hello: 'monde',
	colorr: 'vert' // Typescript error
};

These files should all live inside the same folder. You can split your translations in as many folders as you want and load them only as needed.

Loading translations

To load the translations, you must create a loader with createTranslationsLoader. For every translation, you must provide a key and a loader for every supported language. A loader is essentially a function that returns a dynamic import.

!IMPORTANT Do NOT await the import or it will not work. The 'awaiting' happens later.

Because of how Vite processes dynamic imports, there are several limitations to keep in mind when writing the import path. They are listed here: https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations.

import { createTranslationsLoader } from '@288-toolkit/i18n/translations/server';

export const loadTranslations = createTranslationsLoader([
	{
		key: 'global',
		loaders: {
			en: () => import('./lib/translations/global/en'),
			fr: () => import('./lib/translations/global/fr')
		}
	},
	{
		key: 'newsletter',
		loaders: {
			en: () => import('./lib/translations/newsletter/en'),
			fr: () => import('./lib/translations/newsletter/fr')
		}
	}
]);

To load some translations for a current layout or route, you can use loadTranslations() inside a load function. It accepts an array of translation keys and the current language.

The function returns an object containing all the loaded translations. Each translation has a unique key which is used by the createTranslate() function to pick up the translations on the client. Therefore, it is important the you always spread the object in the returned data.

export const load = async (event) => {
	// We can access `language` from the event locals thanks to the i18n handle
	const language = event.locals.language;
	const translations = await loadTranslations(['global', 'newsletter'], language);
	return {
		...translations
	};
};

Using translations

To use these translations in your project, you need to create a translation function with createTranslate(). This function accepts a key that corresponds to the translation key in the config (the same that you used with loadTranslations()). You can also pass your translation type to get typesafety for the translation keys.

import { createTranslate } from '@288-toolkit/i18n/translations/client';

export const t = createTranslate<GlobalTranslations>('global');
import { createTranslate } from '@288-toolkit/i18n/translations/client';

export const t = createTranslate<NewsletterTranslations>('newsletter');

The translation function

The translation function accepts as a first argument a string with the property key you want to access. It will return the property value associated with the current language. You can access nested properties by using dot notation ('my.nested.property').

The translation values can be strings, numbers, booleans, arrays and objects. string is the default return type. If you want to get a different type, use the first template parameter to specify a different return type.

In fr.ts:

const PROJECT = {
	translatableTitle: 'This is my title',
	my: {
		nested: {
			property: 'This is my nested property'
		}
	},
	myArrayProp: ['hello', 'world'],
	myComplexProp: [
		{
			id: 1,
			firstName: 'Oliver',
			lastName: 'Twist'
		},
		{
			id: 2,
			firstName: 'Luke',
			lastName: 'Skywalker'
		}
	]
};

In fr.ts:

const PROJECT = {
	translatableTitle: 'Ceci est mon titre',
	my: {
		nested: {
			property: 'Ceci est pas propriété imbriquée'
		}
	},
	myArrayProp: ['bonjour', 'monde'],
	myComplexProp: [
		{
			id: 1,
			firstName: 'Oliver',
			lastName: 'Twist'
		},
		{
			id: 2,
			firstName: 'Luke',
			lastName: 'Skywalker'
		}
	]
};

In AnyFile.svelte:

<script lang="ts">
	import { t } from '$lib/translations/global';

	// Getting an array of strings
	const myArrayProp = t<string[]>('myArrayProp');

	// Getting a complex type
	const myComplexProp = t<Translations['myComplexProp']>('myComplexProp');
</script>

<h1>{t('translatableTitle')}</h1>
<h2>{t('my.nested.property')}</h2>

// At /en, output is:
<h1>This is my title</h1>
<h2>This is my nested property</h2>
// At /fr, output is:
<h1>Ceci est mon titre</h1>
<h2>Ceci est pas propriété imbriquée</h2>

{#each myArrayProp as word}
	{word}
	<br />
{/each}
{#each myComplexProp as person}
	{person.id} - {person.firstName}
	{person.lastName}
	<br />
{/each}

If no translation is found, a warning will be printed to the browser console (in dev mode only). The output of the function will be the key passed in.

Dynamic data in translations

You can have dynamic translations with the bracket syntax:

In en.ts:

const PROJECT = {
	myDynamicTranslation: 'Available from {startDate} to {endDate}'
};

In fr.ts:

const PROJECT: Translation.Project = {
	myDynamicTranslation: 'Disponible du {startDate} au {endDate}'
};

You can then pass a data object containing the dynamic values as a second argument to the t function.

These will work with any value type (string, array or object).

In AnyFile.svelte

<script lang="ts">
	import { t } from '$lib/translations/global';
</script>

<p>{t('myDynamicTranslation', { startDate: '13/01/2021', endDate: '16/01/2021' })}</p>

// At /en, output is:
<p>Available from 13/01/2021 to 16/01/2021</p>
// At /fr, output is:
<p>Disponible du 13/01/2021 au 16/01/2021</p>

Pluralization

The API also supports pluralization. It uses the Intl.PluralRules object. You may look at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules to familiarize yourself with it.

In order to pluralize a message, you must provide multiple alternatives, each separated by a rule formatted as follow:

|<rule>:

When calling the t function, you must provide a count number in the data object. The count will determine which alternative is returned.

The available rules are the following, ordered by priority:

  • A specific number, like 4 or 32.
  • A range of two numbers separated by a dash, like 2-5.
  • One of the tags returned by Intl.PluralRules.prototype.select(), which are: zero, one, two, few, many, and other.

For example, with the following message string:

const en = {
	relatedArticles: `
	|one: One related article
	|other: {count} related articles
	|4: Four related articles
	|2-4: A few related articles`
};

we get the following result:

t('relatedArticles', { count: 0 }); // '0 related articles' (corresponds to the 'other' tag)
t('relatedArticles', { count: 1 }); // 'One related article' (corresponds to the 'one' tag)
t('relatedArticles', { count: 2 }); // 'A few related articles' (corresponds to the '2-4' range)
t('relatedArticles', { count: 4 }); // 'Four related articles' (corresponds to the number '4')

This API also supports ordinal pluralization. To enable it, pass ordinal: true in the data object.

For example, with the following message string:

const en = {
	ordinalSuffix: `
	|one: st
	|two: nd
	|few: rd
	|other: th`
};

we get the following result:

t('ordinalSuffix', { count: 0, ordinal: true }); // 'th' (corresponds to the 'other' tag)
t('ordinalSuffix', { count: 1, ordinal: true }); // 'st' (corresponds to the 'one' tag)
t('ordinalSuffix', { count: 2, ordinal: true }); // 'nd' (corresponds to the 'two' tag)
t('ordinalSuffix', { count: 3, ordinal: true }); // 'rd' (corresponds to the 'few' tag)
t('ordinalSuffix', { count: 4, ordinal: true }); // 'th' (corresponds to the 'other' tag)

Translations on the server

To use translations on the server, you need to use the server version of createTranslate(). The differences are that this one returns a promise, accepts a Translation object as first argument (instead of a key) and requires the language as second argument. The returned function is exactly the same as the client side version.

import { createTranslate } from '@288-toolkit/i18n/translations/server';
import type { Translation } from '@288-toolkit/i18n/types';

const globalTranslation: Translation = {
	key: 'global',
	loaders: {
		en: () => import('./lib/translations/global/en.ts'),
		fr: () => import('./lib/translations/global/fr.ts')
	}
};

export const load = async (event) => {
	const t = await createTranslate<GlobalTranslations>(globalTranslation, event.locals.language);
	return {
		title: t('myTitle')
	};
};
8.1.0

3 days ago

8.0.7

10 days ago

8.0.5

11 days ago

8.0.4

11 days ago

8.0.6

11 days ago

8.0.3

11 days ago

7.0.0

12 days ago

7.0.1

12 days ago

8.0.1

12 days ago

8.0.0

12 days ago

8.0.2

12 days ago

5.0.6

13 days ago

5.0.5

17 days ago

5.0.4

18 days ago

5.0.3

26 days ago

5.0.2

26 days ago

5.0.1

26 days ago

5.0.0

26 days ago

4.0.6

1 month ago

4.0.5

1 month ago

4.0.4

1 month ago

4.0.3

1 month ago

4.0.2

1 month ago

4.0.1

1 month ago

4.0.0

1 month ago

2.6.3

1 month ago

2.6.2

1 month ago

2.6.1

1 month ago

2.6.0

1 month ago

2.5.0

1 month ago

2.4.1

1 month ago

2.4.0

1 month ago

2.3.0

1 month ago

2.2.0

1 month ago

2.1.0

1 month ago

2.0.0

1 month ago

1.1.1

1 month ago

1.1.0

1 month ago

1.0.0

1 month ago