diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index e5a6807..46380ac 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -1,4 +1,3 @@ - export default { home: { titles: { @@ -13,5 +12,119 @@ export default { 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/zh.ts b/src/lib/i18n/zh.ts index 261c042..d648260 100644 --- a/src/lib/i18n/zh.ts +++ b/src/lib/i18n/zh.ts @@ -1,4 +1,3 @@ - export default { home: { titles: { @@ -13,5 +12,119 @@ export default { text: { playlistCreatedBy: '{u} 创建' } + }, + admin: { + neteaseLogin: { + title: '网易云登录', + scanTitle: '扫码登录', + scanTip: '请使用网易云音乐 APP 扫码', + generating: '正在生成二维码...', + scanned: '已扫描', + confirm: '请在手机上确认登录', + success: '登录成功', + errorPrefix: '错误: ' + } + }, + import: { + netease: { + title: '从网易云导入', + status: { + importing: '正在导入', + success: '导入完成', + error: '导入出错' + }, + songs: '首歌曲', + tip: '去网易云 APP 找一个你喜欢的日本语歌单,点击分享,再点击复制链接,然后把链接粘贴到这里就可以开始导入了!', + inputLabel: '网易云歌单链接 / ID', + btnStart: '开始导入', + btnView: '查看歌单' + } + }, + playlist: { + detail: { + title: '歌单详情', + creator: '创建者: ', + count: '歌曲数: ', + startPractice: '开始练习', + songList: '歌曲列表', + songs: '首歌曲' + }, + list: { + mine: '我的歌单', + rec: '推荐歌单', + import: '从网易云导入', + 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: '这个 App 像日本的手机游戏一样采用引继码系统管理账号,并不需要用邮箱密码注册帐号。', + instruction: '如果想要在另一个设备上登录账号,请点击「生成引继码」然后在想要登录的设备上点击「登录」', + loginMode: '您当前在「引继登录」页面,请在另一个设备上获取引继码之后输入到下面的输入框内点击「登录」' + }, + input: '输入引继码', + btn: { + generate: '生成引继码', + loginWithCode: '用引继码登录', + login: '登录' + } } } \ No newline at end of file diff --git a/src/routes/admin/netease-login/+page.svelte b/src/routes/admin/netease-login/+page.svelte index 4931051..45f8750 100644 --- a/src/routes/admin/netease-login/+page.svelte +++ b/src/routes/admin/netease-login/+page.svelte @@ -12,7 +12,9 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an import { API } from '$lib/client'; import { fade, scale } from 'svelte/transition'; import AppBar from "$lib/ui/appbar/AppBar.svelte"; - import { Layer } from "m3-svelte"; + import { getI18n } from "$lib/i18n"; + + const t = getI18n().admin.neteaseLogin; let status = $state<'loading' | 'waiting_scan' | 'waiting_confirm' | 'success' | 'error'>('loading'); let qrImg = $state(''); @@ -53,20 +55,20 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an }); - +
-

扫码登录

-

请使用网易云音乐 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}