Integration

SvelteKit

Initialize a SvelteKit app

Use npm create to initialize an new SvelteKit project called svelte-fluent-sveltekit.

  • Select “Skeleton project” when asked for the template
  • Select “Yes, using TypeScript syntax” when asked for typescript type checking
npm create svelte@latest svelte-fluent-sveltekit
cd svelte-fluent-sveltekit
npm install

Now install svelte-fluent:

npm install --save-dev @nubolab-ffwd/svelte-fluent
npm install --save jsdom

We also need @fluent/bundle to parse the .ftl files:

npm install --save-dev @fluent/bundle

We will be using @fluent/langneg for selecting the displayed language based on the browser’s Accept-Language header. Let’s install it with:

npm install --save-dev @fluent/langneg

Some features of svelte-fluent require a vite plugin to function. Let’s add it to vite.config.ts:

 import { sveltekit } from '@sveltejs/kit/vite';
 import { defineConfig } from 'vite';
+import svelteFluent from '@nubolab-ffwd/svelte-fluent/vite';

 export default defineConfig({
-       plugins: [sveltekit()]
+       plugins: [svelteFluent(), sveltekit()]
 });

Create translation files

Let’s create some translation files that we can use in our application:

# src/translations/en.ftl

welcome = Welcome to svelte-fluent!
# src/translations/de.ftl

welcome = Willkommen bei svelte-fluent!

Load translations and select language

Now that we have some translation files, we can load them. We also want our app to respect the browser language settings of our visitors. Let’s create some helpers for that in src/lib/fluent.ts:

// src/lib/fluent.ts

import { FluentBundle, FluentResource } from '@fluent/bundle';
import { acceptedLanguages, negotiateLanguages } from '@fluent/langneg';
import type { RequestEvent } from '@sveltejs/kit';
import resourcesDe from '../translations/de.ftl';
import resourcesEn from '../translations/en.ftl';

const defaultLocale = 'en';

const resources: Record<string, FluentResource> = {
	de: resourcesDe,
	en: resourcesEn
};

export function generateBundles(locale: string): FluentBundle[] {
	const bundle = new FluentBundle(locale);
	bundle.addResource(resources[locale]);
	return [bundle];
}

export function negotiateLocale(ev: RequestEvent): string {
	const accepted = acceptedLanguages(ev.request.headers.get('accept-language') ?? '');
	return (
		negotiateLanguages(accepted, Object.keys(resources), {
			defaultLocale,
			strategy: 'lookup'
		}).at(0) ?? defaultLocale
	);
}

Typescript will complain about the imports of the .ftl files. Don’t worry, we’ll fix this in a moment.

Add server hook

We need to add a SvelteKit server hook that selects the appropriate locale and creates the SvelteFluent object.

Let’s use the new helpers we created to build the hook:

// src/hooks.server.ts

import { generateBundles, negotiateLocale } from '$lib/fluent';
import type { Handle } from '@sveltejs/kit';
import { createSvelteFluent } from '@nubolab-ffwd/svelte-fluent';

export const handle: Handle = async ({ event, resolve }) => {
	event.locals.locale = negotiateLocale(event);
	event.locals.fluent = createSvelteFluent(generateBundles(event.locals.locale));

	return resolve(event);
};

Now Typescript will complain about event.locals.locale and event.locals.fluent. To fix this we need to modify our src/app.d.ts:

