From 9b2c63d92ff77ebce0f90a7be05437504422bf45 Mon Sep 17 00:00:00 2001 From: ivarlovlie Date: Sat, 11 Feb 2023 23:37:12 +0100 Subject: feat: Render localized content from sanity --- src/app.html | 3 +- src/components/contact.svelte | 79 +++++++++++++++++++++------------- src/components/header.svelte | 12 ++++++ src/components/locale-switcher.svelte | 46 ++++++++++++++++++++ src/global.d.ts | 13 ++++++ src/hooks.server.ts | 43 ++++++++++++++++++ src/i18n/en/index.ts | 13 ++++++ src/i18n/formatters.ts | 11 +++++ src/i18n/i18n-svelte.ts | 12 ++++++ src/i18n/i18n-types.ts | 66 ++++++++++++++++++++++++++++ src/i18n/i18n-util.async.ts | 27 ++++++++++++ src/i18n/i18n-util.sync.ts | 26 +++++++++++ src/i18n/i18n-util.ts | 38 ++++++++++++++++ src/i18n/nb/index.ts | 13 ++++++ src/lib/sanity-client.ts | 9 ++++ src/lib/utils.ts | 27 ++++++++++++ src/params/lang.ts | 7 +++ src/routes/+layout.server.ts | 6 +++ src/routes/+layout.svelte | 15 ++++++- src/routes/+layout.ts | 14 ++++++ src/routes/+page.svelte | 0 src/routes/[lang=lang]/+page.server.ts | 19 ++++++++ src/routes/[lang=lang]/+page.svelte | 17 ++++++++ src/routes/[lang=lang]/+page.ts | 18 ++++++++ 24 files changed, 501 insertions(+), 33 deletions(-) create mode 100644 src/components/header.svelte create mode 100644 src/components/locale-switcher.svelte create mode 100644 src/global.d.ts create mode 100644 src/hooks.server.ts create mode 100644 src/i18n/en/index.ts create mode 100644 src/i18n/formatters.ts create mode 100644 src/i18n/i18n-svelte.ts create mode 100644 src/i18n/i18n-types.ts create mode 100644 src/i18n/i18n-util.async.ts create mode 100644 src/i18n/i18n-util.sync.ts create mode 100644 src/i18n/i18n-util.ts create mode 100644 src/i18n/nb/index.ts create mode 100644 src/lib/sanity-client.ts create mode 100644 src/lib/utils.ts create mode 100644 src/params/lang.ts create mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/+layout.ts delete mode 100644 src/routes/+page.svelte create mode 100644 src/routes/[lang=lang]/+page.server.ts create mode 100644 src/routes/[lang=lang]/+page.svelte create mode 100644 src/routes/[lang=lang]/+page.ts (limited to 'src') diff --git a/src/app.html b/src/app.html index c9bd64d..ef0bfa1 100644 --- a/src/app.html +++ b/src/app.html @@ -1,6 +1,5 @@ - - + diff --git a/src/components/contact.svelte b/src/components/contact.svelte index c7b0be3..9f5bb98 100644 --- a/src/components/contact.svelte +++ b/src/components/contact.svelte @@ -1,45 +1,66 @@ + +
-

Contact Us

+

{$LL.contact.title()}

-
-
Address
-
- {address1}
{address2}
{address3} -
-
- -
-
Email
-
- {email} -
-
- -
-
Phone
-
-

{phone}

-

{phoneHours}

