From 2ec9d048365525a45aed71fe3e50802caf8e7a7a Mon Sep 17 00:00:00 2001 From: 2570165831 <200382837+2570165831@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:20:07 +1100 Subject: [PATCH] feat(i18n): complete Chinese GUI statistics and social views Complete the Chinese browser catalog for statistics, sessions, listening parties and settings. Add whole-sentence Chinese ranking checks and translated browser fixtures, and mark the Chinese tables available across the interface. --- docs/configuration.md | 5 +- gui/e2e/localization.shot.ts | 154 +++++++++ gui/src/bindings/strings.json | 504 +++++++++++++++--------------- gui/src/connection.test.ts | 4 +- gui/src/localization.test.tsx | 48 ++- gui/src/mainLocalization.test.tsx | 7 +- src/core/i18n/gui/zh_hans.rs | 128 ++++++++ src/core/i18n/gui/zh_hant.rs | 126 ++++++++ src/core/i18n/mod.rs | 1 - src/gui/protocol.rs | 7 +- 10 files changed, 716 insertions(+), 268 deletions(-) create mode 100644 gui/e2e/localization.shot.ts diff --git a/docs/configuration.md b/docs/configuration.md index bfc4f99b6..33b8c7a93 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,15 +21,14 @@ Fatal errors include YAML syntax errors (the file cannot be parsed at all), and ## Interface language -Chinese tables follow in stacked translation PRs. The language choices are selectable now; areas without Chinese data display their English fallback. - `behavior.language` accepts `auto` (the default), `en`, `zh-Hans` (Simplified Chinese), and `zh-Hant` (Traditional Chinese). Common spellings such as `zh-CN`, `zh_CN`, `zh`, and `ZH-HANS` are accepted and saved canonically. Unknown values warn and fall back to `auto` without aborting configuration loading. Change it live in Settings; the terminal and browser/desktop frontends share the same table. The language picker displays `auto` as **Auto (system)** / **自动(跟随系统)** / **自動(跟隨系統)**, while the saved value stays -`auto`. CLI output and logs remain English. +`auto`. CLI output and logs remain English, and strings without a Chinese translation yet +display in English. `auto` reads the first nonempty `LC_ALL`, `LC_MESSAGES`, or `LANG`. Without one, macOS uses the first preferred `AppleLanguages` entry and Windows uses its UI diff --git a/gui/e2e/localization.shot.ts b/gui/e2e/localization.shot.ts new file mode 100644 index 000000000..9dc0f1e34 --- /dev/null +++ b/gui/e2e/localization.shot.ts @@ -0,0 +1,154 @@ +import { expect, test, type WebSocketRoute } from "@playwright/test"; +import type { ClientMessage } from "../src/bindings/ClientMessage"; +import type { ServerMessage } from "../src/bindings/ServerMessage"; +import strings from "../src/bindings/strings.json" with { type: "json" }; +import { openWith, shot } from "./bridge"; +import { + DEFAULT_LOOK, + hello, + onboarding, + playing, + library, + discover, + settings, +} from "./fixtures"; + +function settingsFor( + language: "en" | "zh-Hans" | "zh-Hant", + rev: number, +): ServerMessage { + const table = + language === "zh-Hans" + ? strings.ZH_HANS + : language === "zh-Hant" + ? strings.ZH_HANT + : strings.EN; + return { + kind: "settings", + rev, + payload: { + language, + strings: table, + visualizer: DEFAULT_LOOK, + rows: [ + { + id: "behavior.language", + name: + language === "en" + ? "Language" + : language === "zh-Hans" + ? "语言" + : "語言", + description: "", + group: "interface", + value: { + kind: "choice", + value: language, + options: ["auto", "en", "zh-Hans", "zh-Hant"], + labels: [ + table.settings.option_auto, + table.settings.option_en, + table.settings.option_zh_hans, + table.settings.option_zh_hant, + ], + }, + button: null, + }, + ], + }, + }; +} + +test("the Hello table localizes onboarding before App or Settings exists", async ({ + page, +}) => { + if (hello.kind !== "hello") throw new Error("hello fixture"); + await openWith(page, [ + { + ...hello, + payload: { + ...hello.payload, + language: "zh-Hant", + strings: strings.ZH_HANT, + }, + }, + onboarding, + ]); + await expect(page.getByRole("heading", { name: "選擇音源" })).toBeVisible(); + await expect(page.getByRole("button", { name: "繼續" })).toBeVisible(); + await expect(page.locator("html")).toHaveAttribute("lang", "zh-Hant"); + await shot(page, "onboarding-zh-Hant"); +}); + +test("a Settings push updates mounted memoized pages and keeps input and raw choices", async ({ + page, +}) => { + let socket: WebSocketRoute | undefined; + const sent: ClientMessage[] = []; + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + await page.routeWebSocket("**/ws", (ws) => { + socket = ws; + ws.onMessage((message) => + sent.push(JSON.parse(String(message)) as ClientMessage), + ); + for (const message of [ + ...playing, + ...library, + ...discover, + ...settings, + settingsFor("en", 2), + ]) + ws.send(JSON.stringify(message)); + }); + await page.goto("/#code=shot"); + await page + .getByRole("searchbox", { name: "Search", exact: true }) + .fill("{name} 五月天"); + await page.getByRole("button", { name: "Discover", exact: true }).click(); + await expect( + page.getByRole("heading", { name: "Discover", exact: true }), + ).toBeVisible(); + socket!.send(JSON.stringify(settingsFor("zh-Hans", 3))); + await expect( + page.getByRole("heading", { name: "发现", exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("searchbox", { name: "搜索", exact: true }), + ).toHaveValue("{name} 五月天"); + await expect( + page.getByRole("button", { name: "随机播放", exact: true }), + ).toBeVisible(); + await expect(page.locator("html")).toHaveAttribute("lang", "zh-Hans"); + await shot(page, "discover-zh-Hans"); + // A stale Settings message cannot roll the language or the table back. + socket!.send(JSON.stringify(settingsFor("en", 1))); + await expect( + page.getByRole("heading", { name: "发现", exact: true }), + ).toBeVisible(); + await page.getByRole("button", { name: "设置", exact: true }).click(); + await expect( + page.getByRole("heading", { name: "设置", exact: true }), + ).toBeVisible(); + socket!.send(JSON.stringify(settingsFor("zh-Hant", 4))); + await expect( + page.getByRole("heading", { name: "設定", exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("searchbox", { name: "搜尋", exact: true }), + ).toHaveValue("{name} 五月天"); + await page + .getByRole("combobox", { name: "語言", exact: true }) + .selectOption("en"); + await expect + .poll(() => sent) + .toContainEqual({ + type: "action", + action: { + SetSetting: { id: "behavior.language", value: { Text: "en" } }, + }, + }); + await expect(page.locator("html")).toHaveAttribute("lang", "zh-Hant"); + await shot(page, "settings-zh-Hant"); + expect(errors).toEqual([]); +}); diff --git a/gui/src/bindings/strings.json b/gui/src/bindings/strings.json index 0e5965f57..c919869fc 100644 --- a/gui/src/bindings/strings.json +++ b/gui/src/bindings/strings.json @@ -2080,7 +2080,7 @@ }, "ZH_HANS": { "gui": { - "a_song": "a song", + "a_song": "某首歌", "add_playlist": "添加到播放列表", "add_queue": "添加到队列", "album": "专辑", @@ -2105,14 +2105,14 @@ "cannot_sync": "无法同步", "change_visualizer": "点击切换可视化效果", "changelog": "更新日志", - "check_updates": "Check for updates", + "check_updates": "检查更新", "choose_sources": "选择音源", "choose_sources_hint": "全部不选则只设置 Spotify;之后可以添加或切换音源。", "close": "关闭", "close_now_playing": "关闭正在播放视图", "close_queue": "关闭队列", - "code": "Code", - "coming_up": "Coming up", + "code": "房间码", + "coming_up": "即将播放", "command": "命令", "command_device_usage": "device <名称>", "command_device_what": "在其他设备上播放", @@ -2135,7 +2135,7 @@ "commands_complete": "命令 · tab 补全", "compilation": "合辑", "connect_device": "连接设备", - "connecting": "Connecting…", + "connecting": "连接中…", "continue_button": "继续", "copied_album_link": "已复制专辑链接。", "copied_episode_link": "已复制单集链接。", @@ -2144,17 +2144,17 @@ "copy_album_link": "复制专辑链接", "copy_episode_link": "复制单集链接", "copy_episode_link_named": "复制 {name} 的单集链接", - "copy_hint": "y copy", + "copy_hint": "y 复制", "copy_link": "复制链接", "copy_link_failed": "无法复制链接。", - "copy_party_done": "Copied the party code.", - "copy_party_failed": "Could not copy the party code.", + "copy_party_done": "已复制房间码。", + "copy_party_failed": "无法复制房间码。", "copy_song_link": "复制歌曲链接", "create": "创建", "create_playlist": "创建播放列表", "current_device": "当前设备", "current_device_kind": "设备", - "date_locale": "en-US", + "date_locale": "zh-CN", "delete": "删除", "delete_library_body": "这将从你的音乐库删除 {name}。", "delete_library_question": "从音乐库删除?", @@ -2180,18 +2180,18 @@ "discover_intro": "Spotify 为你精选的歌单。", "discover_needs_spotify": "“发现”需要登录 Spotify。", "dismiss": "关闭", - "download": "Download", + "download": "下载", "downloading_percent": "正在下载 {version} 版({percent}%)", "downloading_version": "正在下载 {version} 版", - "each_own_copy": "Each listener plays their own copy", - "earlier": "Earlier", - "elapsed_in": "{time} in", - "end_party": "End party", - "end_party_label": "End the party", - "end_party_question": "End it for everyone?", + "each_own_copy": "每位听众都在自己的设备上播放", + "earlier": "之前播放", + "elapsed_in": "已播放 {time}", + "end_party": "结束一起听", + "end_party_label": "结束一起听", + "end_party_question": "为所有人结束一起听?", "episodes_count": "{count} 集", "episodes_of": "{name} 的单集", - "everyone_hears": "Everyone hears", + "everyone_hears": "大家正在听", "exit_full_screen": "退出全屏", "exit_mini_player": "退出迷你播放器", "explicit": "露骨内容", @@ -2201,33 +2201,33 @@ "filter_your_library": "筛选你的音乐库", "folder": "文件夹", "follow": "关注", - "follow_host": "You follow {name}", + "follow_host": "你正在跟随 {name}", "following": "已关注", "four_weeks": "4 周", "from_album": "来自 {album}", "from_your_queue": "来自你的队列", "full_screen": "全屏", - "gap": "Gap", - "gap_detail": "Nothing was recorded for about {minutes} min: playback stopped, or it played something the history does not keep, such as an episode.{after}", - "gap_minutes": "{minutes} min gap", - "getting_code": "Getting a code…", + "gap": "间隔", + "gap_detail": "约 {minutes} 分钟没有记录:播放已停止,或播放了历史记录不保留的内容,例如播客单集。{after}", + "gap_minutes": "间隔 {minutes} 分钟", + "getting_code": "正在获取房间码…", "go_album": "前往专辑", "go_artist": "前往艺人", "go_back": "返回", "go_forward": "前进", "go_song_radio": "前往歌曲电台", - "guest_controls": "Guests can control playback", - "guest_role": "guest", - "guests": "Guests", - "guests_follow": "guests follow", + "guest_controls": "允许听众控制播放", + "guest_role": "听众", + "guests": "听众", + "guests_follow": "听众跟随", "hint_back": "esc 返回", "hint_close": "esc 关闭", "hint_complete": "tab 补全", - "hint_copy_code": "y copy code", - "hint_end_party": "L end party", + "hint_copy_code": "y 复制房间码", + "hint_end_party": "L 结束一起听", "hint_export": "e 导出", - "hint_guest_control": "c guest control", - "hint_leave": "L leave", + "hint_guest_control": "c 听众控制", + "hint_leave": "L 离开", "hint_move": "j k 移动", "hint_period": "[ ] 时间段", "hint_pick": "↓ 后按 1–9 选择", @@ -2244,35 +2244,35 @@ "hint_search": "/ 搜索", "hint_sync": "s 全部同步", "history": "收听记录", - "history_not_loaded": "Your listening history is not loaded.", + "history_not_loaded": "尚未加载收听历史。", "home": "首页", - "host": "Host", - "host_listening": "You host · {count} listening", - "host_party": "Host a party", - "host_picks_songs": "The host picks the songs.", - "host_role": "host", - "hours_minutes": "{hours} hr {minutes} min", + "host": "房主", + "host_listening": "你是房主 · {count} 人正在听", + "host_party": "创建一起听房间", + "host_picks_songs": "歌曲由房主选择。", + "host_role": "房主", + "hours_minutes": "{hours} 小时 {minutes} 分钟", "hours_short": "{hours} 小时", - "in_last_seven_days": "in the last 7 days", - "in_last_thirty_days": "in the last 30 days", + "in_last_seven_days": "在最近 7 天", + "in_last_thirty_days": "在最近 30 天", "in_this_list": "列表中", "install_version": "重启以安装 {version} 版", - "integrations": "Integrations", - "interface": "Interface", - "join": "Join", - "join_party": "Join a party", + "integrations": "集成", + "interface": "界面", + "join": "加入", + "join_party": "加入房间", "keep_it": "保留", "label_shortcut": "{label}({key})", "language_auto": "跟随系统", "language_en": "English", "language_hans": "简体中文", "language_hant": "繁體中文", - "last_seven_days": "Last 7 days", + "last_seven_days": "最近 7 天", "last_sync": "上次同步", - "latest_version": "You have the latest version", + "latest_version": "你已安装最新版本", "latest_version_number": "你已安装最新版本({version})", "leave_hint": "退出", - "leave_party": "Leave party", + "leave_party": "离开房间", "library": "音乐库", "library_health": "音乐库状态", "like": "喜欢", @@ -2284,7 +2284,7 @@ "list": "列表", "listened": "已收听", "listening_history": "你的收听记录", - "listening_party": "Listening party", + "listening_party": "一起听", "live": "直播", "load_playlist_failed": "无法加载 {name}。请重新选择以重试。", "loaded_count": ",已加载 {count} 首", @@ -2298,31 +2298,31 @@ "loading_playlist": "正在加载 {name}…", "loading_recent": "正在加载最近播放…", "local_no_search": "本地音源不支持搜索 · Spotify", - "looking_updates": "Looking for updates…", + "looking_updates": "正在检查更新…", "lyrics": "歌词", "lyrics_hint": "歌词", "lyrics_not_synced": "歌词未同步", "lyrics_play_song": "暂无播放。播放歌曲后会在此显示歌词。", "made_for_you": "为你精选", - "marker_at": "{what} at {time}", - "marker_details": "Marker details", + "marker_at": "{time} {what}", + "marker_details": "标记详情", "matched_count": "{count} 首已匹配", "mini_player": "迷你播放器", - "minutes_count": "{count} minutes", - "minutes_short": "{minutes} min", + "minutes_count": "{count} 分钟", + "minutes_short": "{minutes} 分钟", "mirror_instructions": "包含 Qobuz、Subsonic 或 YouTube 的版本可同步播放列表:退出此窗口,在终端中选中播放列表并按 m。", "mirror_stays": "镜像播放列表会保留在原处。", "mirrored_playlists": "在不同音源间同步的播放列表", "missing_frontend": "此 spotatui-gui 版本未包含前端。请在 gui/ 中运行 `npm ci` 和 `npm run build`,然后重新构建 spotatui-gui。\n", - "more": "more", + "more": "首", "more_track_options": "{name} 的更多选项", "mute": "静音", "my_playlist": "我的播放列表", "name": "名称", - "name_placeholder": "Sam", + "name_placeholder": "小明", "never": "从未", "never_ran": "从未同步", - "new": "new", + "new": "新上榜", "next": "下一首", "next_from_spotify": "接下来来自:Spotify", "next_from_spotify_label": "接下来来自 Spotify", @@ -2334,14 +2334,14 @@ "no_library_matches": "没有符合“{query}”的结果。", "no_liked_songs": "还没有喜欢的歌曲。", "no_links": "没有关联的播放列表。", - "no_listening_history": "No listening history recorded yet. Play something!", + "no_listening_history": "还没有收听记录。播放点音乐吧!", "no_lyrics": "没有这首歌的歌词", "no_lyrics_sentence": "没有这首歌的歌词。", "no_match": "无匹配项", "no_mirrors": "无镜像列表", "no_other_devices": "未找到其他设备。", - "no_period_plays": "No plays in this period yet.", - "no_plays": "no plays", + "no_period_plays": "此时间段还没有播放记录。", + "no_plays": "未播放", "no_saved_albums": "没有收藏的专辑。", "no_saved_podcasts": "还没有收藏的播客。", "no_search_results": "未找到“{query}”的结果", @@ -2349,7 +2349,7 @@ "no_similar_songs": "暂时没有与 {name} 相似的歌曲。", "no_songs": "还没有歌曲。", "no_songs_match": "没有符合“{filter}”的歌曲。", - "nothing_finished": "Nothing has finished playing yet.", + "nothing_finished": "尚未播放完任何歌曲。", "nothing_here": "这里还没有内容。", "nothing_is_playing": "暂无播放。", "nothing_played": "还没有播放记录。", @@ -2357,28 +2357,28 @@ "nothing_queued": "队列为空。", "nothing_synced": "尚未同步任何内容。", "now_playing": "正在播放", - "now_playing_index": "Now playing · Song {index} of {count}", + "now_playing_index": "正在播放 · 第 {index} 首,共 {count} 首", "now_playing_view": "正在播放视图", - "of_all_time": "of all time", + "of_all_time": "全部时间", "on_platter_next": "接下来播放的专辑", "one_episode": "1 集", "one_song": "1 首歌", - "one_song_count": "{count} song", - "one_source": "{count} source", - "only_you": "only you", + "one_song_count": "{count} 首歌", + "one_source": "{count} 个音源", + "only_you": "仅自己", "open_browser": "在浏览器中打开 {url}", "open_mini_player": "打开迷你播放器", "page_disconnected": "此页面已与 spotatui 断开连接。", "party": "一起听", - "party_host_intro": "You get a code to share. Guests hear what you play on Spotify.", - "party_intro": "Listen together: everyone plays the same Spotify song at the same moment.", - "party_needs_spotify": "A listening party needs a Spotify session.", + "party_host_intro": "你会获得一个可分享的房间码。听众会同步收听你播放的 Spotify 歌曲。", + "party_intro": "一起听:大家同时播放同一首 Spotify 歌曲。", + "party_needs_spotify": "一起听需要登录 Spotify。", "pause": "暂停", - "pause_detail": "The song was paused for about {minutes} min in total.", - "paused_about": "Paused about {minutes} min", - "paused_during": "Paused during {title}", - "paused_minutes": "paused {minutes} min", - "period": "Period", + "pause_detail": "歌曲累计暂停了约 {minutes} 分钟。", + "paused_about": "暂停约 {minutes} 分钟", + "paused_during": "播放 {title} 时暂停", + "paused_minutes": "暂停了 {minutes} 分钟", + "period": "时间段", "period_export_hint": "[ ] 切换时间段 · e 导出", "play": "播放", "play_album_here": "播放专辑后会在此显示。", @@ -2386,9 +2386,9 @@ "play_named": "播放 {name}", "play_newest_episode": "播放最新一集", "play_spotify_link": "播放此 Spotify 链接", - "playback": "Playback", - "played": "Played", - "played_from": " {title} played from {source}.", + "playback": "播放", + "played": "已播放", + "played_from": "{title} 来自 {source}。", "playing_album_not_loaded": "尚未加载这首歌的专辑。", "playing_here": "正在此设备上播放", "playing_label": "正在播放:", @@ -2400,10 +2400,10 @@ "playlist_one_mirror": "{source} 播放列表 · {count} 个镜像", "playlist_sync": "播放列表同步", "playlists": "播放列表", - "plays_all_time": "plays, all time", - "plays_from": "plays from {source}", - "plays_seven_days": "plays in the last 7 days", - "plays_thirty_days": "plays in the last 30 days", + "plays_all_time": "累计播放次数", + "plays_from": "播放自 {source}", + "plays_seven_days": "最近 7 天播放次数", + "plays_thirty_days": "最近 30 天播放次数", "podcast": "播客", "podcast_by": "播客 · {publisher}", "podcast_episode": "播客单集", @@ -2416,13 +2416,13 @@ "queue_keys": "J K 移动 · x 移除", "radio_station": "广播电台", "range": "时间范围", - "rank_climbed": "{name} climbed from #{from} all time to {highlight} {period}.", - "rank_down": "▼ {count} from #{rank}", - "rank_fell": "{name} fell from #{from} to {highlight} {period}.", - "rank_new": "{name} is {highlight} in your top 10 {period}.", - "rank_new_marker": "new", - "rank_no_plays": "{name}, your #{from} all time, has {highlight} {period}.", - "rank_up": "▲ {count} from #{rank}", + "rank_climbed": "{period},{name} 从总榜第 {from} 升至{highlight}。", + "rank_down": "▼ {count},原第 {rank}", + "rank_fell": "{period},{name} 从总榜第 {from} 降至{highlight}。", + "rank_new": "{period},{name} {highlight}进入你的前十名。", + "rank_new_marker": "首次", + "rank_no_plays": "{name} 在总榜第 {from},{period}{highlight}。", + "rank_up": "▲ {count},原第 {rank}", "recent_commands": "最近使用 · ↑", "recent_needs_spotify": "最近播放需要连接 Spotify。", "recently_played": "最近播放", @@ -2438,10 +2438,10 @@ "repeat": "循环播放", "repeat_one": "单曲循环", "replay_hint": "enter 从此处重播", - "restart_update": "Restart to update", + "restart_update": "重启以更新", "result_type": "结果类型", "results": "结果", - "room_code_suffix": " · room {code}", + "room_code_suffix": " · 房间 {code}", "row_menu_hint": "右键或 Shift+F10 查看更多选项", "save_liked": "保存到喜欢的歌曲", "save_to_library": "收藏到音乐库", @@ -2460,8 +2460,8 @@ "search_settings": "搜索设置", "searching": "搜索中…", "seconds_short": "{seconds} 秒", - "secret_not_set": "Not set", - "secret_saved": "Saved", + "secret_not_set": "未设置", + "secret_saved": "已保存", "sections": "分区", "see_more": "查看更多", "seek": "播放进度", @@ -2471,7 +2471,7 @@ "session": "本次播放", "session_keys": "enter 从此处重播 · J K 排序 · x 移除", "settings": "设置", - "seven_days": "7 days", + "seven_days": "7 天", "share": "分享", "show": "显示", "show_all": "查看全部", @@ -2484,7 +2484,7 @@ "side_a": "A 面", "side_b": "B 面", "side_panel": "侧栏", - "since_time": "since {time}", + "since_time": "从 {time} 开始", "single": "单曲", "six_months": "6 个月", "song": "歌曲", @@ -2494,7 +2494,7 @@ "songs": "歌曲", "songs_count": "{count} 首歌", "songs_count_more": "{count}+ 首歌", - "songs_duration": "{songs}, {time}", + "songs_duration": "{songs},{time}", "sort_album": "专辑(A-Z)", "sort_album_desc": "专辑(Z-A)", "sort_albums": "专辑排序", @@ -2516,8 +2516,8 @@ "sort_title_desc": "标题(Z-A)", "source": "音源", "source_apple_note": "macOS“音乐”应用,需要自动化权限", - "source_change": "Source change", - "source_detail": "{from} → {to}.{after}", + "source_change": "音源切换", + "source_detail": "{from} → {to}。{after}", "source_free": "免费", "source_local": "本地", "source_local_badge": "本地", @@ -2527,67 +2527,67 @@ "source_radio_badge": "电台", "source_subsonic_note": "免费,需要 Subsonic/Navidrome 服务器", "source_youtube_note": "免费,需要 yt-dlp 程序", - "sources": "Sources", - "sources_count": "{count} sources", + "sources": "音源", + "sources_count": "{count} 个音源", "spotatui_pages": "spotatui 页面", - "spotify_follow_only": "Guests follow Spotify songs only. They keep the last one until you play Spotify again.", + "spotify_follow_only": "听众只能跟随 Spotify 歌曲。你再次播放 Spotify 前,他们会继续播放上一首歌。", "spotify_login": "登录 Spotify", "spotify_not_connected": "Spotify 未连接。", "spotify_songs_count": "来自 Spotify 的 {count} 首歌曲。", "spotify_tracklist_only": "此处显示 Spotify 专辑曲目。", "start_failed": "spotatui 无法启动:{error}", - "start_party": "Start a party", + "start_party": "创建房间", "stats": "统计", - "stats_spotify_only": "Only Spotify tracks play from Stats", - "stopped_following": "You stopped following the host: your own playback has the speaker. Play a Spotify song or wait for the next one to follow again.", - "streaming": "Streaming", + "stats_spotify_only": "统计页只能播放 Spotify 歌曲", + "stopped_following": "你已停止跟随房主,正在播放自己的音乐。播放 Spotify 歌曲或等待下一首即可重新跟随。", + "streaming": "流媒体", "sync_all": "立即全部同步", "sync_master": "主列表", "synced": "已同步", "syncing": "同步中…", - "the_host": "the host", - "then_album": " · then {name}", - "then_played": " Then {title} played.", - "thirty_days": "30 days", + "the_host": "房主", + "then_album": " · 随后播放 {name}", + "then_played": "随后播放了 {title}。", + "thirty_days": "30 天", "this_list": "此列表", - "this_month": "This month", - "this_month_phrase": "this month", - "this_session": "This session", - "this_year": "This year", - "this_year_phrase": "this year", - "time_left": "{time} left", - "timeline": "Timeline", + "this_month": "本月", + "this_month_phrase": "本月", + "this_session": "本次播放", + "this_year": "今年", + "this_year_phrase": "今年", + "time_left": "剩余 {time}", + "timeline": "时间线", "timing_estimated": "时间为估算值", "title": "标题", - "top_albums": "Top albums", - "top_artists": "Top artists", + "top_albums": "最常听专辑", + "top_artists": "最常听艺人", "top_artists_mix": "最常听艺人混合歌单", "top_result": "最佳结果", - "top_tracks": "Top tracks", + "top_tracks": "最常听歌曲", "top_tracks_range": "你的最常听歌曲,{range}", "track_options": "{name} 的选项", "tracklist": "曲目列表", - "try_again": "Try again", + "try_again": "重试", "unmatched": "未匹配", "unmatched_count": "{count} 首未匹配", "unmute": "取消静音", "up_next": "接下来播放", - "update_checks_description": "Skip the checks at launch and every 6 hours; check, download and install updates from this page yourself", - "update_delay_description": "How long a release must be out before it downloads: '0' for at once, or for example '10m', '2h', '7d'", - "update_ready": "Update ready", - "updates": "Updates", + "update_checks_description": "跳过启动时及每 6 小时的自动检查;改为在此页手动检查、下载和安装更新", + "update_delay_description": "新版本发布后等待多久再下载:'0' 表示立即,也可填 '10m'、'2h'、'7d'", + "update_ready": "更新已就绪", + "updates": "更新", "version_available": "有新版本 {version}", "version_manual": "{version} 版已发布。请从发布页面下载。", "version_ready": "{version} 版已就绪", "volume": "音量", - "vs_all_time": "vs all time", - "week_top_five": "This week, top five", + "vs_all_time": "对比全部时间", + "week_top_five": "本周前五", "yes": "是", - "you": "You", - "you_hear": "You hear", + "you": "你", + "you_hear": "你正在听", "your_library": "你的音乐库", "your_music": "你的音乐", - "your_name": "Your name", + "your_name": "你的名字", "your_queue": "你的队列", "your_top_tracks": "你的最常听歌曲" }, @@ -4159,7 +4159,7 @@ }, "ZH_HANT": { "gui": { - "a_song": "a song", + "a_song": "某首歌", "add_playlist": "加入播放清單", "add_queue": "加入佇列", "album": "專輯", @@ -4184,14 +4184,14 @@ "cannot_sync": "無法同步", "change_visualizer": "點擊切換視覺化效果", "changelog": "更新紀錄", - "check_updates": "Check for updates", + "check_updates": "檢查更新", "choose_sources": "選擇音源", "choose_sources_hint": "全部不選則只設定 Spotify;之後可以新增或切換來源。", "close": "關閉", "close_now_playing": "關閉正在播放畫面", "close_queue": "關閉佇列", - "code": "Code", - "coming_up": "Coming up", + "code": "房間碼", + "coming_up": "即將播放", "command": "指令", "command_device_usage": "device <名稱>", "command_device_what": "在其他裝置上播放", @@ -4214,7 +4214,7 @@ "commands_complete": "指令 · tab 補全", "compilation": "合輯", "connect_device": "連線至裝置", - "connecting": "Connecting…", + "connecting": "連線中…", "continue_button": "繼續", "copied_album_link": "已複製專輯連結。", "copied_episode_link": "已複製單集連結。", @@ -4223,17 +4223,17 @@ "copy_album_link": "複製專輯連結", "copy_episode_link": "複製單集連結", "copy_episode_link_named": "複製 {name} 的單集連結", - "copy_hint": "y copy", + "copy_hint": "y 複製", "copy_link": "複製連結", "copy_link_failed": "無法複製連結。", - "copy_party_done": "Copied the party code.", - "copy_party_failed": "Could not copy the party code.", + "copy_party_done": "已複製房間碼。", + "copy_party_failed": "無法複製房間碼。", "copy_song_link": "複製歌曲連結", "create": "建立", "create_playlist": "建立播放清單", "current_device": "目前裝置", "current_device_kind": "裝置", - "date_locale": "en-US", + "date_locale": "zh-TW", "delete": "刪除", "delete_library_body": "這將從你的音樂庫刪除 {name}。", "delete_library_question": "從音樂庫刪除?", @@ -4259,18 +4259,18 @@ "discover_intro": "Spotify 為你精選的播放清單。", "discover_needs_spotify": "「探索」需要登入 Spotify。", "dismiss": "關閉", - "download": "Download", + "download": "下載", "downloading_percent": "正在下載 {version} 版({percent}%)", "downloading_version": "正在下載 {version} 版", - "each_own_copy": "Each listener plays their own copy", - "earlier": "Earlier", - "elapsed_in": "{time} in", - "end_party": "End party", - "end_party_label": "End the party", - "end_party_question": "End it for everyone?", + "each_own_copy": "每位聽眾都在自己的裝置上播放", + "earlier": "之前播放", + "elapsed_in": "已播放 {time}", + "end_party": "結束一起聽", + "end_party_label": "結束一起聽", + "end_party_question": "為所有人結束一起聽?", "episodes_count": "{count} 集", "episodes_of": "{name} 的單集", - "everyone_hears": "Everyone hears", + "everyone_hears": "大家正在聽", "exit_full_screen": "離開全螢幕", "exit_mini_player": "離開迷你播放器", "explicit": "露骨內容", @@ -4280,33 +4280,33 @@ "filter_your_library": "篩選你的音樂庫", "folder": "資料夾", "follow": "追蹤", - "follow_host": "You follow {name}", + "follow_host": "你正在跟隨 {name}", "following": "追蹤中", "four_weeks": "4 週", "from_album": "來自 {album}", "from_your_queue": "來自你的佇列", "full_screen": "全螢幕", - "gap": "Gap", - "gap_detail": "Nothing was recorded for about {minutes} min: playback stopped, or it played something the history does not keep, such as an episode.{after}", - "gap_minutes": "{minutes} min gap", - "getting_code": "Getting a code…", + "gap": "間隔", + "gap_detail": "約 {minutes} 分鐘沒有紀錄:播放已停止,或播放了歷史紀錄不保留的內容,例如 Podcast 單集。{after}", + "gap_minutes": "間隔 {minutes} 分鐘", + "getting_code": "正在取得房間碼…", "go_album": "前往專輯", "go_artist": "前往藝人", "go_back": "返回", "go_forward": "前進", "go_song_radio": "前往歌曲電台", - "guest_controls": "Guests can control playback", - "guest_role": "guest", - "guests": "Guests", - "guests_follow": "guests follow", + "guest_controls": "允許聽眾控制播放", + "guest_role": "聽眾", + "guests": "聽眾", + "guests_follow": "聽眾跟隨", "hint_back": "esc 返回", "hint_close": "esc 關閉", "hint_complete": "tab 補全", - "hint_copy_code": "y copy code", - "hint_end_party": "L end party", + "hint_copy_code": "y 複製房間碼", + "hint_end_party": "L 結束一起聽", "hint_export": "e 匯出", - "hint_guest_control": "c guest control", - "hint_leave": "L leave", + "hint_guest_control": "c 聽眾控制", + "hint_leave": "L 離開", "hint_move": "j k 移動", "hint_period": "[ ] 期間", "hint_pick": "↓ 後按 1–9 選擇", @@ -4323,35 +4323,35 @@ "hint_search": "/ 搜尋", "hint_sync": "s 全部同步", "history": "聆聽紀錄", - "history_not_loaded": "Your listening history is not loaded.", + "history_not_loaded": "尚未載入聆聽紀錄。", "home": "首頁", - "host": "Host", - "host_listening": "You host · {count} listening", - "host_party": "Host a party", - "host_picks_songs": "The host picks the songs.", - "host_role": "host", - "hours_minutes": "{hours} hr {minutes} min", + "host": "房主", + "host_listening": "你是房主 · {count} 人正在聽", + "host_party": "建立一起聽房間", + "host_picks_songs": "歌曲由房主選擇。", + "host_role": "房主", + "hours_minutes": "{hours} 小時 {minutes} 分鐘", "hours_short": "{hours} 小時", - "in_last_seven_days": "in the last 7 days", - "in_last_thirty_days": "in the last 30 days", + "in_last_seven_days": "在最近 7 天", + "in_last_thirty_days": "在最近 30 天", "in_this_list": "清單中", "install_version": "重新啟動以安裝 {version} 版", - "integrations": "Integrations", - "interface": "Interface", - "join": "Join", - "join_party": "Join a party", + "integrations": "整合", + "interface": "介面", + "join": "加入", + "join_party": "加入房間", "keep_it": "保留", "label_shortcut": "{label}({key})", "language_auto": "跟隨系統", "language_en": "English", "language_hans": "简体中文", "language_hant": "繁體中文", - "last_seven_days": "Last 7 days", + "last_seven_days": "最近 7 天", "last_sync": "上次同步", - "latest_version": "You have the latest version", + "latest_version": "你已安裝最新版本", "latest_version_number": "你已安裝最新版本({version})", "leave_hint": "離開", - "leave_party": "Leave party", + "leave_party": "離開房間", "library": "音樂庫", "library_health": "音樂庫狀態", "like": "喜歡", @@ -4363,7 +4363,7 @@ "list": "清單", "listened": "已聆聽", "listening_history": "你的聆聽紀錄", - "listening_party": "Listening party", + "listening_party": "一起聽", "live": "直播", "load_playlist_failed": "無法載入 {name}。請重新選擇以重試。", "loaded_count": ",已載入 {count} 首", @@ -4377,31 +4377,31 @@ "loading_playlist": "正在載入 {name}…", "loading_recent": "正在載入最近播放…", "local_no_search": "本機來源不支援搜尋 · Spotify", - "looking_updates": "Looking for updates…", + "looking_updates": "正在檢查更新…", "lyrics": "歌詞", "lyrics_hint": "歌詞", "lyrics_not_synced": "歌詞未同步", "lyrics_play_song": "目前沒有播放。播放歌曲後會在此顯示歌詞。", "made_for_you": "為你精選", - "marker_at": "{what} at {time}", - "marker_details": "Marker details", + "marker_at": "{time} {what}", + "marker_details": "標記詳情", "matched_count": "{count} 首已配對", "mini_player": "迷你播放器", - "minutes_count": "{count} minutes", - "minutes_short": "{minutes} min", + "minutes_count": "{count} 分鐘", + "minutes_short": "{minutes} 分鐘", "mirror_instructions": "包含 Qobuz、Subsonic 或 YouTube 的版本可同步播放清單:離開此視窗,在終端機中選取播放清單並按 m。", "mirror_stays": "鏡像播放清單會保留在原處。", "mirrored_playlists": "在不同來源間同步的播放清單", "missing_frontend": "此 spotatui-gui 版本未包含前端。請在 gui/ 中執行 `npm ci` 和 `npm run build`,然後重新建置 spotatui-gui。\n", - "more": "more", + "more": "首", "more_track_options": "{name} 的更多選項", "mute": "靜音", "my_playlist": "我的播放清單", "name": "名稱", - "name_placeholder": "Sam", + "name_placeholder": "小明", "never": "從未", "never_ran": "從未同步", - "new": "new", + "new": "新上榜", "next": "下一首", "next_from_spotify": "接下來來自:Spotify", "next_from_spotify_label": "接下來來自 Spotify", @@ -4413,14 +4413,14 @@ "no_library_matches": "沒有符合「{query}」的結果。", "no_liked_songs": "還沒有喜歡的歌曲。", "no_links": "沒有連結的播放清單。", - "no_listening_history": "No listening history recorded yet. Play something!", + "no_listening_history": "還沒有聆聽紀錄。播放點音樂吧!", "no_lyrics": "沒有這首歌的歌詞", "no_lyrics_sentence": "沒有這首歌的歌詞。", "no_match": "沒有配對項目", "no_mirrors": "沒有鏡像清單", "no_other_devices": "找不到其他裝置。", - "no_period_plays": "No plays in this period yet.", - "no_plays": "no plays", + "no_period_plays": "此期間還沒有播放紀錄。", + "no_plays": "未播放", "no_saved_albums": "沒有收藏的專輯。", "no_saved_podcasts": "還沒有收藏的 Podcast。", "no_search_results": "找不到「{query}」的結果", @@ -4428,7 +4428,7 @@ "no_similar_songs": "暫時沒有與 {name} 相似的歌曲。", "no_songs": "還沒有歌曲。", "no_songs_match": "沒有符合「{filter}」的歌曲。", - "nothing_finished": "Nothing has finished playing yet.", + "nothing_finished": "尚未播放完任何歌曲。", "nothing_here": "這裡還沒有內容。", "nothing_is_playing": "目前沒有播放。", "nothing_played": "還沒有播放紀錄。", @@ -4436,28 +4436,28 @@ "nothing_queued": "佇列是空的。", "nothing_synced": "尚未同步任何內容。", "now_playing": "正在播放", - "now_playing_index": "Now playing · Song {index} of {count}", + "now_playing_index": "正在播放 · 第 {index} 首,共 {count} 首", "now_playing_view": "正在播放畫面", - "of_all_time": "of all time", + "of_all_time": "全部時間", "on_platter_next": "接下來播放的專輯", "one_episode": "1 集", "one_song": "1 首歌", - "one_song_count": "{count} song", - "one_source": "{count} source", - "only_you": "only you", + "one_song_count": "{count} 首歌", + "one_source": "{count} 個來源", + "only_you": "僅自己", "open_browser": "在瀏覽器中開啟 {url}", "open_mini_player": "開啟迷你播放器", "page_disconnected": "此頁面已與 spotatui 中斷連線。", "party": "一起聽", - "party_host_intro": "You get a code to share. Guests hear what you play on Spotify.", - "party_intro": "Listen together: everyone plays the same Spotify song at the same moment.", - "party_needs_spotify": "A listening party needs a Spotify session.", + "party_host_intro": "你會取得一組可分享的房間碼。聽眾會同步收聽你播放的 Spotify 歌曲。", + "party_intro": "一起聽:大家同時播放同一首 Spotify 歌曲。", + "party_needs_spotify": "一起聽需要登入 Spotify。", "pause": "暫停", - "pause_detail": "The song was paused for about {minutes} min in total.", - "paused_about": "Paused about {minutes} min", - "paused_during": "Paused during {title}", - "paused_minutes": "paused {minutes} min", - "period": "Period", + "pause_detail": "歌曲累計暫停了約 {minutes} 分鐘。", + "paused_about": "暫停約 {minutes} 分鐘", + "paused_during": "播放 {title} 時暫停", + "paused_minutes": "暫停了 {minutes} 分鐘", + "period": "期間", "period_export_hint": "[ ] 切換期間 · e 匯出", "play": "播放", "play_album_here": "播放專輯後會在此顯示。", @@ -4465,9 +4465,9 @@ "play_named": "播放 {name}", "play_newest_episode": "播放最新一集", "play_spotify_link": "播放此 Spotify 連結", - "playback": "Playback", - "played": "Played", - "played_from": " {title} played from {source}.", + "playback": "播放", + "played": "已播放", + "played_from": "{title} 來自 {source}。", "playing_album_not_loaded": "尚未載入這首歌的專輯。", "playing_here": "正在此裝置上播放", "playing_label": "正在播放:", @@ -4479,10 +4479,10 @@ "playlist_one_mirror": "{source} 播放清單 · {count} 個鏡像", "playlist_sync": "播放清單同步", "playlists": "播放清單", - "plays_all_time": "plays, all time", - "plays_from": "plays from {source}", - "plays_seven_days": "plays in the last 7 days", - "plays_thirty_days": "plays in the last 30 days", + "plays_all_time": "累計播放次數", + "plays_from": "播放自 {source}", + "plays_seven_days": "最近 7 天播放次數", + "plays_thirty_days": "最近 30 天播放次數", "podcast": "Podcast", "podcast_by": "Podcast · {publisher}", "podcast_episode": "Podcast 單集", @@ -4495,13 +4495,13 @@ "queue_keys": "J K 移動 · x 移除", "radio_station": "廣播電台", "range": "時間範圍", - "rank_climbed": "{name} climbed from #{from} all time to {highlight} {period}.", - "rank_down": "▼ {count} from #{rank}", - "rank_fell": "{name} fell from #{from} to {highlight} {period}.", - "rank_new": "{name} is {highlight} in your top 10 {period}.", - "rank_new_marker": "new", - "rank_no_plays": "{name}, your #{from} all time, has {highlight} {period}.", - "rank_up": "▲ {count} from #{rank}", + "rank_climbed": "{period},{name} 從總榜第 {from} 升至{highlight}。", + "rank_down": "▼ {count},原第 {rank}", + "rank_fell": "{period},{name} 從總榜第 {from} 降至{highlight}。", + "rank_new": "{period},{name} {highlight}進入你的前十名。", + "rank_new_marker": "首次", + "rank_no_plays": "{name} 在總榜第 {from},{period}{highlight}。", + "rank_up": "▲ {count},原第 {rank}", "recent_commands": "最近使用 · ↑", "recent_needs_spotify": "最近播放需要連線至 Spotify。", "recently_played": "最近播放", @@ -4517,10 +4517,10 @@ "repeat": "循環播放", "repeat_one": "單曲循環", "replay_hint": "enter 從此處重播", - "restart_update": "Restart to update", + "restart_update": "重新啟動以更新", "result_type": "結果類型", "results": "結果", - "room_code_suffix": " · room {code}", + "room_code_suffix": " · 房間 {code}", "row_menu_hint": "按右鍵或 Shift+F10 查看更多選項", "save_liked": "儲存到喜歡的歌曲", "save_to_library": "收藏到音樂庫", @@ -4539,8 +4539,8 @@ "search_settings": "搜尋設定", "searching": "搜尋中…", "seconds_short": "{seconds} 秒", - "secret_not_set": "Not set", - "secret_saved": "Saved", + "secret_not_set": "未設定", + "secret_saved": "已儲存", "sections": "區段", "see_more": "查看更多", "seek": "播放進度", @@ -4550,7 +4550,7 @@ "session": "本次播放", "session_keys": "enter 從此處重播 · J K 排序 · x 移除", "settings": "設定", - "seven_days": "7 days", + "seven_days": "7 天", "share": "分享", "show": "顯示", "show_all": "查看全部", @@ -4563,7 +4563,7 @@ "side_a": "A 面", "side_b": "B 面", "side_panel": "側邊欄", - "since_time": "since {time}", + "since_time": "從 {time} 開始", "single": "單曲", "six_months": "6 個月", "song": "歌曲", @@ -4573,7 +4573,7 @@ "songs": "歌曲", "songs_count": "{count} 首歌", "songs_count_more": "{count}+ 首歌", - "songs_duration": "{songs}, {time}", + "songs_duration": "{songs},{time}", "sort_album": "專輯(A-Z)", "sort_album_desc": "專輯(Z-A)", "sort_albums": "專輯排序", @@ -4595,8 +4595,8 @@ "sort_title_desc": "標題(Z-A)", "source": "來源", "source_apple_note": "macOS「音樂」App,需要自動化權限", - "source_change": "Source change", - "source_detail": "{from} → {to}.{after}", + "source_change": "來源切換", + "source_detail": "{from} → {to}。{after}", "source_free": "免費", "source_local": "本機", "source_local_badge": "本機", @@ -4606,67 +4606,67 @@ "source_radio_badge": "電台", "source_subsonic_note": "免費,需要 Subsonic/Navidrome 伺服器", "source_youtube_note": "免費,需要 yt-dlp 程式", - "sources": "Sources", - "sources_count": "{count} sources", + "sources": "來源", + "sources_count": "{count} 個來源", "spotatui_pages": "spotatui 頁面", - "spotify_follow_only": "Guests follow Spotify songs only. They keep the last one until you play Spotify again.", + "spotify_follow_only": "聽眾只能跟隨 Spotify 歌曲。你再次播放 Spotify 前,他們會繼續播放上一首歌。", "spotify_login": "登入 Spotify", "spotify_not_connected": "Spotify 未連線。", "spotify_songs_count": "來自 Spotify 的 {count} 首歌曲。", "spotify_tracklist_only": "此處顯示 Spotify 專輯曲目。", "start_failed": "spotatui 無法啟動:{error}", - "start_party": "Start a party", + "start_party": "建立房間", "stats": "統計", - "stats_spotify_only": "Only Spotify tracks play from Stats", - "stopped_following": "You stopped following the host: your own playback has the speaker. Play a Spotify song or wait for the next one to follow again.", - "streaming": "Streaming", + "stats_spotify_only": "統計頁只能播放 Spotify 歌曲", + "stopped_following": "你已停止跟隨房主,正在播放自己的音樂。播放 Spotify 歌曲或等待下一首即可重新跟隨。", + "streaming": "串流", "sync_all": "立即全部同步", "sync_master": "主清單", "synced": "已同步", "syncing": "同步中…", - "the_host": "the host", - "then_album": " · then {name}", - "then_played": " Then {title} played.", - "thirty_days": "30 days", + "the_host": "房主", + "then_album": " · 接著播放 {name}", + "then_played": "接著播放了 {title}。", + "thirty_days": "30 天", "this_list": "此清單", - "this_month": "This month", - "this_month_phrase": "this month", - "this_session": "This session", - "this_year": "This year", - "this_year_phrase": "this year", - "time_left": "{time} left", - "timeline": "Timeline", + "this_month": "本月", + "this_month_phrase": "本月", + "this_session": "本次播放", + "this_year": "今年", + "this_year_phrase": "今年", + "time_left": "剩餘 {time}", + "timeline": "時間軸", "timing_estimated": "時間為估計值", "title": "標題", - "top_albums": "Top albums", - "top_artists": "Top artists", + "top_albums": "最常聽專輯", + "top_artists": "最常聽藝人", "top_artists_mix": "最常聽藝人綜合播放清單", "top_result": "最佳結果", - "top_tracks": "Top tracks", + "top_tracks": "最常聽歌曲", "top_tracks_range": "你的最常聽歌曲,{range}", "track_options": "{name} 的選項", "tracklist": "曲目清單", - "try_again": "Try again", + "try_again": "重試", "unmatched": "未配對", "unmatched_count": "{count} 首未配對", "unmute": "取消靜音", "up_next": "接下來播放", - "update_checks_description": "Skip the checks at launch and every 6 hours; check, download and install updates from this page yourself", - "update_delay_description": "How long a release must be out before it downloads: '0' for at once, or for example '10m', '2h', '7d'", - "update_ready": "Update ready", - "updates": "Updates", + "update_checks_description": "略過啟動時及每 6 小時的自動檢查;改為在此頁手動檢查、下載和安裝更新", + "update_delay_description": "新版本發布後等待多久再下載:'0' 表示立即,也可填 '10m'、'2h'、'7d'", + "update_ready": "更新已就緒", + "updates": "更新", "version_available": "有新版本 {version}", "version_manual": "{version} 版已發布。請從發布頁面下載。", "version_ready": "{version} 版已就緒", "volume": "音量", - "vs_all_time": "vs all time", - "week_top_five": "This week, top five", + "vs_all_time": "與全部時間相比", + "week_top_five": "本週前五", "yes": "是", - "you": "You", - "you_hear": "You hear", + "you": "你", + "you_hear": "你正在聽", "your_library": "你的音樂庫", "your_music": "你的音樂", - "your_name": "Your name", + "your_name": "你的名字", "your_queue": "你的佇列", "your_top_tracks": "你的最常聽歌曲" }, diff --git a/gui/src/connection.test.ts b/gui/src/connection.test.ts index 1dc966dac..1df3793ba 100644 --- a/gui/src/connection.test.ts +++ b/gui/src/connection.test.ts @@ -143,7 +143,7 @@ describe("Connection", () => { expect(connection.getState().booted).toBe(false); expect( connection.getState().localization?.strings.gui.continue_button, - ).toBe(strings.ZH_HANT.gui.continue_button); + ).toBe("繼續"); receive({ kind: "settings", rev: 3, @@ -166,7 +166,7 @@ describe("Connection", () => { }); const active = connection.getState().localization; expect(active?.language).toBe("zh-Hans"); - expect(active?.strings.gui.settings).toBe(strings.ZH_HANS.gui.settings); + expect(active?.strings.gui.settings).toBe("设置"); receive({ kind: "settings", rev: 4, diff --git a/gui/src/localization.test.tsx b/gui/src/localization.test.tsx index 162b21a70..5e519fc9c 100644 --- a/gui/src/localization.test.tsx +++ b/gui/src/localization.test.tsx @@ -9,9 +9,11 @@ import { SettingControl } from "./Settings"; import { SourceBadge } from "./SourceBadge"; import { byName } from "./testTree"; import { TopBar } from "./TopBar"; +import { headline as partyHeadline } from "./partyModel"; import { songCount } from "./libraryModel"; import { sourceOf } from "./format"; import { parse } from "./commandModel"; +import { sentence } from "./statsModel"; describe("shared browser strings", () => { it("generates exactly the Rust-serialized tables, including English punctuation and spaces", () => { @@ -36,14 +38,34 @@ describe("shared browser strings", () => { } }); + it("places the period before the Chinese ranking sentence and keeps literal artist braces", () => { + expect( + sentence( + { name: "{period} 五月天", kind: "climb", from: 9, to: 4 }, + "30d", + ZH_HANS, + ).join(""), + ).toBe("在最近 30 天,{period} 五月天 从总榜第 9 升至#4。"); + expect( + sentence( + { name: "五月天", kind: "new", from: null, to: 4 }, + "30d", + ZH_HANT, + ).join(""), + ).toBe("在最近 30 天,五月天 首次進入你的前十名。"); + }); + it("never replaces placeholder-looking text inside a user value", () => { + expect(fmt(ZH_HANS.play_named, { name: "{count} <&> 五月天" })).toBe( + "播放 {count} <&> 五月天", + ); expect(fmt("{name}: {count}", { name: "{count}", count: 2 })).toBe( "{count}: 2", ); expect(fmt("{unknown}", {})).toBe("{unknown}"); }); - it("renders active accessibility text and unchanged product and command ids", () => { + it("renders both scripts, localized accessibility text, and unchanged product and command ids", () => { for (const s of [ZH_HANS, ZH_HANT]) { const html = renderToStaticMarkup( { expect( renderToStaticMarkup(), ).toContain("SPOTIFY"); - expect(songCount(2, s)).toBe(fmt(s.songs_count, { count: 2 })); + expect(songCount(2, s)).toBe("2 首歌"); } expect(sourceOf("file:///a.flac")).toBe("Local"); expect(parse("source Local")).toEqual({ @@ -94,8 +116,8 @@ describe("shared browser strings", () => { onReply={() => {}} />, ); - expect(html).toContain(strings.ZH_HANT.gui.choose_sources); - expect(html).toContain(strings.ZH_HANT.gui.continue_button); + expect(html).toContain("選擇音源"); + expect(html).toContain("繼續"); expect(html).toContain("Spotify"); }); @@ -129,4 +151,22 @@ describe("shared browser strings", () => { { SetSetting: { id: "behavior.language", value: { Text: "zh-Hant" } } }, ]); }); + + it("keeps a room host's literal braces and code intact", () => { + const line = partyHeadline( + { + phase: "joined", + available: true, + room: { + host: false, + host_name: "{code}", + code: "ABC123", + guests: [], + shared_control: false, + }, + }, + ZH_HANT, + ); + expect(line).toBe("你正在跟隨 {code} · 房間 ABC123"); + }); }); diff --git a/gui/src/mainLocalization.test.tsx b/gui/src/mainLocalization.test.tsx index 7f7e0a129..551f19044 100644 --- a/gui/src/mainLocalization.test.tsx +++ b/gui/src/mainLocalization.test.tsx @@ -242,7 +242,7 @@ describe("main additions in the shared languages", () => { ); expect(html).toContain(fmt(s.song_radio_named, { name: "{name} 歌" })); expect(html).toContain(s.song_radio); - + expect(html).not.toContain("Song radio"); const recent = renderToStaticMarkup( { expect(localized.map((x) => x.value)).toEqual( english.map((x) => x.value), ); + expect(localized.every((x, i) => x.label !== english[i].label)).toBe( + true, + ); } } expect(sortOptions(EN)[2]).toEqual({ value: "name:desc", label: "Title (Z-A)", }); + expect(sortOptions(ZH_HANS)[2].label).toBe("标题(Z-A)"); + expect(sortOptions(ZH_HANT)[2].label).toBe("標題(Z-A)"); }); it("reports a clipboard failure in the selected language", async () => { diff --git a/src/core/i18n/gui/zh_hans.rs b/src/core/i18n/gui/zh_hans.rs index eb101e242..6339458cc 100644 --- a/src/core/i18n/gui/zh_hans.rs +++ b/src/core/i18n/gui/zh_hans.rs @@ -4,6 +4,9 @@ use super::{GuiStrings, EN}; #[allow(clippy::needless_update)] pub const ZH_HANS: GuiStrings = GuiStrings { explicit: "露骨内容", + secret_not_set: "未设置", + secret_saved: "已保存", + sources: "音源", by_spotify: "由 Spotify 提供", made_for_you: "为你精选", home: "首页", @@ -199,6 +202,94 @@ pub const ZH_HANS: GuiStrings = GuiStrings { unmatched: "未匹配", no_match: "无匹配项", cannot_sync: "无法同步", + listening_party: "一起听", + connecting: "连接中…", + party_intro: "一起听:大家同时播放同一首 Spotify 歌曲。", + party_needs_spotify: "一起听需要登录 Spotify。", + host_party: "创建一起听房间", + host: "房主", + join: "加入", + start_party: "创建房间", + party_host_intro: "你会获得一个可分享的房间码。听众会同步收听你播放的 Spotify 歌曲。", + join_party: "加入房间", + code: "房间码", + your_name: "你的名字", + name_placeholder: "小明", + copy_party_failed: "无法复制房间码。", + copy_party_done: "已复制房间码。", + copy_hint: "y 复制", + getting_code: "正在获取房间码…", + end_party_label: "结束一起听", + end_party_question: "为所有人结束一起听?", + end_party: "结束一起听", + leave_party: "离开房间", + everyone_hears: "大家正在听", + you_hear: "你正在听", + spotify_follow_only: "听众只能跟随 Spotify 歌曲。你再次播放 Spotify 前,他们会继续播放上一首歌。", + stopped_following: + "你已停止跟随房主,正在播放自己的音乐。播放 Spotify 歌曲或等待下一首即可重新跟随。", + each_own_copy: "每位听众都在自己的设备上播放", + guest_controls: "允许听众控制播放", + host_picks_songs: "歌曲由房主选择。", + guests: "听众", + only_you: "仅自己", + guests_follow: "听众跟随", + hint_copy_code: "y 复制房间码", + hint_guest_control: "c 听众控制", + hint_end_party: "L 结束一起听", + hint_leave: "L 离开", + the_host: "房主", + you: "你", + host_role: "房主", + guest_role: "听众", + this_session: "本次播放", + timeline: "时间线", + played: "已播放", + more: "首", + coming_up: "即将播放", + marker_details: "标记详情", + earlier: "之前播放", + nothing_finished: "尚未播放完任何歌曲。", + a_song: "某首歌", + source_change: "音源切换", + gap: "间隔", + period: "时间段", + history_not_loaded: "尚未加载收听历史。", + no_listening_history: "还没有收听记录。播放点音乐吧!", + no_period_plays: "此时间段还没有播放记录。", + top_artists: "最常听艺人", + vs_all_time: "对比全部时间", + top_albums: "最常听专辑", + top_tracks: "最常听歌曲", + last_seven_days: "最近 7 天", + week_top_five: "本周前五", + stats_spotify_only: "统计页只能播放 Spotify 歌曲", + seven_days: "7 天", + thirty_days: "30 天", + this_month: "本月", + this_year: "今年", + in_last_seven_days: "在最近 7 天", + in_last_thirty_days: "在最近 30 天", + this_month_phrase: "本月", + this_year_phrase: "今年", + of_all_time: "全部时间", + plays_seven_days: "最近 7 天播放次数", + plays_thirty_days: "最近 30 天播放次数", + plays_all_time: "累计播放次数", + new: "新上榜", + no_plays: "未播放", + interface: "界面", + playback: "播放", + streaming: "流媒体", + integrations: "集成", + updates: "更新", + latest_version: "你已安装最新版本", + check_updates: "检查更新", + looking_updates: "正在检查更新…", + download: "下载", + restart_update: "重启以更新", + try_again: "重试", + update_ready: "更新已就绪", add_playlist: "添加到播放列表", remove_your_liked: "从你喜欢的歌曲中移除", save_your_liked: "保存到你喜欢的歌曲", @@ -230,6 +321,8 @@ pub const ZH_HANS: GuiStrings = GuiStrings { language_en: "English", language_hans: "简体中文", language_hant: "繁體中文", + update_checks_description: "跳过启动时及每 6 小时的自动检查;改为在此页手动检查、下载和安装更新", + update_delay_description: "新版本发布后等待多久再下载:'0' 表示立即,也可填 '10m'、'2h'、'7d'", play_named: "播放 {name}", by_owner: "创建者:{owner}", album_by: "专辑 · {artists}", @@ -263,6 +356,40 @@ pub const ZH_HANS: GuiStrings = GuiStrings { playlist_one_mirror: "{source} 播放列表 · {count} 个镜像", playlist_mirrors: "{source} 播放列表 · {count} 个镜像", search_failed: "搜索失败:{error}", + host_listening: "你是房主 · {count} 人正在听", + follow_host: "你正在跟随 {name}", + room_code_suffix: " · 房间 {code}", + minutes_count: "{count} 分钟", + plays_from: "播放自 {source}", + now_playing_index: "正在播放 · 第 {index} 首,共 {count} 首", + elapsed_in: "已播放 {time}", + time_left: "剩余 {time}", + then_album: " · 随后播放 {name}", + since_time: "从 {time} 开始", + one_source: "{count} 个音源", + sources_count: "{count} 个音源", + one_song_count: "{count} 首歌", + minutes_short: "{minutes} 分钟", + hours_minutes: "{hours} 小时 {minutes} 分钟", + songs_duration: "{songs},{time}", + gap_minutes: "间隔 {minutes} 分钟", + paused_minutes: "暂停了 {minutes} 分钟", + paused_about: "暂停约 {minutes} 分钟", + marker_at: "{time} {what}", + paused_during: "播放 {title} 时暂停", + pause_detail: "歌曲累计暂停了约 {minutes} 分钟。", + gap_detail: + "约 {minutes} 分钟没有记录:播放已停止,或播放了历史记录不保留的内容,例如播客单集。{after}", + then_played: "随后播放了 {title}。", + source_detail: "{from} → {to}。{after}", + played_from: "{title} 来自 {source}。", + rank_up: "▲ {count},原第 {rank}", + rank_down: "▼ {count},原第 {rank}", + rank_climbed: "{period},{name} 从总榜第 {from} 升至{highlight}。", + rank_no_plays: "{name} 在总榜第 {from},{period}{highlight}。", + rank_fell: "{period},{name} 从总榜第 {from} 降至{highlight}。", + rank_new: "{period},{name} {highlight}进入你的前十名。", + date_locale: "zh-CN", source_needs_login: "需要登录", source_free: "免费", source_youtube_note: "免费,需要 yt-dlp 程序", @@ -279,6 +406,7 @@ pub const ZH_HANS: GuiStrings = GuiStrings { command_device_usage: "device <名称>", command_party_usage: "party start | join <房间码> <名字> | leave", command_quit_usage: "quit", + rank_new_marker: "首次", library: "音乐库", remove_from_library: "从音乐库移除", copy_album_link: "复制专辑链接", diff --git a/src/core/i18n/gui/zh_hant.rs b/src/core/i18n/gui/zh_hant.rs index 02fd1ffb9..c0b995a85 100644 --- a/src/core/i18n/gui/zh_hant.rs +++ b/src/core/i18n/gui/zh_hant.rs @@ -4,6 +4,9 @@ use super::{GuiStrings, EN}; #[allow(clippy::needless_update)] pub const ZH_HANT: GuiStrings = GuiStrings { explicit: "露骨內容", + secret_not_set: "未設定", + secret_saved: "已儲存", + sources: "來源", by_spotify: "由 Spotify 提供", made_for_you: "為你精選", home: "首頁", @@ -198,6 +201,93 @@ pub const ZH_HANT: GuiStrings = GuiStrings { unmatched: "未配對", no_match: "沒有配對項目", cannot_sync: "無法同步", + listening_party: "一起聽", + connecting: "連線中…", + party_intro: "一起聽:大家同時播放同一首 Spotify 歌曲。", + party_needs_spotify: "一起聽需要登入 Spotify。", + host_party: "建立一起聽房間", + host: "房主", + join: "加入", + start_party: "建立房間", + party_host_intro: "你會取得一組可分享的房間碼。聽眾會同步收聽你播放的 Spotify 歌曲。", + join_party: "加入房間", + code: "房間碼", + your_name: "你的名字", + name_placeholder: "小明", + copy_party_failed: "無法複製房間碼。", + copy_party_done: "已複製房間碼。", + copy_hint: "y 複製", + getting_code: "正在取得房間碼…", + end_party_label: "結束一起聽", + end_party_question: "為所有人結束一起聽?", + end_party: "結束一起聽", + leave_party: "離開房間", + everyone_hears: "大家正在聽", + you_hear: "你正在聽", + spotify_follow_only: "聽眾只能跟隨 Spotify 歌曲。你再次播放 Spotify 前,他們會繼續播放上一首歌。", + stopped_following: "你已停止跟隨房主,正在播放自己的音樂。播放 Spotify 歌曲或等待下一首即可重新跟隨。", + each_own_copy: "每位聽眾都在自己的裝置上播放", + guest_controls: "允許聽眾控制播放", + host_picks_songs: "歌曲由房主選擇。", + guests: "聽眾", + only_you: "僅自己", + guests_follow: "聽眾跟隨", + hint_copy_code: "y 複製房間碼", + hint_guest_control: "c 聽眾控制", + hint_end_party: "L 結束一起聽", + hint_leave: "L 離開", + the_host: "房主", + you: "你", + host_role: "房主", + guest_role: "聽眾", + this_session: "本次播放", + timeline: "時間軸", + played: "已播放", + more: "首", + coming_up: "即將播放", + marker_details: "標記詳情", + earlier: "之前播放", + nothing_finished: "尚未播放完任何歌曲。", + a_song: "某首歌", + source_change: "來源切換", + gap: "間隔", + period: "期間", + history_not_loaded: "尚未載入聆聽紀錄。", + no_listening_history: "還沒有聆聽紀錄。播放點音樂吧!", + no_period_plays: "此期間還沒有播放紀錄。", + top_artists: "最常聽藝人", + vs_all_time: "與全部時間相比", + top_albums: "最常聽專輯", + top_tracks: "最常聽歌曲", + last_seven_days: "最近 7 天", + week_top_five: "本週前五", + stats_spotify_only: "統計頁只能播放 Spotify 歌曲", + seven_days: "7 天", + thirty_days: "30 天", + this_month: "本月", + this_year: "今年", + in_last_seven_days: "在最近 7 天", + in_last_thirty_days: "在最近 30 天", + this_month_phrase: "本月", + this_year_phrase: "今年", + of_all_time: "全部時間", + plays_seven_days: "最近 7 天播放次數", + plays_thirty_days: "最近 30 天播放次數", + plays_all_time: "累計播放次數", + new: "新上榜", + no_plays: "未播放", + interface: "介面", + playback: "播放", + streaming: "串流", + integrations: "整合", + updates: "更新", + latest_version: "你已安裝最新版本", + check_updates: "檢查更新", + looking_updates: "正在檢查更新…", + download: "下載", + restart_update: "重新啟動以更新", + try_again: "重試", + update_ready: "更新已就緒", add_playlist: "加入播放清單", remove_your_liked: "從你喜歡的歌曲中移除", save_your_liked: "儲存到你喜歡的歌曲", @@ -229,6 +319,8 @@ pub const ZH_HANT: GuiStrings = GuiStrings { language_en: "English", language_hans: "简体中文", language_hant: "繁體中文", + update_checks_description: "略過啟動時及每 6 小時的自動檢查;改為在此頁手動檢查、下載和安裝更新", + update_delay_description: "新版本發布後等待多久再下載:'0' 表示立即,也可填 '10m'、'2h'、'7d'", play_named: "播放 {name}", by_owner: "建立者:{owner}", album_by: "專輯 · {artists}", @@ -262,6 +354,39 @@ pub const ZH_HANT: GuiStrings = GuiStrings { playlist_one_mirror: "{source} 播放清單 · {count} 個鏡像", playlist_mirrors: "{source} 播放清單 · {count} 個鏡像", search_failed: "搜尋失敗:{error}", + host_listening: "你是房主 · {count} 人正在聽", + follow_host: "你正在跟隨 {name}", + room_code_suffix: " · 房間 {code}", + minutes_count: "{count} 分鐘", + plays_from: "播放自 {source}", + now_playing_index: "正在播放 · 第 {index} 首,共 {count} 首", + elapsed_in: "已播放 {time}", + time_left: "剩餘 {time}", + then_album: " · 接著播放 {name}", + since_time: "從 {time} 開始", + one_source: "{count} 個來源", + sources_count: "{count} 個來源", + one_song_count: "{count} 首歌", + minutes_short: "{minutes} 分鐘", + hours_minutes: "{hours} 小時 {minutes} 分鐘", + songs_duration: "{songs},{time}", + gap_minutes: "間隔 {minutes} 分鐘", + paused_minutes: "暫停了 {minutes} 分鐘", + paused_about: "暫停約 {minutes} 分鐘", + marker_at: "{time} {what}", + paused_during: "播放 {title} 時暫停", + pause_detail: "歌曲累計暫停了約 {minutes} 分鐘。", + gap_detail: "約 {minutes} 分鐘沒有紀錄:播放已停止,或播放了歷史紀錄不保留的內容,例如 Podcast 單集。{after}", + then_played: "接著播放了 {title}。", + source_detail: "{from} → {to}。{after}", + played_from: "{title} 來自 {source}。", + rank_up: "▲ {count},原第 {rank}", + rank_down: "▼ {count},原第 {rank}", + rank_climbed: "{period},{name} 從總榜第 {from} 升至{highlight}。", + rank_no_plays: "{name} 在總榜第 {from},{period}{highlight}。", + rank_fell: "{period},{name} 從總榜第 {from} 降至{highlight}。", + rank_new: "{period},{name} {highlight}進入你的前十名。", + date_locale: "zh-TW", source_needs_login: "需要登入", source_free: "免費", source_youtube_note: "免費,需要 yt-dlp 程式", @@ -278,6 +403,7 @@ pub const ZH_HANT: GuiStrings = GuiStrings { command_device_usage: "device <名稱>", command_party_usage: "party start | join <房間碼> <名字> | leave", command_quit_usage: "quit", + rank_new_marker: "首次", library: "音樂庫", remove_from_library: "從音樂庫移除", copy_album_link: "複製專輯連結", diff --git a/src/core/i18n/mod.rs b/src/core/i18n/mod.rs index e12ae3031..70a01a039 100644 --- a/src/core/i18n/mod.rs +++ b/src/core/i18n/mod.rs @@ -1,6 +1,5 @@ //! Frontend-neutral, serializable UI messages. Add English fields first; each //! translated instance inherits untranslated fields from its English instance. -//! Chinese tables follow in stacked translation PRs; missing areas use English. pub mod gui; pub mod help; mod locale; diff --git a/src/gui/protocol.rs b/src/gui/protocol.rs index ec3f54f01..10fdc4a78 100644 --- a/src/gui/protocol.rs +++ b/src/gui/protocol.rs @@ -2806,10 +2806,7 @@ mod tests { app.refresh_language(); let changed = pushed(&diff(&before, &app, Language::En), "settings"); assert_eq!(changed["payload"]["language"], "zh-Hant"); - assert_eq!( - changed["payload"]["strings"]["gui"]["settings"], - app.strings().gui.settings - ); + assert_eq!(changed["payload"]["strings"]["gui"]["settings"], "設定"); let language = changed["payload"]["rows"] .as_array() .unwrap() @@ -2865,7 +2862,7 @@ mod tests { assert_eq!(message["payload"]["language"], "zh-Hans"); assert_eq!( message["payload"]["strings"]["gui"]["continue_button"], - crate::core::i18n::ZH_HANS.gui.continue_button + "继续" ); }