+import '@nubolab-ffwd/svelte-fluent/types';
+import { SvelteFluent } from '@nubolab-ffwd/svelte-fluent';
+
 // See https://kit.svelte.dev/docs/types#app
 // for information about these interfaces
 declare global {
        namespace App {
                // interface Error {}
-               // interface Locals {}
+               interface Locals {
+                       locale: string;
+                       fluent: SvelteFluent;
+               }
                // interface PageData {}
                // interface PageState {}
                // interface Platform {}

Client integration

On the client side, it’s impossible to access event.locals.locale and event.locals.fluent that we added in the server hook. We need some additional code to bridge the gap.

Let’s start by adding a src/routes/+layout.server.ts:

// src/routes/+layout.server.ts

export function load(event) {
	// expose selected locale from hook to client
	return { locale: event.locals.locale };
}

This exposes event.locals.locale as event.data.locale on the client. We also need the SvelteFluent object which we can create in src/routes/+layout.ts:

// src/routes/+layout.ts

import { generateBundles } from '$lib/fluent';
import { createSvelteFluent } from '@nubolab-ffwd/svelte-fluent';

export function load(event) {
	return {
		...event.data,
		fluent: createSvelteFluent(generateBundles(event.data.locale))
	};
}

Now we can access the SvelteFluent object via event.data.fluent and use it to initialize the FluentContext in src/routes/+layout.svelte which is required for using the Localized and Overlay components.

<!-- src/routes/+layout.svelte -->

<script lang="ts">
	import { initFluentContext } from '@nubolab-ffwd/svelte-fluent';
	import type { PageData } from './$types';
	import type { Snippet } from 'svelte';

	let { data, children }: { data: PageData; children: Snippet } = $props();
	initFluentContext(() => data.fluent);
</script>

{@render children()}

Render your first localized message

With all the setup work complete, it’s finally time to render your first localized message in src/routes/+page.svelte:

<!-- src/routes/+page.svelte -->

<script lang="ts">
	import { Localized } from '@nubolab-ffwd/svelte-fluent';
</script>

<h1><Localized id="welcome" /></h1>

Launch the app

Now we have all the pieces in place and can open the app. Run this in a terminal:

npm run dev

Open your browser and go to http://localhost:5173 and you should see the app.

Screenshot of the opened browser window

Bonus: server-side localization

We want to extend our application with a form. The form should validate inputs and generate localized error messages if validation fails.

Let’s start by adding some additional messages to our translation files:

 # src/translations/en.ftl

 welcome = Welcome to svelte-fluent!
+
+example-form =
+  .heading = Example form
+  .name-field-label = Name
+  .submit-label = Submit
+  .name-required-error = Name must not be empty
+  .success-message = Form was submitted successfully!
 # src/translations/de.ftl

 welcome = Willkommen bei svelte-fluent!
+
+example-form =
+  .heading = Beispielformular
+  .name-field-label = Name
+  .submit-label = Absenden
+  .name-required-error = Name darf nicht leer sein
+  .success-message = Formular wurde erfolgreich abgeschickt!

Next we add a form to src/routes/+page.svelte:

 </script>

 <h1><Localized id="welcome" /></h1>
+
+<Localized id="example-form">
+       {#snippet children({ attrs })}
+               <h2>{attrs.heading}</h2>
+               <form method="POST">
+                       <label>
+                               {attrs['name-field-label']}
+                               <input name="name" />
+                       </label>
+                       <button>{attrs['submit-label']}</button>
+               </form>
+       {/snippet}
+</Localized>

Also we need to add a SvelteKit form action to src/routes/+page.server.ts:

import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions = {
	default: async ({ request, locals: { fluent } }) => {
		const data = await request.formData();
		const name = data.get('name');
		if (!name || !name.toString().trim()) {
			// render the localized error message
			const error = fluent.localize('example-form.name-required-error');
			return fail(400, { name, error });
		}
		return { success: true };
	}
} satisfies Actions;

And finally, we need to handle the response from the action in our form in src/routes/+page.svelte:

 <script lang="ts">
        import { Localized } from '@nubolab-ffwd/svelte-fluent';
+       import type { ActionData } from './$types';
+
+       let { form }: { form: ActionData } = $props();
 </script>
 <Localized id="example-form">
        {#snippet children({ attrs })}
                <h2>{attrs.heading}</h2>
+
+               {#if form?.success}
+                       <p>{attrs['success-message']}</p>
+               {/if}
+
                <form method="POST">
+                       {#if form?.error}<p class="error">{form.error}</p>{/if}
                        <label>
                                {attrs['name-field-label']}
-                               <input name="name" />
+                               <input name="name" value={form?.name ?? ''} />
                        </label>
                        <button>{attrs['submit-label']}</button>
                </form>

Now open your browser and go to http://localhost:5173 and you should see the new form that displays localized error messages when you submit it.

Screenshot of the opened browser window

What’s next?

You now have a fully functional application where you can localize messages with svelte-fluent.

You can learn more about how to use svelte-fluent in the Tutorial or check out the Reference for API documentation.