From 7a02d455a76ad356546816b0ff1af2b9d965130f Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sat, 22 Nov 2025 23:17:26 +0800 Subject: [PATCH 1/7] [+] i18n framework --- src/lib/i18n/en.ts | 17 +++++++++++ src/lib/i18n/index.ts | 62 +++++++++++++++++++++++++++++++++++++++++ src/lib/i18n/zh.ts | 17 +++++++++++ src/routes/+page.svelte | 33 ++++++++++++---------- 4 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 src/lib/i18n/en.ts create mode 100644 src/lib/i18n/index.ts create mode 100644 src/lib/i18n/zh.ts diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts new file mode 100644 index 0000000..e5a6807 --- /dev/null +++ b/src/lib/i18n/en.ts @@ -0,0 +1,17 @@ + +export default { + home: { + titles: { + continue: 'Continue from pause', + history: 'History', + myPlaylists: 'My Playlists', + recPlaylists: 'Recommended Playlists' + }, + btn: { + importFromNetease: 'Import from NetEase' + }, + text: { + playlistCreatedBy: 'From {u}' + } + } +} \ No newline at end of file diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts new file mode 100644 index 0000000..f11e722 --- /dev/null +++ b/src/lib/i18n/index.ts @@ -0,0 +1,62 @@ +import EN from "./en" +import ZH from "./zh" + +type Lang = 'en' | 'zh' + +const msgs: Record = { + en: EN, + zh: ZH +} + +let lang: Lang = 'en' + +// Infer language from browser +if (navigator.language.startsWith('zh')) { + lang = 'zh' +} + +export const getI18n = () => msgs[lang] + +// 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 {} + +declare global { + interface String { + sed(variables: { [index: string]: any }): string + } +} + +String.prototype.sed = function (variables: { [index: string]: any }) { + return this.replace(/\${(.*?)}/g, (_: string, v: string | number) => variables[v] + "") +} + +Object.defineProperty(String.prototype, 'sed', { enumerable: false }) diff --git a/src/lib/i18n/zh.ts b/src/lib/i18n/zh.ts new file mode 100644 index 0000000..261c042 --- /dev/null +++ b/src/lib/i18n/zh.ts @@ -0,0 +1,17 @@ + +export default { + home: { + titles: { + continue: '从暂停的位置继续', + history: '历史数据', + myPlaylists: '我的歌单', + recPlaylists: '推荐歌单' + }, + btn: { + importFromNetease: '从网易云导入' + }, + text: { + playlistCreatedBy: '{u} 创建' + } + } +} \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 482cbca..2e47312 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,14 +1,17 @@ - +
-

扫码登录

-

请使用网易云音乐 APP 扫码

+

{t.scanTitle}

+

{t.scanTip}

