From 148ff608c36bb0c00ce3cf3c30fc10b538acc5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Fri, 12 Jan 2024 10:53:15 +0100 Subject: [PATCH 1/6] Add basic i18n support --- lxl-web/src/app.d.ts | 5 +- lxl-web/src/lib/i18n/index.ts | 47 +++++++++++++++++++ lxl-web/src/lib/i18n/interpolate.test.ts | 10 ++++ lxl-web/src/lib/i18n/interpolate.ts | 8 ++++ lxl-web/src/lib/i18n/locales.ts | 14 ++++++ lxl-web/src/lib/i18n/locales/en.js | 11 +++++ lxl-web/src/lib/i18n/locales/sv.js | 12 +++++ lxl-web/src/routes/(app)/+layout.ts | 8 ++++ .../routes/(app)/[[lang=lang]]/+layout.svelte | 35 ++++++++++++-- .../routes/(app)/[[lang=lang]]/+page.svelte | 14 +++++- .../(app)/[[lang=lang]]/search/+page.svelte | 2 +- 11 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 lxl-web/src/lib/i18n/index.ts create mode 100644 lxl-web/src/lib/i18n/interpolate.test.ts create mode 100644 lxl-web/src/lib/i18n/interpolate.ts create mode 100644 lxl-web/src/lib/i18n/locales.ts create mode 100644 lxl-web/src/lib/i18n/locales/en.js create mode 100644 lxl-web/src/lib/i18n/locales/sv.js create mode 100644 lxl-web/src/routes/(app)/+layout.ts diff --git a/lxl-web/src/app.d.ts b/lxl-web/src/app.d.ts index f59b884c5..e97b185ff 100644 --- a/lxl-web/src/app.d.ts +++ b/lxl-web/src/app.d.ts @@ -4,7 +4,10 @@ declare global { namespace App { // interface Error {} // interface Locals {} - // interface PageData {} + interface PageData { + locale: import('$lib/i18n/locales').LocaleCode; + t: Awaited>; + } // interface Platform {} } } diff --git a/lxl-web/src/lib/i18n/index.ts b/lxl-web/src/lib/i18n/index.ts new file mode 100644 index 000000000..88fcac139 --- /dev/null +++ b/lxl-web/src/lib/i18n/index.ts @@ -0,0 +1,47 @@ +import { dev } from '$app/environment'; +import { interpolate } from './interpolate'; +import { defaultLocale, type LocaleCode } from './locales'; +import sv from './locales/sv.js'; + +const loadedTranslations: Record = { + sv +}; + +export async function getTranslator(locale: LocaleCode) { + if (!loadedTranslations[locale]) { + loadedTranslations[locale] = (await import(`./locales/${locale}.js`)).default; + // add error handling? + } + + return (key: string, options?: { values?: Record }): string => { + if (!key.includes('.')) { + // do we require nested keys? + throw new Error('Incorrect i11n key'); + } + const [section, item] = key.split('.') as [string, string]; + + // @ts-expect-error - how to typecheck?? + const localeResult = loadedTranslations[locale][section]?.[item]; + + if (localeResult) { + return interpolate(localeResult, options?.values); + } else { + console.warn(`Missing ${locale} translation for ${key}`); + } + + // @ts-expect-error - how to typecheck?? + const fallbackResult = loadedTranslations[defaultLocale][section][item]; + + if (fallbackResult) { + return interpolate(fallbackResult, options?.values); + } + + const error = `Missing fallback translation for ${key}`; + if (dev) { + throw new Error(error); + } + + console.error(error); + return key; + }; +} diff --git a/lxl-web/src/lib/i18n/interpolate.test.ts b/lxl-web/src/lib/i18n/interpolate.test.ts new file mode 100644 index 000000000..8b8a0f3a6 --- /dev/null +++ b/lxl-web/src/lib/i18n/interpolate.test.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from 'vitest'; +import { interpolate } from './interpolate'; + +describe('iterpolate', () => { + it('replaces placeholders with corresponding values', () => { + const template = 'Hi {name}, welcome to {site}!'; + const values = { name: 'Kalle', site: 'Libris' }; + expect(interpolate(template, values)).toEqual('Hi Kalle, welcome to Libris!'); + }); +}); diff --git a/lxl-web/src/lib/i18n/interpolate.ts b/lxl-web/src/lib/i18n/interpolate.ts new file mode 100644 index 000000000..5b5120f61 --- /dev/null +++ b/lxl-web/src/lib/i18n/interpolate.ts @@ -0,0 +1,8 @@ +const placeholder = /{(.*?)}/g; + +export function interpolate(template: string, values?: Record) { + if (!values) { + return template; + } + return template.replace(placeholder, (match, key) => values[key] || match); +} diff --git a/lxl-web/src/lib/i18n/locales.ts b/lxl-web/src/lib/i18n/locales.ts new file mode 100644 index 000000000..c3a22a5d7 --- /dev/null +++ b/lxl-web/src/lib/i18n/locales.ts @@ -0,0 +1,14 @@ +export enum Locales { + sv = 'Svenska', + en = 'English' +} + +export type LocaleCode = keyof typeof Locales; +export const defaultLocale = 'sv'; + +export function getSupportedLocale(userLocale: string | undefined): LocaleCode { + const locale = Object.keys(Locales).find((supportedLocale) => { + return userLocale?.includes(supportedLocale); + }) as LocaleCode; + return locale || defaultLocale; +} diff --git a/lxl-web/src/lib/i18n/locales/en.js b/lxl-web/src/lib/i18n/locales/en.js new file mode 100644 index 000000000..f993e4061 --- /dev/null +++ b/lxl-web/src/lib/i18n/locales/en.js @@ -0,0 +1,11 @@ +/** @type {typeof import('./sv.js').default} */ +export default { + home: { + welcome_text: 'Welcome to {site}, today is {day}' + }, + search: { + result_info: 'Search result for: {q}' + }, + errors: {}, + general: {} +}; diff --git a/lxl-web/src/lib/i18n/locales/sv.js b/lxl-web/src/lib/i18n/locales/sv.js new file mode 100644 index 000000000..787e7d08e --- /dev/null +++ b/lxl-web/src/lib/i18n/locales/sv.js @@ -0,0 +1,12 @@ +export default { + home: { + welcome_text: 'Välkommen till {site}, idag är det {day}' + }, + search: { + result_info: 'Sökresultat för: {q}' + }, + errors: {}, + general: { + // shared stuff go here + } +}; diff --git a/lxl-web/src/routes/(app)/+layout.ts b/lxl-web/src/routes/(app)/+layout.ts new file mode 100644 index 000000000..4ff6ce72b --- /dev/null +++ b/lxl-web/src/routes/(app)/+layout.ts @@ -0,0 +1,8 @@ +import { getTranslator } from '$lib/i18n/index.js'; +import { getSupportedLocale } from '$lib/i18n/locales'; + +export async function load({ params }) { + const locale = getSupportedLocale(params?.lang); // will use default locale if no lang param + const t = await getTranslator(locale); + return { locale, t }; +} diff --git a/lxl-web/src/routes/(app)/[[lang=lang]]/+layout.svelte b/lxl-web/src/routes/(app)/[[lang=lang]]/+layout.svelte index b8ca15c1f..d60ec5452 100644 --- a/lxl-web/src/routes/(app)/[[lang=lang]]/+layout.svelte +++ b/lxl-web/src/routes/(app)/[[lang=lang]]/+layout.svelte @@ -1,10 +1,37 @@ - -
- Libris - +
+
+ Libris + +
+
diff --git a/lxl-web/src/routes/(app)/[[lang=lang]]/+page.svelte b/lxl-web/src/routes/(app)/[[lang=lang]]/+page.svelte index 8bd470f97..123cfc04d 100644 --- a/lxl-web/src/routes/(app)/[[lang=lang]]/+page.svelte +++ b/lxl-web/src/routes/(app)/[[lang=lang]]/+page.svelte @@ -1 +1,13 @@ -

Welcome to Libris(?)

+ + +

+ {data.t('home.welcome_text', { + values: { + site: 'Libris', + day: `${(() => + new Intl.DateTimeFormat(data.locale, { weekday: 'long' }).format(new Date()))()}` + } + })} +

diff --git a/lxl-web/src/routes/(app)/[[lang=lang]]/search/+page.svelte b/lxl-web/src/routes/(app)/[[lang=lang]]/search/+page.svelte index cd59cc5c9..79d9bc0f2 100644 --- a/lxl-web/src/routes/(app)/[[lang=lang]]/search/+page.svelte +++ b/lxl-web/src/routes/(app)/[[lang=lang]]/search/+page.svelte @@ -7,7 +7,7 @@ $: q = $page.url.searchParams.get('q'); -

Search results for: {q}

+

{$page.data.t('search.result_info', { values: { q: `${q}` } })}

    {#each data.items as item (item['@id'])}
  • {item['@id']}
  • From 9bf3cb594ed9fdd797075a8925b85e15449bc783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Fri, 12 Jan 2024 11:28:52 +0100 Subject: [PATCH 2/6] Create lang component, simplify t options arg --- lxl-web/src/lib/components/LangPicker.svelte | 27 +++++++++++++++++++ lxl-web/src/lib/i18n/index.ts | 7 ++--- .../routes/(app)/[[lang=lang]]/+layout.svelte | 27 ++----------------- .../routes/(app)/[[lang=lang]]/+page.svelte | 7 ++--- .../(app)/[[lang=lang]]/search/+page.svelte | 2 +- 5 files changed, 36 insertions(+), 34 deletions(-) create mode 100644 lxl-web/src/lib/components/LangPicker.svelte diff --git a/lxl-web/src/lib/components/LangPicker.svelte b/lxl-web/src/lib/components/LangPicker.svelte new file mode 100644 index 000000000..09f9adfb0 --- /dev/null +++ b/lxl-web/src/lib/components/LangPicker.svelte @@ -0,0 +1,27 @@ + + + diff --git a/lxl-web/src/lib/i18n/index.ts b/lxl-web/src/lib/i18n/index.ts index 88fcac139..e492ca31a 100644 --- a/lxl-web/src/lib/i18n/index.ts +++ b/lxl-web/src/lib/i18n/index.ts @@ -3,6 +3,7 @@ import { interpolate } from './interpolate'; import { defaultLocale, type LocaleCode } from './locales'; import sv from './locales/sv.js'; +// always import default translation? const loadedTranslations: Record = { sv }; @@ -13,7 +14,7 @@ export async function getTranslator(locale: LocaleCode) { // add error handling? } - return (key: string, options?: { values?: Record }): string => { + return (key: string, values?: { [key: string]: string }): string => { if (!key.includes('.')) { // do we require nested keys? throw new Error('Incorrect i11n key'); @@ -24,7 +25,7 @@ export async function getTranslator(locale: LocaleCode) { const localeResult = loadedTranslations[locale][section]?.[item]; if (localeResult) { - return interpolate(localeResult, options?.values); + return interpolate(localeResult, values); } else { console.warn(`Missing ${locale} translation for ${key}`); } @@ -33,7 +34,7 @@ export async function getTranslator(locale: LocaleCode) { const fallbackResult = loadedTranslations[defaultLocale][section][item]; if (fallbackResult) { - return interpolate(fallbackResult, options?.values); + return interpolate(fallbackResult, values); } const error = `Missing fallback translation for ${key}`; diff --git a/lxl-web/src/routes/(app)/[[lang=lang]]/+layout.svelte b/lxl-web/src/routes/(app)/[[lang=lang]]/+layout.svelte index d60ec5452..bdcc5956f 100644 --- a/lxl-web/src/routes/(app)/[[lang=lang]]/+layout.svelte +++ b/lxl-web/src/routes/(app)/[[lang=lang]]/+layout.svelte @@ -1,18 +1,7 @@
    @@ -20,18 +9,6 @@ Libris - +
    diff --git a/lxl-web/src/routes/(app)/[[lang=lang]]/+page.svelte b/lxl-web/src/routes/(app)/[[lang=lang]]/+page.svelte index 123cfc04d..e4bf49964 100644 --- a/lxl-web/src/routes/(app)/[[lang=lang]]/+page.svelte +++ b/lxl-web/src/routes/(app)/[[lang=lang]]/+page.svelte @@ -4,10 +4,7 @@

    {data.t('home.welcome_text', { - values: { - site: 'Libris', - day: `${(() => - new Intl.DateTimeFormat(data.locale, { weekday: 'long' }).format(new Date()))()}` - } + site: 'Libris', + day: `${(() => new Intl.DateTimeFormat(data.locale, { weekday: 'long' }).format(new Date()))()}` })}

    diff --git a/lxl-web/src/routes/(app)/[[lang=lang]]/search/+page.svelte b/lxl-web/src/routes/(app)/[[lang=lang]]/search/+page.svelte index 79d9bc0f2..4b8fec3b5 100644 --- a/lxl-web/src/routes/(app)/[[lang=lang]]/search/+page.svelte +++ b/lxl-web/src/routes/(app)/[[lang=lang]]/search/+page.svelte @@ -7,7 +7,7 @@ $: q = $page.url.searchParams.get('q'); -

    {$page.data.t('search.result_info', { values: { q: `${q}` } })}

    +

    {$page.data.t('search.result_info', { q: `${q}` })}

      {#each data.items as item (item['@id'])}
    • {item['@id']}
    • From 5a99c2afb3a641edb199b2f37e53a8dc0199d28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Engstr=C3=B6m?= Date: Fri, 12 Jan 2024 12:51:17 +0100 Subject: [PATCH 3/6] Change html lang in server hook --- lxl-web/src/hooks.server.ts | 18 ++++++++++++++++++ lxl-web/src/lib/components/LangPicker.svelte | 9 +-------- 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 lxl-web/src/hooks.server.ts diff --git a/lxl-web/src/hooks.server.ts b/lxl-web/src/hooks.server.ts new file mode 100644 index 000000000..e32826a26 --- /dev/null +++ b/lxl-web/src/hooks.server.ts @@ -0,0 +1,18 @@ +import { defaultLocale, Locales } from '$lib/i18n/locales'; + +export const handle = async ({ event, resolve }) => { + // set HTML lang + // https://github.com/sveltejs/kit/issues/3091#issuecomment-1112589090 + const path = event.url.pathname; + let lang = defaultLocale; + + Object.keys(Locales).forEach((locale) => { + if (path && path.startsWith(`/${locale}/`)) { + lang = locale; + } + }); + + return resolve(event, { + transformPageChunk: ({ html }) => html.replace('%lang%', lang) + }); +}; diff --git a/lxl-web/src/lib/components/LangPicker.svelte b/lxl-web/src/lib/components/LangPicker.svelte index 09f9adfb0..4c8cb09ca 100644 --- a/lxl-web/src/lib/components/LangPicker.svelte +++ b/lxl-web/src/lib/components/LangPicker.svelte @@ -1,15 +1,8 @@