-
-
+ {#if (model.addressLines?.length ?? 0) > 0} +
+
{$LL.contact.addressTitle()}
+
+ {#each model.addressLines as line} + {line}
+ {/each} +
+
+ {/if} + {#if model.email} +
+
{$LL.contact.emailTitle()}
+
+ {model.email} +
+
+ {/if} + {#if model.phone} +
+
{$LL.contact.phoneTitle()}
+
+

{model.phone}

+ {#if model.phoneHours} +

{model.phoneHours}

+ {/if} +
+
+ {/if}
-
diff --git a/src/components/header.svelte b/src/components/header.svelte new file mode 100644 index 0000000..5789196 --- /dev/null +++ b/src/components/header.svelte @@ -0,0 +1,12 @@ + + +
+ +

Auroraklinikken

+
+ + +
diff --git a/src/components/locale-switcher.svelte b/src/components/locale-switcher.svelte new file mode 100644 index 0000000..938d18a --- /dev/null +++ b/src/components/locale-switcher.svelte @@ -0,0 +1,46 @@ + + + + +
    + {#each locales as l} +
  • + + {l} + +
  • + {/each} +
diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..e49922a --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,13 @@ +/// + +type Locales = import('$i18n/i18n-types').Locales +type TranslationFunctions = import('$i18n/i18n-types').TranslationFunctions + +declare namespace App { + interface Locals { + locale: Locales + LL: TranslationFunctions + } + + // interface Platform { } +} \ No newline at end of file diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..ddd9342 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,43 @@ +import { detectLocale, i18n, isLocale } from '$i18n/i18n-util' +import { loadAllLocales } from '$i18n/i18n-util.sync' +import type { Handle, RequestEvent } from '@sveltejs/kit' +import { initAcceptLanguageHeaderDetector } from 'typesafe-i18n/detectors' + +loadAllLocales() +const L = i18n() + +export const handle: Handle = async ({ event, resolve }) => { + // read language slug + const [, lang] = event.url.pathname.split('/') + + // redirect to base locale if no locale slug was found + if (!lang) { + const locale = getPreferredLocale(event) + + return new Response(null, { + status: 302, + headers: { Location: `/${locale}` }, + }) + } + + // if slug is not a locale, use base locale (e.g. api endpoints) + const locale = isLocale(lang) ? (lang as Locales) : getPreferredLocale(event) + const LL = L[locale] + + // bind locale and translation functions to current request + event.locals.locale = locale + event.locals.LL = LL + + console.info(LL.log({ fileName: 'hooks.server.ts' })) + + // replace html lang attribute with correct language + return resolve(event, { transformPageChunk: ({ html }) => html.replace('%lang%', locale) }) +} + +const getPreferredLocale = ({ request }: RequestEvent) => { + // detect the preferred language the user has configured in his browser + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language + const acceptLanguageDetector = initAcceptLanguageHeaderDetector(request) + + return detectLocale(acceptLanguageDetector) +} \ No newline at end of file diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts new file mode 100644 index 0000000..796f4a5 --- /dev/null +++ b/src/i18n/en/index.ts @@ -0,0 +1,13 @@ +import type { BaseTranslation } from '../i18n-types' + +const en = { + contact: { + title: "Contact us", + addressTitle: "Address", + emailTitle: "Email", + phoneTitle: "Phone" + }, + homeTitle: "Home", +} satisfies BaseTranslation + +export default en diff --git a/src/i18n/formatters.ts b/src/i18n/formatters.ts new file mode 100644 index 0000000..78734f9 --- /dev/null +++ b/src/i18n/formatters.ts @@ -0,0 +1,11 @@ +import type { FormattersInitializer } from 'typesafe-i18n' +import type { Locales, Formatters } from './i18n-types' + +export const initFormatters: FormattersInitializer = (locale: Locales) => { + + const formatters: Formatters = { + // add your formatter functions here + } + + return formatters +} diff --git a/src/i18n/i18n-svelte.ts b/src/i18n/i18n-svelte.ts new file mode 100644 index 0000000..6cdffb3 --- /dev/null +++ b/src/i18n/i18n-svelte.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initI18nSvelte } from 'typesafe-i18n/svelte' +import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales } from './i18n-util' + +const { locale, LL, setLocale } = initI18nSvelte(loadedLocales, loadedFormatters) + +export { locale, LL, setLocale } + +export default LL diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts new file mode 100644 index 0000000..9a32b2f --- /dev/null +++ b/src/i18n/i18n-types.ts @@ -0,0 +1,66 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ +import type { BaseTranslation as BaseTranslationType, LocalizedString } from 'typesafe-i18n' + +export type BaseTranslation = BaseTranslationType +export type BaseLocale = 'en' + +export type Locales = + | 'en' + | 'nb' + +export type Translation = RootTranslation + +export type Translations = RootTranslation + +type RootTranslation = { + contact: { + /** + * C​o​n​t​a​c​t​ ​u​s + */ + title: string + /** + * A​d​d​r​e​s​s + */ + addressTitle: string + /** + * E​m​a​i​l + */ + emailTitle: string + /** + * P​h​o​n​e + */ + phoneTitle: string + } + /** + * H​o​m​e + */ + homeTitle: string +} + +export type TranslationFunctions = { + contact: { + /** + * Contact us + */ + title: () => LocalizedString + /** + * Address + */ + addressTitle: () => LocalizedString + /** + * Email + */ + emailTitle: () => LocalizedString + /** + * Phone + */ + phoneTitle: () => LocalizedString + } + /** + * Home + */ + homeTitle: () => LocalizedString +} + +export type Formatters = {} diff --git a/src/i18n/i18n-util.async.ts b/src/i18n/i18n-util.async.ts new file mode 100644 index 0000000..3f38e3e --- /dev/null +++ b/src/i18n/i18n-util.async.ts @@ -0,0 +1,27 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initFormatters } from './formatters' +import type { Locales, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales, locales } from './i18n-util' + +const localeTranslationLoaders = { + en: () => import('./en'), + nb: () => import('./nb'), +} + +const updateDictionary = (locale: Locales, dictionary: Partial): Translations => + loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary } + +export const importLocaleAsync = async (locale: Locales): Promise => + (await localeTranslationLoaders[locale]()).default as unknown as Translations + +export const loadLocaleAsync = async (locale: Locales): Promise => { + updateDictionary(locale, await importLocaleAsync(locale)) + loadFormatters(locale) +} + +export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync)) + +export const loadFormatters = (locale: Locales): void => + void (loadedFormatters[locale] = initFormatters(locale)) diff --git a/src/i18n/i18n-util.sync.ts b/src/i18n/i18n-util.sync.ts new file mode 100644 index 0000000..f1a8e9e --- /dev/null +++ b/src/i18n/i18n-util.sync.ts @@ -0,0 +1,26 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initFormatters } from './formatters' +import type { Locales, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales, locales } from './i18n-util' + +import en from './en' +import nb from './nb' + +const localeTranslations = { + en, + nb, +} + +export const loadLocale = (locale: Locales): void => { + if (loadedLocales[locale]) return + + loadedLocales[locale] = localeTranslations[locale] as unknown as Translations + loadFormatters(locale) +} + +export const loadAllLocales = (): void => locales.forEach(loadLocale) + +export const loadFormatters = (locale: Locales): void => + void (loadedFormatters[locale] = initFormatters(locale)) diff --git a/src/i18n/i18n-util.ts b/src/i18n/i18n-util.ts new file mode 100644 index 0000000..0d1cef0 --- /dev/null +++ b/src/i18n/i18n-util.ts @@ -0,0 +1,38 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n' +import type { LocaleDetector } from 'typesafe-i18n/detectors' +import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n' +import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors' +import { initExtendDictionary } from 'typesafe-i18n/utils' +import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types' + +export const baseLocale: Locales = 'en' + +export const locales: Locales[] = [ + 'en', + 'nb' +] + +export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales) + +export const loadedLocales: Record = {} as Record + +export const loadedFormatters: Record = {} as Record + +export const extendDictionary = initExtendDictionary() + +export const i18nString = (locale: Locales): TranslateByString => initI18nString(locale, loadedFormatters[locale]) + +export const i18nObject = (locale: Locales): TranslationFunctions => + initI18nObject( + locale, + loadedLocales[locale], + loadedFormatters[locale] + ) + +export const i18n = (): LocaleTranslationFunctions => + initI18n(loadedLocales, loadedFormatters) + +export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn(baseLocale, locales, ...detectors) diff --git a/src/i18n/nb/index.ts b/src/i18n/nb/index.ts new file mode 100644 index 0000000..9e13fba --- /dev/null +++ b/src/i18n/nb/index.ts @@ -0,0 +1,13 @@ +import type { Translation } from '../i18n-types' + +const nb = { + contact: { + title: "Kontakt oss", + addressTitle: "Adresse", + emailTitle: "E-postadresse", + phoneTitle: "Telefon" + }, + homeTitle: "Hjem", +} satisfies Translation + +export default nb diff --git a/src/lib/sanity-client.ts b/src/lib/sanity-client.ts new file mode 100644 index 0000000..70be363 --- /dev/null +++ b/src/lib/sanity-client.ts @@ -0,0 +1,9 @@ +import { env } from "$env/dynamic/private"; +import sanityClient from "@sanity/client"; + +export const sanity = sanityClient({ + projectId: env.SANITY_STUDIO_API_PROJECT_ID, + dataset: env.SANITY_STUDIO_API_DATASET, + apiVersion: "2022-03-24", + useCdn: true, +}); \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..4ec8b01 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,27 @@ +// Replaces the locale slug in a URL. +// +// If the `full` argument is set to `true`, the full URL is returned as a string. +// e.g. https://mywebsite.com/en/blog/article-1 => https://mywebsite.com/de/blog/article-1 +// +// Otherwise (default) the URL relative to the base is returned. +// e.g. https://mywebsite.com/en/blog/article-1 => /de/blog/article-1 +export const replaceLocaleInUrl = (url: URL, locale: string, full = false): string => { + const [, , ...rest] = url.pathname.split('/') + const new_pathname = `/${[locale, ...rest].join('/')}` + if (!full) { + return `${new_pathname}${url.search}` + } + const newUrl = new URL(url.toString()) + newUrl.pathname = new_pathname + return newUrl.toString() +} + +export function fromLocalizedString(localizedString: string | object, locale: Locales) { + if (typeof localizedString === "string") return localizedString; + // @ts-ignore + if (localizedString[locale]) return localizedString[locale]; + // @ts-ignore + if (localizedString["nb"]) return localizedString["nb"]; + // @ts-ignore + if (localizedString["en"]) return localizedString["en"]; +} diff --git a/src/params/lang.ts b/src/params/lang.ts new file mode 100644 index 0000000..5f0b857 --- /dev/null +++ b/src/params/lang.ts @@ -0,0 +1,7 @@ +import type { ParamMatcher } from '@sveltejs/kit' +import { isLocale } from '$i18n/i18n-util' + +// only accept valid languages as a segment in the URL +export const match: ParamMatcher = (param) => { + return isLocale(param) +} \ No newline at end of file diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..fc087d8 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,6 @@ +import type { LayoutServerLoad } from './$types' + +export const load: LayoutServerLoad = ({ locals: { locale, LL } }) => { + // pass locale information from "server-context" to "shared server + client context" + return { locale } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 30062ed..990dd94 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,8 +1,19 @@ + + {$page.data.title || "auroraklinikken.no"} + {#each locales as l} + + {/each} + + + diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..5a78ab1 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,14 @@ +import type { LayoutLoad } from './$types' +import { loadLocaleAsync } from '$i18n/i18n-util.async' +import { setLocale } from '$i18n/i18n-svelte' + +export const load = (async ({ data: { locale } }) => { + // load dictionary into memory + await loadLocaleAsync(locale) + + // if you need to output a localized string in a `load` function, + // you always need to call `setLocale` right before you access the `LL` store + setLocale(locale) + // pass locale to the "rendering context" + return { locale } +}) satisfies LayoutLoad<{ locale: Locales }>; \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/[lang=lang]/+page.server.ts b/src/routes/[lang=lang]/+page.server.ts new file mode 100644 index 0000000..d3a98a5 --- /dev/null +++ b/src/routes/[lang=lang]/+page.server.ts @@ -0,0 +1,19 @@ +import { sanity } from '$lib/sanity-client'; +import type { PageServerLoad } from './$types'; +import groq from "groq"; +import type { ContactModel } from '$components/contact.svelte'; +import { fromLocalizedString } from '$lib/utils'; + +export const load = (async ({ locals }) => { + const contactSection = await sanity.fetch(groq`*[_type == "contact"][0]`); + console.log(contactSection); + + return { + contact: { + phone: fromLocalizedString(contactSection.phone, locals.locale), + email: fromLocalizedString(contactSection.email, locals.locale), + phoneHours: fromLocalizedString(contactSection.phoneHours, locals.locale), + addressLines: contactSection.addressLines.map((el: string | object) => fromLocalizedString(el, locals.locale)), + } as ContactModel + }; +}) satisfies PageServerLoad; \ No newline at end of file diff --git a/src/routes/[lang=lang]/+page.svelte b/src/routes/[lang=lang]/+page.svelte new file mode 100644 index 0000000..666aa80 --- /dev/null +++ b/src/routes/[lang=lang]/+page.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/routes/[lang=lang]/+page.ts b/src/routes/[lang=lang]/+page.ts new file mode 100644 index 0000000..fa3907b --- /dev/null +++ b/src/routes/[lang=lang]/+page.ts @@ -0,0 +1,18 @@ +import type { PageLoad } from './$types'; +import LL, { setLocale } from '$i18n/i18n-svelte' +import { get } from 'svelte/store' + +export const load = (async ({ parent, data }) => { + // wait for `+layout.ts` to load dictionary and pass locale information + const { locale } = await parent() + + // if you need to output a localized string in a `load` function, + // you always need to call `setLocale` right before you access the `LL` store + setLocale(locale) + // get the translation functions value from the store + const $LL = get(LL) + return { + title: $LL.homeTitle(), + contact: data.contact + } +}) satisfies PageLoad; \ No newline at end of file -- cgit v1.3