diff --git a/src/app.d.ts b/src/app.d.ts index da08e6d..518120a 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' | 'ja' + } + // 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..4eb73fe 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,3 +1,27 @@ 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' || langCookie === 'ja') { + 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 if (acceptLanguage && acceptLanguage.includes('ja')) { + event.locals.lang = 'ja'; + } else { + event.locals.lang = 'en'; + } + } + + return resolve(event, { + transformPageChunk: ({ html }) => html.replace('%lang%', event.locals.lang) + }); +}; diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts new file mode 100644 index 0000000..048ca18 --- /dev/null +++ b/src/lib/i18n/en.ts @@ -0,0 +1,130 @@ +export default { + home: { + titles: { + continue: 'Continue From Last Session', + history: 'History', + myPlaylists: 'My Playlists', + recPlaylists: 'Recommended Playlists' + }, + btn: { + importFromNetease: 'Import from NetEase' + }, + text: { + playlistCreatedBy: 'From {u}' + } + }, + admin: { + neteaseLogin: { + title: 'NetEase Login', + scanTitle: 'Scan to Login', + scanTip: 'Please use NetEase Music App to scan', + generating: 'Generating QR Code...', + scanned: 'Scanned', + confirm: 'Please confirm login on your phone', + success: 'Login Successful', + errorPrefix: 'Error: ' + } + }, + import: { + netease: { + title: 'Import from NetEase', + status: { + importing: 'Importing', + success: 'Import Complete', + error: 'Import Failed' + }, + songs: 'songs', + tip: 'Go to NetEase Music App, find a Japanese playlist you like, click share, copy link, and paste it here to start importing!', + inputLabel: 'NetEase Playlist Link / ID', + btnStart: 'Start Import', + btnView: 'View Playlist' + } + }, + playlist: { + detail: { + title: 'Playlist Details', + creator: 'Creator: ', + count: 'Songs: ', + startPractice: 'Start Practice', + songList: 'Song List', + songs: 'songs' + }, + list: { + mine: 'My Playlists', + rec: 'Recommended Playlists', + import: 'Import from NetEase', + created: 'Created by {u}', + count: '{n} songs' + } + }, + results: { + title: 'Practice Results', + fields: { + speed: 'Speed', + accuracy: 'Accuracy', + realtime: 'Realtime Rate', + count: 'Count', + time: 'Time', + duration: 'Duration' + }, + units: { + cpm: 'CPM', + percent: '%', + x: 'x', + char: 'chars' + }, + chart: { + speed: 'Speed (CPM)', + accuracy: 'Accuracy (%)' + }, + btn: { + next: 'Next Song', + back: 'Back to Playlist', + retry: 'Retry' + } + }, + song: { + mode: { + typing: 'Typing Mode', + music: 'Music Mode' + }, + karaoke: { + noVocals: 'No vocal separation track detected, cannot adjust vocal volume. Please process in song details page first.' + }, + play: { + speed: 'Speed: ', + accuracy: 'Accuracy: ', + stats: { + right: 'Right: ', + fuzzy: 'Fuzzy: ', + wrong: 'Wrong: ', + remaining: 'Remaining: ' + } + } + }, + user: { + title: 'Account Management', + loginSuccess: { + title: 'Login Successful', + content: 'Login Successful!', + jump: 'Jump' + }, + generateCode: { + title: 'Generate Sync Code', + copy: 'Copy', + success: 'Sync Code Generated! Code is: {code}', + expiry: 'This code will expire after use or in 7 days if unused.' + }, + desc: { + intro: 'This App uses a sync code system like Japanese mobile games, no email/password registration needed.', + instruction: 'To login on another device, click "Generate Sync Code" then click "Login" on the target device.', + loginMode: 'You are in "Sync Login" page. Get a code from another device and enter it below to login.' + }, + input: 'Enter Sync Code', + btn: { + generate: 'Generate Sync Code', + loginWithCode: 'Login with Sync Code', + login: 'Login' + } + } +} \ 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..50024eb --- /dev/null +++ b/src/lib/i18n/index.ts @@ -0,0 +1,46 @@ +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' | 'ja' + +const msgs: Record请使用网易云音乐 APP 扫码
+{t.scanTip}
正在生成二维码...
+{t.generating}
已扫描
-请在手机上确认登录
+{t.scanned}
+{t.confirm}
登录成功
+{t.success}
错误: {errorMessage}
+{t.errorPrefix}{errorMessage}