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.
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.
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.