Skip to content

Commit

Permalink
Merge pull request #122 from sveltekit-i18n/example/locale-router-adv…
Browse files Browse the repository at this point in the history
…anced

Add SEO friendly `locale-router` example
  • Loading branch information
jarda-svoboda authored Jul 10, 2023
2 parents d964db5 + bb1ddfb commit 689c34a
Show file tree
Hide file tree
Showing 35 changed files with 860 additions and 2 deletions.
8 changes: 8 additions & 0 deletions examples/locale-router-advanced/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
node_modules
build
.svelte-kit
package
.env
.env.*
!.env.example
15 changes: 15 additions & 0 deletions examples/locale-router-advanced/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[![Netlify Status](https://api.netlify.com/api/v1/badges/bcefda87-9dad-4c73-bf5f-d9b4c03cac9c/deploy-status)](https://locale-router-advanced.netlify.app)

# Locale-router-advanced
This app shows how to integrate locale routing using dynamic adapters (e.g. `@sveltejs/adapter-node`). It includes two pages and three language mutations (`en`, `de`, `cs`). Error pages are included as well. The default language (`en`) has no lang prefix in URL path.

## Preview
You can view this demo live on [Netlify](https://locale-router-advanced.netlify.app).

## Noticeable files

### `./src/hooks.server.js`
Takes care about redirects to appropriate language mutation. It fetches pages with the default language mutation on background, and serves it to the client with no lang prefix in the URL path.

### `./src/params/locale.js` (`./src/routes/[...lang=locale]`)
Allows to pass only pages starting with locale or having no `path` (dafault lang index).
3 changes: 3 additions & 0 deletions examples/locale-router-advanced/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "./.svelte-kit/tsconfig.json"
}
5 changes: 5 additions & 0 deletions examples/locale-router-advanced/netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build]
command = "cd ../../ && npm run build && npm run build --workspace locale-router-advanced"
publish = "build"
[functions]
node_bundler = "esbuild"
21 changes: 21 additions & 0 deletions examples/locale-router-advanced/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "locale-router-advanced",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"package": "svelte-kit package",
"preview": "vite preview",
"prepare": "svelte-kit sync"
},
"devDependencies": {
"@sveltejs/adapter-netlify": "^2.0.7",
"@sveltejs/kit": "^1.22.1",
"svelte": "^4.0.5",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"sveltekit-i18n": "file:../../"
}
}
10 changes: 10 additions & 0 deletions examples/locale-router-advanced/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// <reference types="@sveltejs/kit" />

// See https://kit.svelte.dev/docs/types#the-app-namespace
// for information about these interfaces
declare namespace App {
// interface Locals {}
// interface Platform {}
// interface Session {}
// interface Stuff {}
}
16 changes: 16 additions & 0 deletions examples/locale-router-advanced/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="%lang%">

<head>
<meta charset="utf-8" />
<meta name="description" content="" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>

<body>
<div>%sveltekit.body%</div>
</body>

</html>
84 changes: 84 additions & 0 deletions examples/locale-router-advanced/src/hooks.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { defaultLocale, loadTranslations, locales } from '$lib/translations';