{#if status === 'loading'}
-

正在生成二维码...

+

{t.generating}

{:else if status === 'waiting_scan'}
@@ -78,8 +80,8 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an
-

已扫描

-

请在手机上确认登录

+

{t.scanned}

+

{t.confirm}

{:else if status === 'success'} @@ -88,7 +90,7 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an
-

登录成功

+

{t.success}

{:else if status === 'error'} @@ -96,7 +98,7 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an
-

错误: {errorMessage}

+

{t.errorPrefix}{errorMessage}

{/if} diff --git a/src/routes/import/netease/+page.svelte b/src/routes/import/netease/+page.svelte index f6de6e6..95b96ce 100644 --- a/src/routes/import/netease/+page.svelte +++ b/src/routes/import/netease/+page.svelte @@ -6,6 +6,9 @@ import { API } from "$lib/client" import ErrorDialog from "$lib/ui/status/ErrorDialog.svelte" import ProgressList from "$lib/ui/ProgressList.svelte" + import { getI18n } from "$lib/i18n" + + const t = getI18n().import.netease let link = $state('') @@ -34,8 +37,8 @@ return '' } - let listTitle = $derived(status === 'idle' ? '' : (status === 'importing' ? '正在导入' : (status === 'success' ? '导入完成' : '导入出错'))) - let listSubtitle = $derived(`${progress.done} / ${progress.total} 首歌曲`) + let listTitle = $derived(status === 'idle' ? '' : (status === 'importing' ? t.status.importing : (status === 'success' ? t.status.success : t.status.error))) + let listSubtitle = $derived(`${progress.done} / ${progress.total} ${t.songs}`) let listPercent = $derived(progress.total ? progress.done / progress.total * 100 : 0) let listItems = $derived(songs.map(song => ({ title: song.song.name, @@ -72,26 +75,26 @@ } - +
- 去网易云 APP 找一个你喜欢的日本语歌单,点击分享,再点击复制链接,然后把链接粘贴到这里就可以开始导入了! + {t.tip}
- +
{#if status === 'idle'} - + {:else if status === 'success'} - + {/if}
diff --git a/src/routes/playlist/[id]/+page.svelte b/src/routes/playlist/[id]/+page.svelte index 0de4172..a149b03 100644 --- a/src/routes/playlist/[id]/+page.svelte +++ b/src/routes/playlist/[id]/+page.svelte @@ -5,6 +5,9 @@ import Button from "$lib/ui/Button.svelte" import SongInfo from "$lib/ui/listitem/SongInfo.svelte" import { API } from "$lib/client" + import { getI18n } from "$lib/i18n" + + const t = getI18n().playlist.detail let { data }: PageProps = $props() @@ -42,7 +45,7 @@ } -
{meta.name}
-
创建者: {meta.creator.nickname}
-
歌曲数: {meta.trackCount}
+
{t.creator}{meta.creator.nickname}
+
{t.count}{meta.trackCount}
- +
-
歌曲列表
-
{songs.length} 首歌曲
+
{t.songList}
+
{songs.length} {t.songs}
{#each songs as song, index} diff --git a/src/routes/playlists/[category]/+page.svelte b/src/routes/playlists/[category]/+page.svelte index e236d57..aa25c12 100644 --- a/src/routes/playlists/[category]/+page.svelte +++ b/src/routes/playlists/[category]/+page.svelte @@ -4,18 +4,21 @@ import ImageListItem from "$lib/ui/listitem/ImageListItem.svelte" import MenuItem from "$lib/ui/material3/MenuItem.svelte" import type { PageProps } from "./$types" + import { getI18n } from "$lib/i18n" + + const t = getI18n().playlist.list let { data }: PageProps = $props() - - goto("/import/netease")}>从网易云导入 + + goto("/import/netease")}>{t.import}
{#each data.playlists as pl} - + {/each}
diff --git a/src/routes/results/[id]/+page.svelte b/src/routes/results/[id]/+page.svelte index be5e832..b19abbd 100644 --- a/src/routes/results/[id]/+page.svelte +++ b/src/routes/results/[id]/+page.svelte @@ -7,6 +7,9 @@ import Chart from "chart.js/auto" import { API } from "$lib/client" + import { getI18n } from "$lib/i18n" + + const t = getI18n().results import type { NeteaseSong, UserData } from "../../../lib/types" @@ -19,12 +22,12 @@ let duration = endTime - startTime let fields = [ - { label: "速度", value: Math.round(totalTyped / (Math.max(1, duration) / 60000)), unit: "CPM" }, - { label: "准确率", value: totalTyped === 0 ? 100 : Math.round((totalRight / totalTyped) * 10000) / 100, unit: "%" }, - { label: "实时率", value: data.result.realTimeFactor.toFixed(2), unit: "x" }, - { label: "字数", value: totalTyped, unit: "字" }, - { label: "用时", value: new Date(duration).toISOString().slice(14, 19), unit: "" }, - { label: "歌曲时长", value: new Date(data.song.dt).toISOString().slice(14, 19), unit: "" } + { label: t.fields.speed, value: Math.round(totalTyped / (Math.max(1, duration) / 60000)), unit: t.units.cpm }, + { label: t.fields.accuracy, value: totalTyped === 0 ? 100 : Math.round((totalRight / totalTyped) * 10000) / 100, unit: t.units.percent }, + { label: t.fields.realtime, value: data.result.realTimeFactor.toFixed(2), unit: t.units.x }, + { label: t.fields.count, value: totalTyped, unit: t.units.char }, + { label: t.fields.time, value: new Date(duration).toISOString().slice(14, 19), unit: "" }, + { label: t.fields.duration, value: new Date(data.song.dt).toISOString().slice(14, 19), unit: "" } ] let chartCanvas: HTMLCanvasElement @@ -38,7 +41,7 @@ labels: statsHistory.map((_: any, i: number) => i), datasets: [ { - label: "速度 (CPM)", + label: t.chart.speed, data: statsHistory.map((h: { cpm: number }) => h.cpm), tension: 0.4, pointRadius: 0, @@ -47,7 +50,7 @@ backgroundColor: "rgba(123, 120, 194, 0.1)", }, { - label: "准确率 (%)", + label: t.chart.accuracy, data: statsHistory.map((h: { acc: number }) => h.acc), tension: 0.4, yAxisID: "y1", @@ -146,9 +149,9 @@ } let buttonText = $derived( - nextSongId !== null ? "下一首" : - isPlaylistFinished ? "返回歌单" : - "再来一次" + nextSongId !== null ? t.btn.next : + isPlaylistFinished ? t.btn.back : + t.btn.retry ) @@ -156,7 +159,7 @@
-
练习结果
+
{t.title}
diff --git a/src/routes/song/[id]/+page.svelte b/src/routes/song/[id]/+page.svelte index dafeabb..f870126 100644 --- a/src/routes/song/[id]/+page.svelte +++ b/src/routes/song/[id]/+page.svelte @@ -6,6 +6,9 @@ import ProgressList from "$lib/ui/ProgressList.svelte" import { goto } from "$app/navigation" import { artistAndAlbum } from "$lib/utils" + import { getI18n } from "$lib/i18n" + + const t = getI18n().song.mode let { data } = $props() let loadStatus = $state<"idle" | "loading" | "done">("idle") @@ -52,7 +55,7 @@ {#if loadStatus === "done"}
- - + +
{/if} diff --git a/src/routes/song/[id]/karaoke/+page.svelte b/src/routes/song/[id]/karaoke/+page.svelte index 99e8696..bb7e02d 100644 --- a/src/routes/song/[id]/karaoke/+page.svelte +++ b/src/routes/song/[id]/karaoke/+page.svelte @@ -9,6 +9,9 @@ import { MusicControl } from "$lib/ui/player/MusicControl" import Lyrics from "$lib/ui/player/Lyrics.svelte" import PlayerAppBar from "$lib/ui/player/PlayerAppBar.svelte" + import { getI18n } from "$lib/i18n" + + const t = getI18n().song.karaoke let { data }: PageProps = $props() @@ -88,7 +91,7 @@
{:else}
- 未检测到人声分离音轨,无法调节人声音量。请先在歌曲详情页进行处理。 + {t.noVocals}
{/if}
diff --git a/src/routes/song/[id]/play/+page.svelte b/src/routes/song/[id]/play/+page.svelte index 5354931..7383f59 100644 --- a/src/routes/song/[id]/play/+page.svelte +++ b/src/routes/song/[id]/play/+page.svelte @@ -12,6 +12,9 @@ import { MusicControl } from "$lib/ui/player/MusicControl.ts" import Lyrics from "$lib/ui/player/Lyrics.svelte" import PlayerAppBar from "$lib/ui/player/PlayerAppBar.svelte" + import { getI18n } from "$lib/i18n" + + const t = getI18n().song.play let { data }: PageProps = $props() @@ -167,14 +170,14 @@
-
速度: {startTime ? Math.round(totalTyped / (Math.max(1, (now - startTime)) / 60000)) : '-'} cpm
-
正確率: {totalTyped === 0 ? 100 : Math.round((totalRight / totalTyped) * 100)}%
+
{t.speed}{startTime ? Math.round(totalTyped / (Math.max(1, (now - startTime)) / 60000)) : '-'} cpm
+
{t.accuracy}{totalTyped === 0 ? 100 : Math.round((totalRight / totalTyped) * 100)}%
-
正确:{flat.filter(s => s === 'right').length}
-
模糊:{flat.filter(s => s === 'fuzzy').length}
-
错误:{flat.filter(s => s === 'wrong').length}
-
剩余:{flat.filter(s => s === 'unseen').length}
+
{t.stats.right}{flat.filter(s => s === 'right').length}
+
{t.stats.fuzzy}{flat.filter(s => s === 'fuzzy').length}
+
{t.stats.wrong}{flat.filter(s => s === 'wrong').length}
+
{t.stats.remaining}{flat.filter(s => s === 'unseen').length}
diff --git a/src/routes/user/+page.svelte b/src/routes/user/+page.svelte index 03e0283..5af24e9 100644 --- a/src/routes/user/+page.svelte +++ b/src/routes/user/+page.svelte @@ -5,6 +5,9 @@ import Dialog from "$lib/ui/status/Dialog.svelte" import { API } from "$lib/client" import ErrorDialog from "$lib/ui/status/ErrorDialog.svelte" + import { getI18n } from "$lib/i18n" + + const t = getI18n().user let showCodeOpen = $state(false) let loginSuccessOpen = $state(false) @@ -25,34 +28,34 @@ - location.href = '/' + location.href = '/' }]}> - 登录成功! + {t.loginSuccess.content} - navigator.clipboard.writeText(generatedCode) + navigator.clipboard.writeText(generatedCode) }]}> - 引继码生成成功!生成的引继码是:{generatedCode} + {t.generateCode.success.sed({code: generatedCode})}

- 这个引继码将会在使用之后、或者未使用的 7 天后会失效 + {t.generateCode.expiry}
- +
- 这个 App 像日本的手机游戏一样采用引继码系统管理账号,并不需要用邮箱密码注册帐号。 + {t.desc.intro}

- 如果想要在另一个设备上登录账号,请点击「生成引继码」然后在想要登录的设备上点击「登录」 + {t.desc.instruction} {#if loginMode} -

您当前在「引继登录」页面,请在另一个设备上获取引继码之后输入到下面的输入框内点击「登录」 +

{t.desc.loginMode} {/if}
{#if loginMode}
- +
{/if} @@ -60,9 +63,9 @@
{#if !loginMode} - - + + {:else} - + {/if}
From dd73599bb3efb265daeff4b96b74ab348d5d0e8f Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:23:05 +0800 Subject: [PATCH 3/7] [F] Fix i18n --- src/lib/i18n/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index f11e722..fb68ff8 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -1,6 +1,8 @@ import EN from "./en" import ZH from "./zh" +import { browser } from '$app/environment' + type Lang = 'en' | 'zh' const msgs: Record = { @@ -11,7 +13,7 @@ const msgs: Record = { let lang: Lang = 'en' // Infer language from browser -if (navigator.language.startsWith('zh')) { +if (browser && navigator.language.startsWith('zh')) { lang = 'zh' } @@ -56,7 +58,7 @@ declare global { } String.prototype.sed = function (variables: { [index: string]: any }) { - return this.replace(/\${(.*?)}/g, (_: string, v: string | number) => variables[v] + "") + return this.replace(/{(.*?)}/g, (_: string, v: string | number) => variables[v] + "") } Object.defineProperty(String.prototype, 'sed', { enumerable: false }) From 82f2858cdebd255953c4be23fc79328a54864c99 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:34:03 +0800 Subject: [PATCH 4/7] [+] i18n SSR --- src/app.d.ts | 16 +++++++----- src/app.html | 2 +- src/hooks.server.ts | 22 ++++++++++++++++ src/lib/i18n/index.ts | 50 +++++++++++------------------------- src/routes/+layout.server.ts | 6 ++--- src/routes/+layout.svelte | 2 ++ 6 files changed, 52 insertions(+), 46 deletions(-) diff --git a/src/app.d.ts b/src/app.d.ts index da08e6d..6ef9db7 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,13 +1,15 @@ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } + namespace App { + // interface Error {} + interface Locals { + lang: 'en' | 'zh' + } + // interface PageData {} + // interface PageState {} + // interface Platform {} + } } export {}; diff --git a/src/app.html b/src/app.html index 0ea778b..ef0f393 100644 --- a/src/app.html +++ b/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/src/hooks.server.ts b/src/hooks.server.ts index d795584..3cadebf 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,3 +1,25 @@ import { checkAudioSeparator } from '$lib/server/separator'; +import type { Handle } from '@sveltejs/kit'; 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) + }); +}; diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index fb68ff8..1d92106 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -1,7 +1,11 @@ +import { getContext, setContext } from 'svelte' import EN from "./en" 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' @@ -10,44 +14,20 @@ const msgs: Record = { zh: ZH } -let lang: Lang = 'en' +const I18N_KEY = Symbol('i18n') -// Infer language from browser -if (browser && navigator.language.startsWith('zh')) { - lang = 'zh' +export const initI18n = (lang: Lang) => { + setContext(I18N_KEY, msgs[lang]) } -export const getI18n = () => msgs[lang] - -// 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 getI18n = () => { + return getContext(I18N_KEY) || msgs['en'] +} +export const setLanguage = (lang: Lang) => { + document.cookie = `lang=${lang}; path=/; max-age=31536000` + location.reload() +} export {} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 38dc587..331d9bf 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,7 +1,7 @@ import { createUser, login } from '$lib/server/user' 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') const registUA = request.headers.get('user-agent') || '' @@ -20,7 +20,7 @@ export const load: LayoutServerLoad = async ({ cookies, request }) => { try { const user = structuredClone(await login(session)) - return { user } + return { user, lang: locals.lang } } catch (e) { // Invalid session, create new session = await createUser(registUA) @@ -32,6 +32,6 @@ export const load: LayoutServerLoad = async ({ cookies, request }) => { maxAge: 60 * 60 * 24 * 365 }) const user = structuredClone(await login(session)) - return { user } + return { user, lang: locals.lang } } } \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 76f5360..7863452 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,10 +6,12 @@ import "../style/material.scss" import '@unocss/reset/normalize.css' import '@unocss/reset/tailwind-v4.css' + import { initI18n } from "$lib/i18n" import type { LayoutProps } from "./$types" import { onNavigate } from '$app/navigation' 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 onNavigate((navigation) => { From 199032c0621742610547bb81f5af16009f79fd76 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:46:59 +0800 Subject: [PATCH 5/7] [+] Set language button --- src/lib/i18n/en.ts | 2 +- src/routes/+page.svelte | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index 46380ac..048ca18 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -1,7 +1,7 @@ export default { home: { titles: { - continue: 'Continue from pause', + continue: 'Continue From Last Session', history: 'History', myPlaylists: 'My Playlists', recPlaylists: 'Recommended Playlists' diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2e47312..c5df11d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -6,7 +6,7 @@ import Button from "$lib/ui/Button.svelte" import { Layer } from "m3-svelte" import { goto } from "$app/navigation" - import { getI18n } from "$lib/i18n" + import { getI18n, setLanguage } from "$lib/i18n" let { data }: PageProps = $props() @@ -20,7 +20,11 @@ goto('/user')} right={[ - {icon: "i-material-symbols:settings-rounded", onclick: () => alert('Settings clicked')} + // {icon: "i-material-symbols:settings-rounded", onclick: () => alert('Settings clicked')} + // Language switching button + {icon: "i-material-symbols:translate-rounded", onclick: () => { + setLanguage(data.lang === 'en' ? 'zh' : 'en') + }} ]} />
From 5f83008ec0ab6c7316b094cc195da0f6a87d16c2 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:50:53 +0800 Subject: [PATCH 6/7] [+] Japanese --- src/app.d.ts | 2 +- src/hooks.server.ts | 4 +- src/lib/i18n/index.ts | 6 +- src/lib/i18n/ja.ts | 130 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 src/lib/i18n/ja.ts diff --git a/src/app.d.ts b/src/app.d.ts index 6ef9db7..518120a 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -4,7 +4,7 @@ declare global { namespace App { // interface Error {} interface Locals { - lang: 'en' | 'zh' + lang: 'en' | 'zh' | 'ja' } // interface PageData {} // interface PageState {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 3cadebf..4eb73fe 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -5,7 +5,7 @@ checkAudioSeparator().catch(e => console.error('Audio separator check failed:', export const handle: Handle = async ({ event, resolve }) => { const langCookie = event.cookies.get('lang'); - if (langCookie === 'zh' || langCookie === 'en') { + if (langCookie === 'zh' || langCookie === 'en' || langCookie === 'ja') { event.locals.lang = langCookie; } else { const acceptLanguage = event.request.headers.get('accept-language'); @@ -14,6 +14,8 @@ export const handle: Handle = async ({ event, resolve }) => { // 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 if (acceptLanguage && acceptLanguage.includes('ja')) { + event.locals.lang = 'ja'; } else { event.locals.lang = 'en'; } diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index 1d92106..50024eb 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -1,17 +1,19 @@ import { getContext, setContext } from 'svelte' import EN from "./en" import ZH from "./zh" +import JA from "./ja" // i18n related files: // src\hooks.server.ts // src\routes\+layout.server.ts -type Lang = 'en' | 'zh' +type Lang = 'en' | 'zh' | 'ja' const msgs: Record = { en: EN, - zh: ZH + zh: ZH, + ja: JA } const I18N_KEY = Symbol('i18n') diff --git a/src/lib/i18n/ja.ts b/src/lib/i18n/ja.ts new file mode 100644 index 0000000..f14d9c9 --- /dev/null +++ b/src/lib/i18n/ja.ts @@ -0,0 +1,130 @@ +export default { + home: { + titles: { + continue: '前回のセッションから続ける', + history: '履歴', + myPlaylists: 'マイプレイリスト', + recPlaylists: 'おすすめプレイリスト' + }, + btn: { + importFromNetease: 'NetEaseからインポート' + }, + text: { + playlistCreatedBy: '{u} 作成' + } + }, + admin: { + neteaseLogin: { + title: 'NetEaseログイン', + scanTitle: 'スキャンしてログイン', + scanTip: 'NetEase Musicアプリでスキャンしてください', + generating: 'QRコードを生成中...', + scanned: 'スキャン完了', + confirm: '携帯電話でログインを確認してください', + success: 'ログイン成功', + errorPrefix: 'エラー: ' + } + }, + import: { + netease: { + title: 'NetEaseからインポート', + status: { + importing: 'インポート中', + success: 'インポート完了', + error: 'インポート失敗' + }, + songs: '曲', + tip: 'NetEase Musicアプリでお気に入りの日本語プレイリストを見つけ、共有をクリックし、リンクをコピーしてここに貼り付けると、インポートを開始できます!', + inputLabel: 'NetEaseプレイリストリンク / ID', + btnStart: 'インポート開始', + btnView: 'プレイリストを表示' + } + }, + playlist: { + detail: { + title: 'プレイリスト詳細', + creator: '作成者: ', + count: '曲数: ', + startPractice: '練習開始', + songList: '曲リスト', + songs: '曲' + }, + list: { + mine: 'マイプレイリスト', + rec: 'おすすめプレイリスト', + import: 'NetEaseからインポート', + created: '{u} 作成', + count: '{n} 曲' + } + }, + results: { + title: '練習結果', + fields: { + speed: '速度', + accuracy: '正確率', + realtime: 'リアルタイム率', + count: '文字数', + time: '時間', + duration: '曲の長さ' + }, + units: { + cpm: 'CPM', + percent: '%', + x: 'x', + char: '文字' + }, + chart: { + speed: '速度 (CPM)', + accuracy: '正確率 (%)' + }, + btn: { + next: '次の曲', + back: 'プレイリストに戻る', + retry: 'もう一度' + } + }, + song: { + mode: { + typing: 'タイピングモード', + music: '音楽モード' + }, + karaoke: { + noVocals: 'ボーカル分離トラックが検出されないため、ボーカル音量を調整できません。まず曲の詳細ページで処理してください。' + }, + play: { + speed: '速度: ', + accuracy: '正確率: ', + stats: { + right: '正解:', + fuzzy: '曖昧:', + wrong: '不正解:', + remaining: '残り:' + } + } + }, + user: { + title: 'アカウント管理', + loginSuccess: { + title: 'ログイン成功', + content: 'ログインに成功しました!', + jump: '移動' + }, + generateCode: { + title: '引き継ぎコード生成', + copy: 'コピー', + success: '引き継ぎコードが生成されました!コードは:{code}', + expiry: 'このコードは使用後、または未使用の場合は7日後に無効になります。' + }, + desc: { + intro: 'このアプリは日本のモバイルゲームのように引き継ぎコードシステムを使用しており、メールアドレスやパスワードの登録は不要です。', + instruction: '別のデバイスでログインするには、「引き継ぎコード生成」をクリックし、ログインしたいデバイスで「ログイン」をクリックしてください。', + loginMode: '現在は「引き継ぎログイン」ページです。別のデバイスで引き継ぎコードを取得し、下の入力欄に入力して「ログイン」をクリックしてください。' + }, + input: '引き継ぎコードを入力', + btn: { + generate: '引き継ぎコード生成', + loginWithCode: '引き継ぎコードでログイン', + login: 'ログイン' + } + } +} From caad3818778388fba43cb8ba057a2bf172c4df46 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:54:55 +0800 Subject: [PATCH 7/7] [+] Multi-language switch --- src/lib/ui/appbar/AppBar.svelte | 5 +++-- src/routes/+page.svelte | 13 ++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib/ui/appbar/AppBar.svelte b/src/lib/ui/appbar/AppBar.svelte index fe5f1d2..c9975b3 100644 --- a/src/lib/ui/appbar/AppBar.svelte +++ b/src/lib/ui/appbar/AppBar.svelte @@ -8,12 +8,13 @@ onclick: () => void } - let { title, sub, account, right, children }: { + let { title, sub, account, right, children, moreIcon }: { title?: string sub?: string account?: () => void right?: Icon[] children?: any + moreIcon?: string } = $props() let showMenu = $state(false) @@ -37,7 +38,7 @@ {/each} {#if children} - showMenu = !showMenu} /> + showMenu = !showMenu} /> {/if}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index c5df11d..4ffc648 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -7,6 +7,7 @@ import { Layer } from "m3-svelte" import { goto } from "$app/navigation" import { getI18n, setLanguage } from "$lib/i18n" + import MenuItem from "$lib/ui/material3/MenuItem.svelte"; let { data }: PageProps = $props() @@ -19,13 +20,11 @@ - goto('/user')} right={[ - // {icon: "i-material-symbols:settings-rounded", onclick: () => alert('Settings clicked')} - // Language switching button - {icon: "i-material-symbols:translate-rounded", onclick: () => { - setLanguage(data.lang === 'en' ? 'zh' : 'en') - }} -]} /> + goto('/user')} moreIcon="i-material-symbols:translate-rounded"> + setLanguage('en')}>English + setLanguage('zh')}>中文 + setLanguage('ja')}>日本語 +
{#if data.last}