[+] i18n SSR

This commit is contained in:
2025-11-23 00:34:03 +08:00
parent dd73599bb3
commit 82f2858cde
6 changed files with 52 additions and 46 deletions
+9 -7
View File
@@ -1,13 +1,15 @@
// See https://svelte.dev/docs/kit/types#app.d.ts // See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces // for information about these interfaces
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
// interface Locals {} interface Locals {
// interface PageData {} lang: 'en' | 'zh'
// interface PageState {} }
// interface Platform {} // interface PageData {}
} // interface PageState {}
// interface Platform {}
}
} }
export {}; export {};
+1 -1
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="%lang%">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
+22
View File
@@ -1,3 +1,25 @@
import { checkAudioSeparator } from '$lib/server/separator'; import { checkAudioSeparator } from '$lib/server/separator';
import type { Handle } from '@sveltejs/kit';
checkAudioSeparator().catch(e => console.error('Audio separator check failed:', e)); checkAudioSeparator().catch(e => console.error('Audio separator check failed:', e));
export const handle: Handle = async ({ event, resolve }) => {
const langCookie = event.cookies.get('lang');
if (langCookie === 'zh' || langCookie === 'en') {
event.locals.lang = langCookie;
} else {
const acceptLanguage = event.request.headers.get('accept-language');
// Simple check: if zh is present, prefer it (unless en is weighted higher, but let's keep it simple for now)
// A better parser would parse q-values.
// For now, let's assume if 'zh' is in the header, the user likely understands Chinese.
if (acceptLanguage && acceptLanguage.includes('zh')) {
event.locals.lang = 'zh';
} else {
event.locals.lang = 'en';
}
}
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%lang%', event.locals.lang)
});
};
+15 -35
View File
@@ -1,7 +1,11 @@
import { getContext, setContext } from 'svelte'
import EN from "./en" import EN from "./en"
import ZH from "./zh" import ZH from "./zh"
import { browser } from '$app/environment'
// i18n related files:
// src\hooks.server.ts
// src\routes\+layout.server.ts
type Lang = 'en' | 'zh' type Lang = 'en' | 'zh'
@@ -10,44 +14,20 @@ const msgs: Record<Lang, typeof ZH> = {
zh: ZH zh: ZH
} }
let lang: Lang = 'en' const I18N_KEY = Symbol('i18n')
// Infer language from browser export const initI18n = (lang: Lang) => {
if (browser && navigator.language.startsWith('zh')) { setContext(I18N_KEY, msgs[lang])
lang = 'zh'
} }
export const getI18n = () => msgs[lang] export const getI18n = () => {
return getContext<typeof ZH>(I18N_KEY) || msgs['en']
// export function ts(key: string, variables?: { [index: string]: any }) { }
// return t(key as keyof LocalizedMessages, variables)
// }
// /**
// * Load the translation for the given key
// *
// * @param key
// * @param variables
// */
// export function t(key: keyof LocalizedMessages, variables?: { [index: string]: any }) {
// // Check if the key exists
// let msg = getI18n()[key]
// if (!msg) {
// // Check if the key exists in English
// if (!(msg = getI18n()[key])) {
// msg = key
// console.error(`ERROR!! Missing translation reference entry (English) for ${key}`)
// }
// else console.warn(`Missing translation for ${key} in ${lang}`)
// }
// // Replace variables
// if (variables) {
// return msg.replace(/\${(.*?)}/g, (_: string, v: string | number) => variables[v] + "")
// }
// return msg
// }
// Object.assign(window, { t })
export const setLanguage = (lang: Lang) => {
document.cookie = `lang=${lang}; path=/; max-age=31536000`
location.reload()
}
export {} export {}
+3 -3
View File
@@ -1,7 +1,7 @@
import { createUser, login } from '$lib/server/user' import { createUser, login } from '$lib/server/user'
import type { LayoutServerLoad } from './$types' import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ cookies, request }) => { export const load: LayoutServerLoad = async ({ cookies, request, locals }) => {
let session = cookies.get('session') let session = cookies.get('session')
const registUA = request.headers.get('user-agent') || '' const registUA = request.headers.get('user-agent') || ''
@@ -20,7 +20,7 @@ export const load: LayoutServerLoad = async ({ cookies, request }) => {
try { try {
const user = structuredClone(await login(session)) const user = structuredClone(await login(session))
return { user } return { user, lang: locals.lang }
} catch (e) { } catch (e) {
// Invalid session, create new // Invalid session, create new
session = await createUser(registUA) session = await createUser(registUA)
@@ -32,6 +32,6 @@ export const load: LayoutServerLoad = async ({ cookies, request }) => {
maxAge: 60 * 60 * 24 * 365 maxAge: 60 * 60 * 24 * 365
}) })
const user = structuredClone(await login(session)) const user = structuredClone(await login(session))
return { user } return { user, lang: locals.lang }
} }
} }
+2
View File
@@ -6,10 +6,12 @@
import "../style/material.scss" import "../style/material.scss"
import '@unocss/reset/normalize.css' import '@unocss/reset/normalize.css'
import '@unocss/reset/tailwind-v4.css' import '@unocss/reset/tailwind-v4.css'
import { initI18n } from "$lib/i18n"
import type { LayoutProps } from "./$types" import type { LayoutProps } from "./$types"
import { onNavigate } from '$app/navigation' import { onNavigate } from '$app/navigation'
let { data, children }: LayoutProps = $props() let { data, children }: LayoutProps = $props()
initI18n(data.lang || 'en')
// This function is called when the user navigates to a new page, it will start the view transition // This function is called when the user navigates to a new page, it will start the view transition
onNavigate((navigation) => { onNavigate((navigation) => {