const routeRegex = new RegExp(/^\/[^.]*([?#].*)?$/);

/** @type {import('@sveltejs/kit').Handle} */
export const handle = async ({ event, resolve }) => {
const { url, request, isDataRequest } = event;
const { pathname, origin } = url;

// If this request is a route request
if (routeRegex.test(pathname)) {

// Get defined locales
const supportedLocales = locales.get();

// Try to get locale from `pathname`.
let locale = supportedLocales.find((l) => l === `${pathname.match(/[^/]+?(?=\/|$)/)}`.toLowerCase());
// We want to redirect the default locale to "no-locale" path
if (locale === defaultLocale && !request.headers.get('prevent-redirect')) {
const localeRegex = new RegExp(`^/${locale}`);
const location = `${pathname}`.replace(localeRegex, '') || '/';

return new Response(undefined, { headers: { location }, status: 301 });

// If route locale is not supported
} else if (!locale) {
// Get user preferred locale if it's a direct navigation
if (!isDataRequest) {
locale = `${`${request.headers.get('accept-language')}`.match(/[a-zA-Z]+?(?=-|_|,|;)/)}`.toLowerCase();
}

// Set default locale if user preferred locale does not match
if (!supportedLocales.includes(locale)) locale = defaultLocale;

if (locale === defaultLocale) {

const path = `${pathname}`.replace(/\/$/, '');
const redirectTo = `${origin}/${locale}${path}${isDataRequest ? '/__data.json?x-sveltekit-invalidated=100' : ''}`;

// We want to prevent redirect to fetch data for the default locale
request.headers.set('prevent-redirect', '1');

// Fetch the redirected route
const response = await fetch(redirectTo, request);

// Get response body and set html headers
const data = await response.text();

// Serve the redirected route.
// In this case we don't have to set the html 'lang' attribute
// as the default locale is already included in our app.html.
return new Response(data, {
...response,
headers: {
...response.headers,
'Content-Type': isDataRequest ? 'application/json' : 'text/html',
},
});

}

// 301 redirect
return new Response(undefined, { headers: { 'location': `/${locale}${pathname}` }, status: 301 });
}

// Add html `lang` attribute
return resolve({ ...event, locals: { lang: locale } }, {
transformPageChunk: ({ html }) => html.replace('%lang%', `${locale}`),
});
}

return resolve(event);
};


/** @type {import('@sveltejs/kit').HandleServerError} */
export const handleError = async ({ event }) => {
const { locals } = event;
const { lang } = locals;

await loadTranslations(lang, 'error');

return locals;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "O této aplikaci",
"text": "<p>Toto je <a href=\"https://kit.svelte.dev\">SvelteKit</a> aplikace. Můžete si vytvořit svou vlastní vložením následujícího příkazu do příkazové řádky:</p><pre>npm init svelte@next</pre> <p> Tato stránka je čistě statické HTML, bez nutnosti klientské interakce. Díky tomu není potřeba načítat žádný JavaScript. Zkuste zobrazit drojový kód stránky, nebo otevřete vývojářské nástroje a znovu načtěte stránku.</p>"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"shit.happens": "No toto...",
"404": "Stránka nenalezena.",
"500": "Interní chyba serveru.",
"default": "Něco se pokazilo."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "Vítejte ve SvelteKit",
"text": "Dokumentace je k přečtení na <a href=\"{{link}}\">kit.svelte.dev</a>"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"home": "Domů",
"about": "O nás",
"notification": "{{count:gt; 0:Máte {{count}} {{count:gte; 1:novou zprávu; 2:nové zprávy; 5:nových zpráv}}!; default:Nemáte žádné zprávy...}}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "Über diese App",
"text": "<p>Dies ist eine <a href=\"https://kit.svelte.dev\">SvelteKit</a>-App. Sie können Ihre eigene erstellen, indem Sie Folgendes in Ihre Befehlszeile eingeben und den Eingabeaufforderungen folgen:</p><pre>npm init svelte@next</pre> <p> Die Seite, die Sie sich ansehen, ist rein statisches HTML mit keine clientseitige Interaktivität erforderlich. Aus diesem Grund müssen wir kein JavaScript laden. Versuchen Sie, die Quelle der Seite anzuzeigen oder das devtools-Netzwerkfenster zu öffnen und neu zu laden.</p>"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"shit.happens": "Auweh...",
"404": "Seite nicht gefunden.",
"500": "Server interner Fehler.",
"default": "Ein Fehler ist aufgetreten."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "Willkommen bei SvelteKit",
"text": "Besuchen Sie <a href=\"{{link}}\">kit.svelte.dev</a>, um die Dokumentation zu lesen"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"home": "Startseite",
"about": "Über uns",
"notification": "Sie haben {{count:gt; 0:{{count}} neue {{count; 1:Nachricht; default:Nachrichten}}!; default:keine Nachrichten...}}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "About this app",
"text": "<p>This is a <a href=\"https://kit.svelte.dev\">SvelteKit</a> app. You can make your own by typing the following into your command line and following the prompts:</p><pre>npm init svelte@next</pre> <p> The page you're looking at is purely static HTML, with no client-side interactivity needed. Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening the devtools network panel and reloading. </p>"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"shit.happens": "Oh, dear...",
"404": "Page not found.",
"500": "Server internal error.",
"default": "Some error occurred."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "Welcome to SvelteKit",
"text": "Visit <a href=\"{{link}}\">kit.svelte.dev</a> to read the documentation"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"home": "Home",
"about": "About",
"notification": "You have {{count:gt; 0:{{count}} new {{count; 1:message; default:messages}}!; default:no messages...}}"
}
96 changes: 96 additions & 0 deletions examples/locale-router-advanced/src/lib/translations/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import i18n from 'sveltekit-i18n';
import lang from './lang.json';

/** @type {import('sveltekit-i18n').Config} */
const config = {
translations: {
en: { lang },
de: { lang },
cs: { lang },
},
loaders: [
{
locale: 'en',
key: 'menu',
loader: async () => (await import('./en/menu.json')).default,
},
{
locale: 'en',
key: 'home',
routes: ['', '/'],
loader: async () => (await import('./en/home.json')).default,
},
{
locale: 'en',
key: 'about',
routes: ['/about'],
loader: async () => (await import('./en/about.json')).default,
},
{
locale: 'de',
key: 'menu',
loader: async () => (await import('./de/menu.json')).default,
},
{
locale: 'en',
key: 'error',
routes: ['error'],
loader: async () => (await import('./en/error.json')).default,
},
{
locale: 'de',
key: 'home',
routes: ['', '/'],
loader: async () => (await import('./de/home.json')).default,
},
{
locale: 'de',
key: 'about',
routes: ['/about'],
loader: async () => (await import('./de/about.json')).default,
},
{
locale: 'cs',
key: 'menu',
loader: async () => (await import('./cs/menu.json')).default,
},
{
locale: 'de',
key: 'error',
routes: ['error'],
loader: async () => (await import('./de/error.json')).default,
},
{
locale: 'cs',
key: 'home',
routes: ['', '/'],
loader: async () => (await import('./cs/home.json')).default,
},
{
locale: 'cs',
key: 'about',
routes: ['/about'],
loader: async () => (await import('./cs/about.json')).default,
},
{
locale: 'cs',
key: 'error',
routes: ['error'],
loader: async () => (await import('./cs/error.json')).default,
},
],
};

export const defaultLocale = 'en';

export const { t, locale, locales, loading, addTranslations, loadTranslations, translations, setRoute, setLocale } = new i18n(config);

// Translations logs
loading.subscribe(async ($loading) => {
if ($loading) {
console.log('Loading translations...');

await loading.toPromise();
console.log('Updated translations', translations.get());
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"en": "English",
"de": "Deutsch",
"cs": "Česky"
}
10 changes: 10 additions & 0 deletions examples/locale-router-advanced/src/params/locale.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { locales } from '$lib/translations';

/** @type {import('@sveltejs/kit').ParamMatcher} */
export function match(param) {
const definedLocales = locales.get();
const paths = [...definedLocales, ''];
const slashPaths = paths.map((l) => `${l}/`);

return [...paths, ...slashPaths].includes(param);
}
14 changes: 14 additions & 0 deletions examples/locale-router-advanced/src/routes/+error.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
import { page } from '$app/stores';
import { t, locale } from '$lib/translations';
const { status } = $page;
</script>

<div class="content">
<h1>{$t('error.shit.happens')} ({status})</h1>
<p>{$t(`error.${status}`, { default: $t('error.default') })}</p>
<br>
<br>
{$locale} – {$t(`lang.${$locale}`)}
</div>
Loading

0 comments on commit 689c34a

Please sign in to comment.