Tutorial

First steps

You need to provide your translations to svelte-fluent by adding the FluentProvider component in your component hierarchy.

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

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

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

<FluentProvider bundles={[bundle]}>
	<Localized id="hello" />
</FluentProvider>
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.

The bundler must support importing .ftl files as strings:

<script>
	import { FluentBundle, FluentResource } from '@fluent/bundle';
	import { FluentProvider, Localized } from '@nubolab-ffwd/svelte-fluent';
	import translationsEn from './en.ftl?raw';

	const resource = new FluentResource(translationsEn);
	const bundle = new FluentBundle('en');
	bundle.addResource(resource);
</script>

<FluentProvider bundles={[bundle]}>
	<Localized id="hello" />
</FluentProvider>
en.ftl
hello = Hello, world!
Result:
Hello, world!

Multiple languages

With the basics in place we can now extend this to multiple languages. This example shows selection of the desired language and provides a fallback to the auto-detected language from 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.

<script>
	import { FluentBundle, FluentResource } from '@fluent/bundle';
	import { FluentProvider, Localized } from '@nubolab-ffwd/svelte-fluent';
	import { negotiateLanguages } from '@fluent/langneg';
	import translationsEn from './en.ftl?raw';
	import translationsDe from './de.ftl?raw';
	import translationsFr from './fr.ftl?raw';

	// this could be stored in a user profile or browser localStorage
	export let selectedLocale = '';

	const defaultLocale = 'en';
	const resources = {
		en: new FluentResource(translationsEn),
		fr: new FluentResource(translationsFr),
		de: new FluentResource(translationsDe)
	};
	const supportedLocales = Object.keys(resources);

	function generateBundles(userLocales) {
		// 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;
		});
	}
</script>

<FluentProvider bundles={generateBundles(selectedLocale ? [selectedLocale] : navigator.languages)}>
	<Localized id="hello" />
</FluentProvider>
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, FluentResource } from '@fluent/bundle';
	import { FluentProvider, Localized } from '@nubolab-ffwd/svelte-fluent';
	import translationsEn from './en.ftl?raw';

	const resource = new FluentResource(translationsEn);
	const bundle = new FluentBundle('en');
	bundle.addResource(resource);
</script>

<FluentProvider bundles={[bundle]}>
	<Localized id="hello" args={{ name: 'everyone' }} />
</FluentProvider>
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, FluentResource } from '@fluent/bundle';
	import { FluentProvider, Localized } from '@nubolab-ffwd/svelte-fluent';
	import translationsEn from './en.ftl?raw';

	const resource = new FluentResource(translationsEn);
	const bundle = new FluentBundle('en');
	bundle.addResource(resource);
</script>

<FluentProvider bundles={[bundle]}>
	<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>
</FluentProvider>
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 ⁨May 6, 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,
		FluentResource
	} from '@fluent/bundle';
	import { FluentProvider, Localized } from '@nubolab-ffwd/svelte-fluent';
	import translationsEn from './en.ftl?raw';

	function values(opts, allowed) {
		const unwrapped = Object.create(null);
		for (const [name, opt] of Object.entries(opts)) {
			if (allowed.includes(name)) {
				unwrapped[name] = opt.valueOf();
			}
		}
		return unwrapped;
	}

	const WEEKDAY_ALLOWED = ['weekday'];

	function WEEKDAY(args, opts) {
		const arg = args[0];

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

		if (arg instanceof FluentDateTime) {
			return new FluentDateTime(arg.valueOf(), {
				...values(arg.opts, WEEKDAY_ALLOWED),
				...values(opts, WEEKDAY_ALLOWED)
			});
		}

		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',
				...values(opts, WEEKDAY_ALLOWED)
			});
		}

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

	const resource = new FluentResource(translationsEn);
	const bundle = new FluentBundle('en', { functions: { WEEKDAY } });
	bundle.addResource(resource);
</script>

<FluentProvider bundles={[bundle]}>
	<Localized id="weekday-is" args={{ weekday: 2 }} />
</FluentProvider>
en.ftl
weekday-is = Weekday { $weekday } is { WEEKDAY($weekday) }
Result:
Weekday ⁨2⁩ is ⁨Tuesday⁩