Implementing Internationalization with SvelteKit and Directus
Published May 6th, 2024
Before You Start
You will need:
- Node.js v20.11.1 or later.
- A code editor on your computer.
- A Directus project - follow our quickstart guide if you don't already have one.
- Some knowledge of React and Svelte.
The code for this tutorial is available on this GitHub repository.
Installing SvelteKit and setting up a new project.
Start by setting up a new Svelte project and install the required dependencies including the Directus SDK:
npm create svelte@latest frontend # Select the Skeleton project
cd directus-i18n-app
npm install
npm install @directus/sdk
npm create svelte@latest frontend # Select the Skeleton project
cd directus-i18n-app
npm install
npm install @directus/sdk
In the src/libs
directory, create a directus.js
file to create and export a Directus SDK instance:
import { createDirectus, rest } from '@directus/sdk';
import { PUBLIC_DIRECTUS_API_URL } from '$env/static/public';
function getDirectusInstance(fetch) {
const options = fetch ? { globals: { fetch } } : {};
const directus = createDirectus(PUBLIC_DIRECTUS_API_URL).with(rest());
return directus;
}
export default getDirectusInstance;
import { createDirectus, rest } from '@directus/sdk';
import { PUBLIC_DIRECTUS_API_URL } from '$env/static/public';
function getDirectusInstance(fetch) {
const options = fetch ? { globals: { fetch } } : {};
const directus = createDirectus(PUBLIC_DIRECTUS_API_URL).with(rest());
return directus;
}
export default getDirectusInstance;
Then create a .env
file in the root directory of your project and add your Directus API URL:
PUBLIC_DIRECTUS_API_URL='https://directus.example.com';
PUBLIC_DIRECTUS_API_URL='https://directus.example.com';
Designing the Data Model
In the Directus Data Studio, navigate to Settings -> Data Model and create a new collection called news
:
slug
(Primary Key Field, Type: Manually entered string)author
(Type: String, Interface: Input)cover
(Type: Image)
Create a collection called languages
:
code
(Primary Key Field, Type: Manually entered string )name
(Type: String, Interface: Input)direction
(Type: String, Interface: Dropdown, Options:ltr
andrtl
. Default Value:ltr
)
The direction
field enables support for languages that read right to left.
To enable content translation in your news
collection, create a translations
field using translation interface. Select name
as the Language Indicator Field, direction
as the Language Direction Field and en-US
as the Default Language.
Once you save, a new collection named news_translations
will be created for you. In the news_translations
collection, you will add the fields that need translations.
Add the following fields to the news_translations
collection:
title
(Type: String, Interface: Input)body
(Type: Text, Interface: WYSIWYG)
Add each language you want to support as items in the languages
collection.
The item page for the news
collection now includes a translations interface.
llow the Public role to read the news
, languages
and news_translations
collections in the Access Control settings to ensure the frontend can access these collections.
Building the News App Frontend with SvelteKit
In your Svelte project, update your +page.js
file to fetch your content using the SDK:
import getDirectusInstance from "$lib/directus";
import { readItems } from "@directus/sdk";
export async function load({ fetch }) {
const directus = getDirectusInstance(fetch);
return {
global: await directus.request(readItems("global")),
news: await directus.request(readItems("news", {
deep: {
translations: {
_filter: {
_and: [
{
languages_code: { _eq: "en-US" },
},
],
},
},
},
fields: ["*", { translations: ["*"] }],
})
),
};
}
import getDirectusInstance from "$lib/directus";
import { readItems } from "@directus/sdk";
export async function load({ fetch }) {
const directus = getDirectusInstance(fetch);
return {
global: await directus.request(readItems("global")),
news: await directus.request(readItems("news", {
deep: {
translations: {
_filter: {
_and: [
{
languages_code: { _eq: "en-US" },
},
],
},
},
},
fields: ["*", { translations: ["*"] }],
})
),
};
}
The above code snippet will use:
readItems
function to fetch all the contents in the news collection.deep
parameter to filter the related collection to only show the translations in en-US (English US).
Update the code in +page.svelte
file in the src
directory to render the news:
<script>
export let data;
</script>
<h1>Trending Today!</h1>
<ul>
{#each data.news as article}
<li>
<div>
<h2>
<a href={`/${article.id}`}>
{article.translations[0].title}
</a>
</h2>
<p>By {article.author}</p>
</div>
</li>
{/each}
</ul>
<script>
export let data;
</script>
<h1>Trending Today!</h1>
<ul>
{#each data.news as article}
<li>
<div>
<h2>
<a href={`/${article.id}`}>
{article.translations[0].title}
</a>
</h2>
<p>By {article.author}</p>
</div>
</li>
{/each}
</ul>
The above code will:
- Loop through the news array returned in the
+page.js
file to display the contents. - Attach a link to each news list pointing to the news single page.
Create a news/+page.js
file in the routes
directory for the route that will render the individual news contents:
import { readItem } from "@directus/sdk";
import getDirectusInstance from "$lib/directus";
import { error } from "@sveltejs/kit";
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params, url }) {
const directus = getDirectusInstance(fetch);
const slug = params.slug;
try {
const [newsData, languagesData] = await Promise.all([
directus.request(
readItem("news", slug, {
fields: ["*", { "*": ["*"] }],
})
),
directus.request(readItems("languages")),
]);
return {
article: newsData ? newsData : null,
languages: languagesData,
};
} catch (err) {
error(404, "Post not found");
}
}
import { readItem } from "@directus/sdk";
import getDirectusInstance from "$lib/directus";
import { error } from "@sveltejs/kit";
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params, url }) {
const directus = getDirectusInstance(fetch);
const slug = params.slug;
try {
const [newsData, languagesData] = await Promise.all([
directus.request(
readItem("news", slug, {
fields: ["*", { "*": ["*"] }],
})
),
directus.request(readItems("languages")),
]);
return {
article: newsData ? newsData : null,
languages: languagesData,
};
} catch (err) {
error(404, "Post not found");
}
}
The above code will:
- Use the
readItem
funtion to find and get the news that matches the primary key field (slug) in the news collection. - Fetch all the available languages from the
languages
collection.
Create a +page.svelte
file in the routes/news
directory and add the code:
<script>
export let data;
$: ({ article, languages } = data);
</script>
{#if article}
<h1>{article.translations[0].title}</h1>
{@html article.translations[0].body}
<select>
{#each languages as language}
<option value={language.code}>{language.name}</option>
{/each}
</select>
{:else}
<p>News not found.</p>
{/if}
<script>
export let data;
$: ({ article, languages } = data);
</script>
{#if article}
<h1>{article.translations[0].title}</h1>
{@html article.translations[0].body}
<select>
{#each languages as language}
<option value={language.code}>{language.name}</option>
{/each}
</select>
{:else}
<p>News not found.</p>
{/if}
The above code will:
- Get the languages and selected news article data returned from
news/+page.js
file and render them. - Render the languages in a select field so users can choose the language they need the content to be translated into.
- Use the
@html
decorator to properly render the WYSIWYGbody
field content.
Adding Multilingual Navigation and Search
Update your project to add the multilingual navigation and search functionalities. Update the code in the routes/news/+page.svelte
file to add a handler to dynamically render the article translation based on the selected language.
<script>
import { goto } from '$app/navigation';
export let data;
$: ({ article, languages, languageCode } = data);
let selectedLanguageCode = languageCode;
function handleLanguageChange(event) {
const newLanguageCode = event.target.value;
selectedLanguageCode = newLanguageCode; // Update the selectedLanguageCode
goto(`?lang=${newLanguageCode}`, { replaceState: true });
}
</script>
{#if article}
<h1>{article.translations[0].title}</h1>
{@html article.translations[0].body}
<select value={selectedLanguageCode} on:change={handleLanguageChange}>
{#each languages as language}
{console.log(language)}
<option value={language.code}>{language.name}</option>
{/each}
</select>
{:else}
<p>News not found.</p>
{/if}
<script>
import { goto } from '$app/navigation';
export let data;
$: ({ article, languages, languageCode } = data);
let selectedLanguageCode = languageCode;
function handleLanguageChange(event) {
const newLanguageCode = event.target.value;
selectedLanguageCode = newLanguageCode; // Update the selectedLanguageCode
goto(`?lang=${newLanguageCode}`, { replaceState: true });
}
</script>
{#if article}
<h1>{article.translations[0].title}</h1>
{@html article.translations[0].body}
<select value={selectedLanguageCode} on:change={handleLanguageChange}>
{#each languages as language}
{console.log(language)}
<option value={language.code}>{language.name}</option>
{/each}
</select>
{:else}
<p>News not found.</p>
{/if}
Then, update the code in your routes/news/+page.js
file to add a filter that allows users to dynamically select the language they need the news to be translated by adding a new URL parameter for the desired language code and use it to filter the news translations.
import { readItem } from "@directus/sdk";
import getDirectusInstance from "$lib/directus";
import { error } from "@sveltejs/kit";
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params, url }) {
const directus = getDirectusInstance(fetch);
const slug = params.slug;
const languageCode = url.searchParams.get("lang") || "en-US";
try {
const [newsData, languagesData] = await Promise.all([
directus.request(
readItem("news", slug, {
deep: {
translations: {
_filter: {
_and: [
{ languages_code: { _eq: languageCode } },
],
},
},
},
fields: ["*", { "*": ["*"] }],
})
),
directus.request(readItems("languages")),
]);
return {
article: newsData ? newsData : null,
languages: languagesData,
languageCode,
};
} catch (err) {
error(404, "Post not found");
}
}
import { readItem } from "@directus/sdk";
import getDirectusInstance from "$lib/directus";
import { error } from "@sveltejs/kit";
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params, url }) {
const directus = getDirectusInstance(fetch);
const slug = params.slug;
const languageCode = url.searchParams.get("lang") || "en-US";
try {
const [newsData, languagesData] = await Promise.all([
directus.request(
readItem("news", slug, {
deep: {
translations: {
_filter: {
_and: [
{ languages_code: { _eq: languageCode } },
],
},
},
},
fields: ["*", { "*": ["*"] }],
})
),
directus.request(readItems("languages")),
]);
return {
article: newsData ? newsData : null,
languages: languagesData,
languageCode,
};
} catch (err) {
error(404, "Post not found");
}
}
Now you translate the news in English, German, and French.
Replace the code in your routes/+page.svelte
file with the code snippets below to add search functionality:
<script>
import { goto } from "$app/navigation";
import { page } from "$app/stores";
export let data;
let searchQuery = $page.url.searchParams.get("q") || "";
function handleSearchChange() {
goto(`/?q=${searchQuery}`, { replaceState: true });
}
</script>
<h1>Trending Today!</h1>
<div>
<input type="text" bind:value={searchQuery} placeholder="Search News..." />
<button on:click={handleSearchChange}>Search</button>
</div>
<ul>
{#each data.news as article}
<li>
<div>
<h2>
<a href={`/${article.id}`}>
{article.translations[0].title}
</a>
</h2>
<p>By {article.author}</p>
</div>
</li>
{/each}
</ul>
<script>
import { goto } from "$app/navigation";
import { page } from "$app/stores";
export let data;
let searchQuery = $page.url.searchParams.get("q") || "";
function handleSearchChange() {
goto(`/?q=${searchQuery}`, { replaceState: true });
}
</script>
<h1>Trending Today!</h1>
<div>
<input type="text" bind:value={searchQuery} placeholder="Search News..." />
<button on:click={handleSearchChange}>Search</button>
</div>
<ul>
{#each data.news as article}
<li>
<div>
<h2>
<a href={`/${article.id}`}>
{article.translations[0].title}
</a>
</h2>
<p>By {article.author}</p>
</div>
</li>
{/each}
</ul>
The above code will:
- Define variables
searchQuery
to store the user's search input. - Initialize the
searchQuery
variable with the value of theq
query parameter from the current URL($page.url.searchParams.get("q"))
. If noq
parameter is present, it defaults to an empty string. - Use the
handleSearchChange
to update the URL with the currentsearchQuery
value using thegoto
function from$app/navigation
. ThereplaceState: true
option will replace the current history entry instead of creating a new one. - Render an input field and a button to allow the user to enter a search query and trigger the search.
- Display the searched news or all the news if no search is made.
Summary
Throughout this tutorial, you've learned how to build a multilingual news application using SvelteKit and Directus. You have set up a SvelteKit project, created a Directus Wrapper, and used it to query data. We created translation collections using Directus's flexible CMS and used the translation interface to translate the news article content into different languages.