diff --git a/examples/locale-router-advanced/.gitignore b/examples/locale-router-advanced/.gitignore new file mode 100644 index 0000000..2194f15 --- /dev/null +++ b/examples/locale-router-advanced/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +node_modules +build +.svelte-kit +package +.env +.env.* +!.env.example diff --git a/examples/locale-router-advanced/.npmrc b/examples/locale-router-advanced/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/examples/locale-router-advanced/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/examples/locale-router-advanced/README.md b/examples/locale-router-advanced/README.md new file mode 100644 index 0000000..96f3777 --- /dev/null +++ b/examples/locale-router-advanced/README.md @@ -0,0 +1,7 @@ +# Locale-loader +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. + +## Setup + +### `./src/hooks.server.js` +Takes care about redirects to appropriate language mutation. diff --git a/examples/locale-router-advanced/jsconfig.json b/examples/locale-router-advanced/jsconfig.json new file mode 100644 index 0000000..0a13bbe --- /dev/null +++ b/examples/locale-router-advanced/jsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.svelte-kit/tsconfig.json" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/package.json b/examples/locale-router-advanced/package.json new file mode 100644 index 0000000..198c170 --- /dev/null +++ b/examples/locale-router-advanced/package.json @@ -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-auto": "^2.1.0", + "@sveltejs/kit": "^1.22.1", + "svelte": "^4.0.5", + "vite": "^4.4.2" + }, + "type": "module", + "dependencies": { + "sveltekit-i18n": "file:../../" + } +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/app.d.ts b/examples/locale-router-advanced/src/app.d.ts new file mode 100644 index 0000000..807ac95 --- /dev/null +++ b/examples/locale-router-advanced/src/app.d.ts @@ -0,0 +1,10 @@ +/// + +// 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 {} +} diff --git a/examples/locale-router-advanced/src/app.html b/examples/locale-router-advanced/src/app.html new file mode 100644 index 0000000..f12d610 --- /dev/null +++ b/examples/locale-router-advanced/src/app.html @@ -0,0 +1,16 @@ + + + + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + + \ No newline at end of file diff --git a/examples/locale-router-advanced/src/hooks.server.js b/examples/locale-router-advanced/src/hooks.server.js new file mode 100644 index 0000000..225e092 --- /dev/null +++ b/examples/locale-router-advanced/src/hooks.server.js @@ -0,0 +1,81 @@ +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('no-redirect')) { + const localeRegex = new RegExp(`^/${locale}`); + const location = `${pathname}`.replace(localeRegex, '') || '/'; + + return new Response(undefined, { headers: new Headers({ location }), status: 301 }); + + // If route locale is not supported + } else if (!locale) { + // Get user preferred locale + 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' : ''}`; + + request.headers.set('no-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: 302 }); + } + + // Add html `lang` attribute + return resolve({ ...event, locals: { lang: locale } }, { + transformPageChunk: ({ html }) => html.replace(//, ``), + }); + } + + 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; +}; diff --git a/examples/locale-router-advanced/src/lib/translations/cs/about.json b/examples/locale-router-advanced/src/lib/translations/cs/about.json new file mode 100644 index 0000000..8e6ae9b --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/cs/about.json @@ -0,0 +1,4 @@ +{ + "title": "O této aplikaci", + "text": "

Toto je SvelteKit aplikace. Můžete si vytvořit svou vlastní vložením následujícího příkazu do příkazové řádky:

npm init svelte@next

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.

" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/cs/error.json b/examples/locale-router-advanced/src/lib/translations/cs/error.json new file mode 100644 index 0000000..91240d9 --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/cs/error.json @@ -0,0 +1,6 @@ +{ + "shit.happens": "No toto...", + "404": "Stránka nenalezena.", + "500": "Interní chyba serveru.", + "default": "Něco se pokazilo." +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/cs/home.json b/examples/locale-router-advanced/src/lib/translations/cs/home.json new file mode 100644 index 0000000..d234856 --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/cs/home.json @@ -0,0 +1,4 @@ +{ + "title": "Vítejte ve SvelteKit", + "text": "Dokumentace je k přečtení na kit.svelte.dev" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/cs/menu.json b/examples/locale-router-advanced/src/lib/translations/cs/menu.json new file mode 100644 index 0000000..327a19c --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/cs/menu.json @@ -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...}}" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/de/about.json b/examples/locale-router-advanced/src/lib/translations/de/about.json new file mode 100644 index 0000000..ee1f762 --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/de/about.json @@ -0,0 +1,4 @@ +{ + "title": "Über diese App", + "text": "

Dies ist eine SvelteKit-App. Sie können Ihre eigene erstellen, indem Sie Folgendes in Ihre Befehlszeile eingeben und den Eingabeaufforderungen folgen:

npm init svelte@next

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.

" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/de/error.json b/examples/locale-router-advanced/src/lib/translations/de/error.json new file mode 100644 index 0000000..b665edd --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/de/error.json @@ -0,0 +1,6 @@ +{ + "shit.happens": "Auweh...", + "404": "Seite nicht gefunden.", + "500": "Server interner Fehler.", + "default": "Ein Fehler ist aufgetreten." +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/de/home.json b/examples/locale-router-advanced/src/lib/translations/de/home.json new file mode 100644 index 0000000..e4767ba --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/de/home.json @@ -0,0 +1,4 @@ +{ + "title": "Willkommen bei SvelteKit", + "text": "Besuchen Sie kit.svelte.dev, um die Dokumentation zu lesen" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/de/menu.json b/examples/locale-router-advanced/src/lib/translations/de/menu.json new file mode 100644 index 0000000..9cc8fa3 --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/de/menu.json @@ -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...}}" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/en/about.json b/examples/locale-router-advanced/src/lib/translations/en/about.json new file mode 100644 index 0000000..f4f55a9 --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/en/about.json @@ -0,0 +1,4 @@ +{ + "title": "About this app", + "text": "

This is a SvelteKit app. You can make your own by typing the following into your command line and following the prompts:

npm init svelte@next

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.

" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/en/error.json b/examples/locale-router-advanced/src/lib/translations/en/error.json new file mode 100644 index 0000000..873a298 --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/en/error.json @@ -0,0 +1,6 @@ +{ + "shit.happens": "Oh, dear...", + "404": "Page not found.", + "500": "Server internal error.", + "default": "Some error occurred." +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/en/home.json b/examples/locale-router-advanced/src/lib/translations/en/home.json new file mode 100644 index 0000000..f1f65f6 --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/en/home.json @@ -0,0 +1,4 @@ +{ + "title": "Welcome to SvelteKit", + "text": "Visit kit.svelte.dev to read the documentation" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/en/menu.json b/examples/locale-router-advanced/src/lib/translations/en/menu.json new file mode 100644 index 0000000..f66a63c --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/en/menu.json @@ -0,0 +1,5 @@ +{ + "home": "Home", + "about": "About", + "notification": "You have {{count:gt; 0:{{count}} new {{count; 1:message; default:messages}}!; default:no messages...}}" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/index.js b/examples/locale-router-advanced/src/lib/translations/index.js new file mode 100644 index 0000000..23001aa --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/index.js @@ -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 = 'cs'; + +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()); + } +}); \ No newline at end of file diff --git a/examples/locale-router-advanced/src/lib/translations/lang.json b/examples/locale-router-advanced/src/lib/translations/lang.json new file mode 100644 index 0000000..7e9974a --- /dev/null +++ b/examples/locale-router-advanced/src/lib/translations/lang.json @@ -0,0 +1,5 @@ +{ + "en": "English", + "de": "Deutsch", + "cs": "Česky" +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/params/locale.js b/examples/locale-router-advanced/src/params/locale.js new file mode 100644 index 0000000..44a8e9d --- /dev/null +++ b/examples/locale-router-advanced/src/params/locale.js @@ -0,0 +1,6 @@ +import { locales } from '$lib/translations'; + +/** @type {import('@sveltejs/kit').ParamMatcher} */ +export function match(param) { + return locales.get().includes(param); +} \ No newline at end of file diff --git a/examples/locale-router-advanced/src/routes/+error.svelte b/examples/locale-router-advanced/src/routes/+error.svelte new file mode 100644 index 0000000..12d9269 --- /dev/null +++ b/examples/locale-router-advanced/src/routes/+error.svelte @@ -0,0 +1,14 @@ + + +
+

{$t('error.shit.happens')} ({status})

+

{$t(`error.${status}`, { default: $t('error.default') })}

+
+
+ {$locale} – {$t(`lang.${$locale}`)} +
\ No newline at end of file diff --git a/examples/locale-router-advanced/src/routes/+layout.js b/examples/locale-router-advanced/src/routes/+layout.js new file mode 100644 index 0000000..a1113db --- /dev/null +++ b/examples/locale-router-advanced/src/routes/+layout.js @@ -0,0 +1,14 @@ +import { addTranslations, setLocale, setRoute } from '$lib/translations'; + +/** @type {import('@sveltejs/kit').LayoutLoad} */ +export const load = async ({ data }) => { + const { i18n, translations } = data; + const { lang, route } = i18n; + + addTranslations(translations); + + await setRoute(route); + await setLocale(lang); + + return i18n; +}; \ No newline at end of file diff --git a/examples/locale-router-advanced/src/routes/+layout.server.js b/examples/locale-router-advanced/src/routes/+layout.server.js new file mode 100644 index 0000000..8653187 --- /dev/null +++ b/examples/locale-router-advanced/src/routes/+layout.server.js @@ -0,0 +1,13 @@ +import { loadTranslations, translations } from '$lib/translations'; + +/** @type {import('@sveltejs/kit').ServerLoad} */ +export const load = async ({ url, locals }) => { + const { pathname } = url; + const { lang } = locals; + + const route = pathname.replace(new RegExp(`^/${lang}`), ''); + + await loadTranslations(lang, route); + + return { i18n: { route, lang }, translations: translations.get() }; +}; \ No newline at end of file diff --git a/examples/locale-router-advanced/src/routes/[lang=locale]/+layout.svelte b/examples/locale-router-advanced/src/routes/[lang=locale]/+layout.svelte new file mode 100644 index 0000000..3b8de44 --- /dev/null +++ b/examples/locale-router-advanced/src/routes/[lang=locale]/+layout.svelte @@ -0,0 +1,29 @@ + + +{$t('menu.home')} +{$t('menu.about')} +
+
+{$t('menu.notification', { count: $count })}
+ + +
+ +
+
+
+
+ \ No newline at end of file diff --git a/examples/locale-router-advanced/src/routes/[lang=locale]/+page.svelte b/examples/locale-router-advanced/src/routes/[lang=locale]/+page.svelte new file mode 100644 index 0000000..a6f3837 --- /dev/null +++ b/examples/locale-router-advanced/src/routes/[lang=locale]/+page.svelte @@ -0,0 +1,8 @@ + + +

{$t('home.title')}

+

{@html $t('home.text', { link })}

diff --git a/examples/locale-router-advanced/src/routes/[lang=locale]/about/+page.svelte b/examples/locale-router-advanced/src/routes/[lang=locale]/about/+page.svelte new file mode 100644 index 0000000..ee40d43 --- /dev/null +++ b/examples/locale-router-advanced/src/routes/[lang=locale]/about/+page.svelte @@ -0,0 +1,6 @@ + + +

{$t('about.title')}

+

{@html $t('about.text')}

diff --git a/examples/locale-router-advanced/static/favicon.png b/examples/locale-router-advanced/static/favicon.png new file mode 100644 index 0000000..825b9e6 Binary files /dev/null and b/examples/locale-router-advanced/static/favicon.png differ diff --git a/examples/locale-router-advanced/svelte.config.js b/examples/locale-router-advanced/svelte.config.js new file mode 100644 index 0000000..ef8f370 --- /dev/null +++ b/examples/locale-router-advanced/svelte.config.js @@ -0,0 +1,11 @@ +/* eslint-disable */ +import adapter from '@sveltejs/adapter-auto'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter(), + }, +}; + +export default config; diff --git a/examples/locale-router-advanced/vite.config.js b/examples/locale-router-advanced/vite.config.js new file mode 100644 index 0000000..9a23612 --- /dev/null +++ b/examples/locale-router-advanced/vite.config.js @@ -0,0 +1,8 @@ +import { sveltekit } from '@sveltejs/kit/vite'; + +/** @type {import('vite').UserConfig} */ +const config = { + plugins: [sveltekit()], +}; + +export default config; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 52d345f..7c58748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,18 @@ "vite": "^4.4.2" } }, + "examples/locale-router-advanced": { + "version": "0.0.1", + "dependencies": { + "sveltekit-i18n": "file:../../" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^2.1.0", + "@sveltejs/kit": "^1.22.1", + "svelte": "^4.0.5", + "vite": "^4.4.2" + } + }, "examples/locale-router-static": { "version": "0.0.1", "dependencies": { @@ -5364,6 +5376,10 @@ "resolved": "examples/locale-router", "link": true }, + "node_modules/locale-router-advanced": { + "resolved": "examples/locale-router-advanced", + "link": true + }, "node_modules/locale-router-static": { "resolved": "examples/locale-router-static", "link": true