Tutorial

First steps

You need to provide your translations to svelte-fluent by creating a SvelteFluent object and initializing the FluentContext.

In the most basic setup those translations can be defined directly in the code like this:

<script>
	import { FluentBundle, FluentResource } from '@fluent/bundle';
	import { createSvelteFluent, initFluentContext, Localized } from '@nubolab-ffwd/svelte-fluent';

	const translations = 'hello = Hello, world!';
	const bundle = new FluentBundle('en');
	bundle.addResource(new FluentResource(translations));

	initFluentContext(() => createSvelteFluent([bundle]));
</script>

<Localized id="hello" />
Result:
Hello, world!

Load translations from files

Managing translations directly in the code can get messy. A better way is to load translations from .ftl files.

<script>
	import { FluentBundle } from '@fluent/bundle';
	import { createSvelteFluent, initFluentContext, Localized } from '@nubolab-ffwd/svelte-fluent';
	import resourceEn from './en.ftl';

	const bundle = new FluentBundle('en');
	bundle.addResource(resourceEn);

	initFluentContext(() => createSvelteFluent([bundle]));
</script>

<Localized id="hello" />
en.ftl
hello = Hello, world!
Result:
Hello, world!

Multiple languages

We can now extend this to multiple languages. Let’s add a selection for the desired language and fallback to the auto-detected language of the browser.

This example will fail when used with server-side rendering (SSR) because during SSR the component cannot use browser-only globals like navigator.languages.

As an alternative that works in SSR you can parse the HTTP Accept-Language header and replace navigator.languages with the resulting list of locale identifiers.

Check out the SvelteKit integration guide for an example how to do this.

<script>
	import { FluentBundle } from '@fluent/bundle';
	import { createSvelteFluent, initFluentContext, Localized } from '@nubolab-ffwd/svelte-fluent';
	import { negotiateLanguages } from '@fluent/langneg';
	import resourceEn from './en.ftl';
	import resourceDe from './de.ftl';
	import resourceFr from './fr.ftl';

	// this could be stored in a user profile or browser localStorage
	let { selectedLocale = '' } = $props();

	const defaultLocale = 'en';
	const resources = {
		en: resourceEn,
		fr: resourceFr,
		de: resourceDe
	};
	const supportedLocales = Object.keys(resources);

	const bundles = $derived.by(() => {
		const userLocales = selectedLocale ? [selectedLocale] : navigator.languages;
		// Choose locales that are best for the user.
		const selectedLocales = negotiateLanguages(userLocales, supportedLocales, {
			defaultLocale,
			strategy: 'lookup'
		});
		// prepare fluent bundles
		return selectedLocales.map((locale) => {
			const bundle = new FluentBundle(locale);
			bundle.addResource(resources[locale]);
			return bundle;
		});
	});
	initFluentContext(() => createSvelteFluent(bundles));
</script>

<Localized id="hello" />
en.ftl
hello = Hello, world!
fr.ftl
hello = Salut le monde !
de.ftl
hello = Hallo Welt!
Result:
Hello, world!

Interpolation

You can insert variables into your translated text by using Fluent Placeables. Values for those variables are provided via the args prop of the Localized and Overlay components.

<script>
	import { FluentBundle } from '@fluent/bundle';
	import { createSvelteFluent, initFluentContext, Localized } from '@nubolab-ffwd/svelte-fluent';
	import resourceEn from './en.ftl';

	const bundle = new FluentBundle('en');
	bundle.addResource(resourceEn);

	initFluentContext(() => createSvelteFluent([bundle]));
</script>

<Localized id="hello" args={{ name: 'everyone' }} />
en.ftl
hello = Hello, { $name }!
Result:
Hello, ⁨everyone⁩!

Interpolation formatting

Fluent outputs interpolations in a human readable format appropriate to the currently used locale. You can customize the formatting by using Fluent Functions.

Formatting parameters listed in “Parameters” in the Fluent Functions documentation can be set both in the .ftl files or in the JS source.

Parameters listed in “Developer parameters” can only be set in JS code.

<script>
	import { FluentBundle, FluentNumber } from '@fluent/bundle';
	import { createSvelteFluent, initFluentContext, Localized } from '@nubolab-ffwd/svelte-fluent';
	import resourceEn from './en.ftl';

	const bundle = new FluentBundle('en');
	bundle.addResource(resourceEn);

	initFluentContext(() => createSvelteFluent([bundle]));
</script>

<div><Localized id="dpi-ratio" args={{ ratio: 16 / 9 }} /></div>
<div>
	<Localized
		id="balance"
		args={{ balance: new FluentNumber(1234.56, { style: 'currency', currency: 'USD' }) }}
	/>
</div>
<div><Localized id="today-is" args={{ date: new Date() }} /></div>
en.ftl
dpi-ratio = Your DPI ratio is { NUMBER($ratio, minimumFractionDigits: 2) }
balance = Your account balance is { $balance }
today-is = Today is { DATETIME($date, month: "long", year: "numeric", day: "numeric") }
Result:
Your DPI ratio is ⁨1.778⁩
Your account balance is ⁨$1,234.56⁩
Today is ⁨December 9, 2024⁩

Custom functions

You can extend the default Fluent Functions with custom formatting functions by adding them to the functions option of FluentBundle.

You can also check out the code of the built-in Fluent Functions for more examples.

<script>
	import { FluentBundle, FluentDateTime, FluentNone, FluentNumber } from '@fluent/bundle';
	import { createSvelteFluent, initFluentContext, Localized } from '@nubolab-ffwd/svelte-fluent';
	import resourceEn from './en.ftl';

	// eslint-disable-next-line @typescript-eslint/no-unused-vars
	function WEEKDAY(args, opts) {
		const arg = args[0];

		if (arg instanceof FluentNone) {
			return new FluentNone(`WEEKDAY(${arg.valueOf()})`);
		}

		if (arg instanceof FluentNumber) {
			const date = new Date(Date.now());
			const offset = arg.valueOf() - date.getDay();
			date.setDate(date.getDate() + offset);
			return new FluentDateTime(date.valueOf(), {
				weekday: 'long'
			});
		}

		if (arg instanceof FluentDateTime) {
			return new FluentDateTime(arg.valueOf(), {
				weekday: 'long'
			});
		}

		throw new TypeError('Invalid argument to WEEKDAY');
	}

	const bundle = new FluentBundle('en', { functions: { WEEKDAY } });
	bundle.addResource(resourceEn);

	initFluentContext(() => createSvelteFluent([bundle]));
</script>

<div>
	<Localized id="weekday-number" args={{ weekday: 2 }} />
</div>
<div>
	<Localized id="weekday-date" args={{ date: new Date(2024, 1, 6) }} />
</div>
en.ftl
weekday-number = Weekday { $weekday } is { WEEKDAY($weekday) }
weekday-date = Weekday of { $date } is { WEEKDAY($date) }
Result:
Weekday ⁨2⁩ is ⁨Tuesday⁩
Weekday of ⁨2/6/2024⁩ is ⁨Tuesday⁩