feat: implement internationalization with locale management and configuration from url
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -1,70 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { locale, isLoading } from "svelte-i18n";
|
||||
import { browser } from "$app/environment";
|
||||
import type { LocaleConfig } from "$lib/i18n/types";
|
||||
import { fade } from "svelte/transition";
|
||||
import { page } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import { config } from "$lib/i18n";
|
||||
|
||||
const supportedLocales: LocaleConfig[] = [
|
||||
{ code: "en", label: "English" },
|
||||
{ code: "es", label: "Español" }
|
||||
];
|
||||
const localeLabels: Record<string, string> = {
|
||||
en: "English",
|
||||
es: "Español",
|
||||
// ...add other locale labels
|
||||
};
|
||||
|
||||
async function setLocale(code: LocaleConfig['code']) {
|
||||
try {
|
||||
await locale.set(code);
|
||||
if (browser) {
|
||||
localStorage.setItem("locale", code);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set locale:', error);
|
||||
$: currentLocale = $page.params.locale || config.defaultLocale;
|
||||
|
||||
function getLocalePath(newLocale: string): string {
|
||||
const currentPath = $page.url.pathname;
|
||||
const segments = currentPath.split("/").filter(Boolean);
|
||||
|
||||
if (newLocale === config.defaultLocale) {
|
||||
return segments.length > 1 ? `/${segments.slice(1).join("/")}` : "/";
|
||||
}
|
||||
|
||||
return currentLocale === config.defaultLocale
|
||||
? `/${newLocale}${currentPath}`
|
||||
: currentPath.replace(`/${currentLocale}`, `/${newLocale}`);
|
||||
}
|
||||
|
||||
async function switchLocale(newLocale: string) {
|
||||
const newPath = getLocalePath(newLocale);
|
||||
await goto(newPath, { invalidateAll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="locale-switcher" role="group" aria-label="Language selector">
|
||||
{#each supportedLocales as { code, label }}
|
||||
<button
|
||||
class="locale-button"
|
||||
class:active={$locale === code}
|
||||
on:click={() => setLocale(code)}
|
||||
aria-label={`Switch to ${label}`}
|
||||
disabled={$isLoading}
|
||||
aria-current={$locale === code}
|
||||
>
|
||||
{#if $isLoading && $locale === code}
|
||||
<span class="loading">...</span>
|
||||
{/if}
|
||||
{label}
|
||||
</button>
|
||||
{/each}
|
||||
<div class="relative" role="group" aria-label="Language selector">
|
||||
<select
|
||||
class="appearance-none w-full py-2 px-4 pr-8 border border-gray-300 rounded-md bg-white cursor-pointer text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
value={currentLocale}
|
||||
on:change={(e) => switchLocale(e.currentTarget.value)}
|
||||
aria-label="Select language"
|
||||
transition:fade
|
||||
>
|
||||
{#each config.availableLocales as locale}
|
||||
<option value={locale}>
|
||||
{localeLabels[locale] || locale.toUpperCase()}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.locale-switcher {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.locale-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.locale-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.locale-button.active {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
+27
-5
@@ -1,10 +1,32 @@
|
||||
import { locale } from "svelte-i18n";
|
||||
import { sequence } from "@sveltejs/kit/hooks";
|
||||
import { init, locale } from "svelte-i18n";
|
||||
import type { Handle } from "@sveltejs/kit";
|
||||
import { config, hasTranslations, isValidLocale } from "$lib/i18n";
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const lang = event.request.headers.get("accept-language")?.split(",")[0];
|
||||
if (lang) {
|
||||
locale.set(lang);
|
||||
const i18n: Handle = async ({ event, resolve }) => {
|
||||
const urlLocale = event.params.locale;
|
||||
const acceptLang = event.request.headers.get("accept-language")?.split(",")[0]?.split("-")[0];
|
||||
|
||||
let resolvedLocale = config.defaultLocale;
|
||||
|
||||
if (urlLocale && isValidLocale(urlLocale) && hasTranslations(urlLocale)) {
|
||||
resolvedLocale = urlLocale;
|
||||
} else if (acceptLang && isValidLocale(acceptLang) && hasTranslations(acceptLang)) {
|
||||
resolvedLocale = acceptLang;
|
||||
}
|
||||
|
||||
// Initialize i18n for SSR if not already initialized
|
||||
try {
|
||||
locale.set(resolvedLocale);
|
||||
} catch {
|
||||
init({
|
||||
fallbackLocale: config.defaultLocale,
|
||||
initialLocale: resolvedLocale,
|
||||
});
|
||||
}
|
||||
|
||||
event.locals.locale = resolvedLocale;
|
||||
return resolve(event);
|
||||
};
|
||||
|
||||
export const handle = sequence(i18n);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { LocaleConfig, SupportedLocale } from "./types";
|
||||
|
||||
export const config: LocaleConfig = {
|
||||
supportedLocales: ["en", "es", "ca", "fr", "de"],
|
||||
defaultLocale: "en",
|
||||
availableLocales: ["en", "es"], // locales with actual translations
|
||||
};
|
||||
|
||||
export function isValidLocale(locale: string): locale is SupportedLocale {
|
||||
return config.supportedLocales.includes(locale as SupportedLocale);
|
||||
}
|
||||
|
||||
export function hasTranslations(locale: SupportedLocale): boolean {
|
||||
return config.availableLocales.includes(locale);
|
||||
}
|
||||
+31
-36
@@ -1,42 +1,37 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { init, register } from "svelte-i18n";
|
||||
import { init, register, getLocaleFromNavigator } from "svelte-i18n";
|
||||
import { config, hasTranslations } from "./config";
|
||||
import type { SupportedLocale } from "./types";
|
||||
|
||||
const defaultLocale: SupportedLocale = "es";
|
||||
const supportedLocales: SupportedLocale[] = ["en", "es"];
|
||||
let initializationPromise: Promise<void> | null = null;
|
||||
|
||||
register("en", () => import("./locales/en.json").catch(() => ({})));
|
||||
register("es", () => import("./locales/es.json").catch(() => ({})));
|
||||
|
||||
/**
|
||||
* Determines the initial locale based on the following priority:
|
||||
* 1. Previously saved locale in localStorage
|
||||
* 2. Browser's language
|
||||
* 3. Default locale
|
||||
*/
|
||||
const getInitialLocale = (): SupportedLocale => {
|
||||
if (!browser) return defaultLocale;
|
||||
|
||||
try {
|
||||
const savedLocale = localStorage.getItem("locale") as SupportedLocale;
|
||||
if (savedLocale && supportedLocales.includes(savedLocale)) {
|
||||
return savedLocale;
|
||||
}
|
||||
|
||||
const browserLocale = window.navigator.language.split(
|
||||
"-",
|
||||
)[0] as SupportedLocale;
|
||||
return supportedLocales.includes(browserLocale)
|
||||
? browserLocale
|
||||
: defaultLocale;
|
||||
} catch {
|
||||
return defaultLocale;
|
||||
}
|
||||
};
|
||||
|
||||
init({
|
||||
fallbackLocale: defaultLocale,
|
||||
initialLocale: getInitialLocale(),
|
||||
// Pre-register all available locales
|
||||
config.availableLocales.forEach((locale) => {
|
||||
register(locale, () => import(`./locales/${locale}.json`).then((module) => module.default));
|
||||
});
|
||||
|
||||
export { supportedLocales, defaultLocale };
|
||||
export function getBrowserLocale(): SupportedLocale | null {
|
||||
if (!browser) return null;
|
||||
const navigatorLocale = getLocaleFromNavigator()?.split("-")[0] as SupportedLocale;
|
||||
return hasTranslations(navigatorLocale) ? navigatorLocale : null;
|
||||
}
|
||||
|
||||
export async function initI18n() {
|
||||
if (initializationPromise) return initializationPromise;
|
||||
|
||||
initializationPromise = (async () => {
|
||||
const initLocale = browser ? getBrowserLocale() || config.defaultLocale : config.defaultLocale;
|
||||
|
||||
init({
|
||||
fallbackLocale: config.defaultLocale,
|
||||
initialLocale: initLocale,
|
||||
});
|
||||
})();
|
||||
|
||||
return initializationPromise;
|
||||
}
|
||||
|
||||
// Remove loadLocaleData as it's no longer needed
|
||||
export * from "./config";
|
||||
export { config, isValidLocale, hasTranslations } from "./config";
|
||||
export type { LocaleConfig, SupportedLocale } from "./types";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export const locale = writable("en");
|
||||
@@ -1,6 +1,7 @@
|
||||
export type SupportedLocale = "en" | "es";
|
||||
export type SupportedLocale = "en" | "es" | "ca" | "fr" | "de";
|
||||
|
||||
export interface LocaleConfig {
|
||||
code: SupportedLocale;
|
||||
label: string;
|
||||
supportedLocales: SupportedLocale[];
|
||||
defaultLocale: SupportedLocale;
|
||||
availableLocales: SupportedLocale[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { isValidLocale } from "$lib/i18n";
|
||||
|
||||
export const match = (param: string) => {
|
||||
return isValidLocale(param);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { isLoading } from "svelte-i18n";
|
||||
import "../app.css";
|
||||
</script>
|
||||
|
||||
{#if $isLoading}
|
||||
<div class="loading">Loading...</div>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
+2
-15
@@ -1,20 +1,7 @@
|
||||
import { locale, waitLocale } from "svelte-i18n";
|
||||
import { browser } from "$app/environment";
|
||||
import "$lib/i18n";
|
||||
import type { LayoutLoad } from "./$types";
|
||||
import { defaultLocale } from "$lib/i18n";
|
||||
import { initI18n } from "$lib/i18n";
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
try {
|
||||
if (browser) {
|
||||
await locale.set(window.navigator.language);
|
||||
}
|
||||
await waitLocale();
|
||||
} catch (error) {
|
||||
console.error("Failed to load locale:", error);
|
||||
if (browser) {
|
||||
await locale.set(defaultLocale);
|
||||
}
|
||||
}
|
||||
await initI18n();
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import LocaleSwitcher from "../components/LocaleSwitcher.svelte";
|
||||
</script>
|
||||
|
||||
<h1 class="text-3xl font-bold underline">{$_("greeting")}</h1>
|
||||
<p>{$_("farewell")}</p>
|
||||
<LocaleSwitcher />
|
||||
|
||||
<style>
|
||||
@reference "tailwindcss/theme";
|
||||
:global(html) {
|
||||
background-color: theme(--color-gray-100);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { isLoading, locale } from "svelte-i18n";
|
||||
import { page } from "$app/stores";
|
||||
import "../../app.css";
|
||||
|
||||
$: {
|
||||
const currentLocale = $page.data.locale;
|
||||
if (currentLocale) {
|
||||
locale.set(currentLocale);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $isLoading}
|
||||
<div class="grid place-items-center h-screen">Loading...</div>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import type { LayoutLoad } from "./$types";
|
||||
import { getBrowserLocale, hasTranslations, isValidLocale } from "$lib/i18n";
|
||||
import { config } from "$lib/i18n/config";
|
||||
|
||||
export const load: LayoutLoad = async ({ params, url, parent }) => {
|
||||
const parentData = await parent();
|
||||
const urlLocale = params.locale;
|
||||
|
||||
if (!urlLocale) {
|
||||
const browserLocale = getBrowserLocale();
|
||||
const locale = browserLocale || config.defaultLocale;
|
||||
return { locale, ...parentData };
|
||||
}
|
||||
|
||||
if (!isValidLocale(urlLocale) || !hasTranslations(urlLocale)) {
|
||||
const redirectUrl = url.pathname.replace(`/${urlLocale}`, "");
|
||||
throw redirect(301, redirectUrl);
|
||||
}
|
||||
|
||||
return { locale: urlLocale, ...parentData };
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import LocaleSwitcher from "../../components/LocaleSwitcher.svelte";
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<div class="p-4">
|
||||
<LocaleSwitcher />
|
||||
<h1 class="text-3xl font-bold underline mt-4">{$_("greeting")}</h1>
|
||||
<p class="mt-2">{$_("farewell")}</p>
|
||||
<p class="mt-2">Current locale: {data.locale}</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@reference "tailwindcss/theme";
|
||||
:global(html) {
|
||||
background-color: theme(--color-gray-100);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user