Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
884 changes: 442 additions & 442 deletions gui/src/bindings/strings.json

Large diffs are not rendered by default.

72 changes: 70 additions & 2 deletions src/core/i18n/recap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,79 @@ pub const EN: RecapStrings = RecapStrings {

// Keep ..EN so new English fields compile before their translations are added.
#[allow(clippy::needless_update)]
pub const ZH_HANS: RecapStrings = RecapStrings { ..EN };
pub const ZH_HANS: RecapStrings = RecapStrings {
language: "zh-Hans",
page_title: "spotatui 听歌回顾",
title: "听歌记录回顾",
cover_art: "封面",
first_track: "最常听歌曲",
qualified_plays: "有效播放次数",
listening_time: "听歌时长",
top_artist: "最常听艺人",
top_album: "最常听专辑",
generated_by: "生成自 ",
generated_by_product: "由 spotatui 生成",
download: "下载分享卡片",
analytics: "详细统计",
top_tracks: "最常听歌曲",
top_artists: "最常听艺人",
top_albums: "最常听专辑",
recent_plays: "最近播放",
by_day: "每日听歌时长",
by_hour: "各时段听歌时长",
footer: "回顾根据 spotatui 本地听歌记录生成,从启用此功能时开始统计。短时播放或跳过的歌曲会被记录,但不计入回顾的主要统计。",
generating: "正在生成…",
render_failed: "无法生成图片,请重试!",
empty_summary: "此时间段还没有有效的本地听歌记录。",
summary: "由 spotatui 直接生成的本地听歌回顾,可作为 HTML 页面分享。",
no_tracks: "暂无歌曲。",
no_artists: "暂无艺人。",
no_albums: "暂无专辑。",
no_recent: "暂无最近播放记录。",
no_data_yet: "暂无数据。",
no_data: "暂无数据",
listened_at_template: "播放于 {time}",
recent_date_format: "%m月%d日 %H:%M",
..EN
};

// Keep ..EN so new English fields compile before their translations are added.
#[allow(clippy::needless_update)]
pub const ZH_HANT: RecapStrings = RecapStrings { ..EN };
pub const ZH_HANT: RecapStrings = RecapStrings {
language: "zh-Hant",
page_title: "spotatui 聆聽回顧",
title: "聆聽紀錄回顧",
cover_art: "封面",
first_track: "最常聽歌曲",
qualified_plays: "有效播放次數",
listening_time: "聆聽時間",
top_artist: "最常聽藝人",
top_album: "最常聽專輯",
generated_by: "產生自 ",
generated_by_product: "由 spotatui 產生",
download: "下載分享卡片",
analytics: "詳細統計",
top_tracks: "最常聽歌曲",
top_artists: "最常聽藝人",
top_albums: "最常聽專輯",
recent_plays: "最近播放",
by_day: "每日聆聽時間",
by_hour: "各時段聆聽時間",
footer: "回顧根據 spotatui 本機聆聽紀錄產生,從啟用此功能時開始統計。短暫播放或跳過的歌曲會被記錄,但不計入回顧的主要統計。",
generating: "正在產生…",
render_failed: "無法產生圖片,請重試!",
empty_summary: "此時間範圍還沒有有效的本機聆聽紀錄。",
summary: "由 spotatui 直接產生的本機聆聽回顧,可作為 HTML 頁面分享。",
no_tracks: "尚無歌曲。",
no_artists: "尚無藝人。",
no_albums: "尚無專輯。",
no_recent: "尚無最近播放紀錄。",
no_data_yet: "尚無資料。",
no_data: "尚無資料",
listened_at_template: "播放於 {time}",
recent_date_format: "%m月%d日 %H:%M",
..EN
};

impl RecapStrings {
pub fn listened_at(&self, time: &str) -> String {
Expand Down
254 changes: 254 additions & 0 deletions src/core/i18n/status_infra.rs

Large diffs are not rendered by default.

134 changes: 132 additions & 2 deletions src/core/i18n/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,140 @@ pub const EN: ToolStrings = ToolStrings {
};
// Keep ..EN so new English fields compile before their translations are added.
#[allow(clippy::needless_update)]
pub const ZH_HANS: ToolStrings = ToolStrings { ..EN };
pub const ZH_HANS: ToolStrings = ToolStrings {
brief_artists: "最常听艺人",
brief_tracks: "最常听歌曲",
brief_albums: "最常听专辑",
brief_recent: "最近播放(不要重复)",
brief_now: "\n正在播放:{name}\n",
brief_vibe: "\n听众想听:{name}\n",
period_week: "最近 7 天",
period_month: "最近 30 天",
period_this_month: "本月",
period_year: "今年",
period_all: "全部时间",
ownership_incomplete: "\n\n提示:播放列表索引仍在建立,本次搜索的 [已收藏] 仅表示喜欢的歌曲。稍后重新搜索可获得完整结果。",
queue_not_found: "\n\n未找到(已跳过):{names}",
queue_owned: "\n已在音乐库中(因 exclude_owned 跳过):{names}",
queue_duplicate: "\n已在队列中、正在播放或最近播放过(已跳过):{names}",
queue_filter_spotify_only: "\nexclude_owned 仅检查 Spotify 歌曲,本次未跳过任何歌曲。",
none_found: "未找到:{names}",
none_owned: "已在音乐库中:{names}",
none_duplicate: "已在队列中、正在播放或最近播放过:{names}",
none_new: "所有歌曲都已在队列中或正在播放。",
skipped: "已跳至下一首",
vibe_set: "DJ 氛围已设为:{vibe}。{effect}",
vibe_cleared: "已清除 DJ 氛围",
vibe_on: "内置 DJ 将从下次补充队列起遵循此氛围。",
vibe_off: "内置 DJ 的自动补充队列已关闭,开启后才会生效;目前请手动加入歌曲。get_listening_history 仍会返回此氛围。",
vibe_unavailable: "此版本没有内置自动补充队列 DJ,不会自动应用此氛围。氛围已保存,可由 get_listening_history 读取后用于加入歌曲。",
creating: "正在创建播放列表“{name}”,包含 {count} 首歌曲。Spotify 创建后即可由 list_playlists 查看。",
adding: "正在向 {playlist} 添加 {count} 首歌曲",
removing: "正在从 {playlist} 的位置 {position} 移除 {uri}(仅在曲目相符时移除)",
following: "正在关注 spotify:artist:{artist}",
unfollowing: "正在取消关注 spotify:artist:{artist}",
transfer: "正在将播放切换至 {name}",
liking: "正在喜欢 {changed} 首歌曲;{unchanged} 首已喜欢",
unliking: "正在取消喜欢 {changed} 首歌曲;{unchanged} 首原本未喜欢",
search_failed: "搜索失败:{error}",
no_tracks: "未找到与“{query}”相关的歌曲",
found_track: "{name} — {artists} [{uri}] [{owned}]",
owned: "已收藏",
new_track: "未收藏",
queued: "已加入 {count} 首歌曲:",
queue_empty: "队列为空",
nothing_playing: "没有正在播放的内容(队列中有 {count} 首歌曲)",
now_playing: "{state}:{label}(队列中有 {count} 首歌曲)",
playing: "正在播放",
paused: "已暂停",
sent: "已将 {uri} 交给播放器",
play_started: "正在播放 {label}",
missing_feature: "无法播放:此 spotatui 未编译 `{feature}` 功能,没有可播放 {uri} 的音源。当前播放未更改。",
missing_track: "无法播放:曲库中没有 {uri},当前播放未更改。请用 search_tracks 获取有效 URI。",
history_failed: "无法读取收听历史:{error}",
history_task_failed: "收听历史任务失败:{error}",
history_sparse: "收听历史较少({period}有 {count} 次有效播放)。请先询问用户想听什么。",
history_summary: "收听历史({period},{count} 次有效播放):",
catalogue_unavailable: "spotatui 尚未登录 Spotify,无法访问曲库。请在 spotatui 界面中登录后重试。",
session_unavailable: "spotatui 尚未登录 Spotify。请在 spotatui 界面中登录后重试。",
uneditable: "“{name}”不属于用户,也不是协作播放列表,无法编辑",
queue_filter_failed: "无法读取播放列表以执行 exclude_owned,未加入任何歌曲。请稍后重试,或在 queue_tracks 中关闭 exclude_owned 后加入。",
nothing_queued: "未加入任何歌曲。\n{detail}",
unknown_tool: "没有名为 {name} 的工具。请使用上方列出的工具。",
invalid_arguments: "参数无效:{detail}",
shutting_down: "spotatui 正在关闭,未执行此请求",
dropped: "spotatui 在回应前取消了请求",
timed_out: "spotatui 未及时回应,可能正在忙碌。请稍后重试。",
..EN
};
// Keep ..EN so new English fields compile before their translations are added.
#[allow(clippy::needless_update)]
pub const ZH_HANT: ToolStrings = ToolStrings { ..EN };
pub const ZH_HANT: ToolStrings = ToolStrings {
brief_artists: "最常聽藝人",
brief_tracks: "最常聽歌曲",
brief_albums: "最常聽專輯",
brief_recent: "最近播放(不要重複)",
brief_now: "\n正在播放:{name}\n",
brief_vibe: "\n聽眾想聽:{name}\n",
period_week: "最近 7 天",
period_month: "最近 30 天",
period_this_month: "本月",
period_year: "今年",
period_all: "全部時間",
ownership_incomplete: "\n\n提示:播放清單索引仍在建立,本次搜尋的 [已收藏] 僅表示喜歡的歌曲。稍後重新搜尋可取得完整結果。",
queue_not_found: "\n\n未找到(已略過):{names}",
queue_owned: "\n已在音樂庫中(因 exclude_owned 略過):{names}",
queue_duplicate: "\n已在佇列中、正在播放或最近播放過(已略過):{names}",
queue_filter_spotify_only: "\nexclude_owned 僅檢查 Spotify 歌曲,本次未略過任何歌曲。",
none_found: "未找到:{names}",
none_owned: "已在音樂庫中:{names}",
none_duplicate: "已在佇列中、正在播放或最近播放過:{names}",
none_new: "所有歌曲都已在佇列中或正在播放。",
skipped: "已跳至下一首",
vibe_set: "DJ 氛圍已設為:{vibe}。{effect}",
vibe_cleared: "已清除 DJ 氛圍",
vibe_on: "內建 DJ 將從下次補充佇列起遵循此氛圍。",
vibe_off: "內建 DJ 的自動補充佇列已關閉,開啟後才會生效;目前請手動加入歌曲。get_listening_history 仍會傳回此氛圍。",
vibe_unavailable: "此版本沒有內建自動補充佇列 DJ,不會自動套用此氛圍。氛圍已儲存,可由 get_listening_history 讀取後用於加入歌曲。",
creating: "正在建立播放清單「{name}」,包含 {count} 首歌曲。Spotify 建立後即可由 list_playlists 查看。",
adding: "正在向 {playlist} 新增 {count} 首歌曲",
removing: "正在從 {playlist} 的位置 {position} 移除 {uri}(僅在曲目相符時移除)",
following: "正在追蹤 spotify:artist:{artist}",
unfollowing: "正在取消追蹤 spotify:artist:{artist}",
transfer: "正在將播放切換至 {name}",
liking: "正在喜歡 {changed} 首歌曲;{unchanged} 首已喜歡",
unliking: "正在取消喜歡 {changed} 首歌曲;{unchanged} 首原本未喜歡",
search_failed: "搜尋失敗:{error}",
no_tracks: "未找到與「{query}」相關的歌曲",
found_track: "{name} — {artists} [{uri}] [{owned}]",
owned: "已收藏",
new_track: "未收藏",
queued: "已加入 {count} 首歌曲:",
queue_empty: "佇列為空",
nothing_playing: "沒有正在播放的內容(佇列中有 {count} 首歌曲)",
now_playing: "{state}:{label}(佇列中有 {count} 首歌曲)",
playing: "正在播放",
paused: "已暫停",
sent: "已將 {uri} 交給播放器",
play_started: "正在播放 {label}",
missing_feature: "無法播放:此 spotatui 未編譯 `{feature}` 功能,沒有可播放 {uri} 的來源。目前播放未變更。",
missing_track: "無法播放:曲庫中沒有 {uri},目前播放未變更。請用 search_tracks 取得有效 URI。",
history_failed: "無法讀取聆聽紀錄:{error}",
history_task_failed: "聆聽紀錄工作失敗:{error}",
history_sparse: "聆聽紀錄較少({period}有 {count} 次有效播放)。請先詢問使用者想聽什麼。",
history_summary: "聆聽紀錄({period},{count} 次有效播放):",
catalogue_unavailable: "spotatui 尚未登入 Spotify,無法存取曲庫。請在 spotatui 介面中登入後重試。",
session_unavailable: "spotatui 尚未登入 Spotify。請在 spotatui 介面中登入後重試。",
uneditable: "「{name}」不屬於使用者,也不是協作播放清單,無法編輯",
queue_filter_failed: "無法讀取播放清單以執行 exclude_owned,未加入任何歌曲。請稍後重試,或在 queue_tracks 中關閉 exclude_owned 後加入。",
nothing_queued: "未加入任何歌曲。\n{detail}",
unknown_tool: "沒有名為 {name} 的工具。請使用上方列出的工具。",
invalid_arguments: "參數無效:{detail}",
shutting_down: "spotatui 正在關閉,未執行此請求",
dropped: "spotatui 在回應前取消了請求",
timed_out: "spotatui 未及時回應,可能正在忙碌。請稍後重試。",
..EN
};
#[cfg(any(feature = "dj-core", test))]
impl ToolStrings {
pub fn brief_now(&self, name: &str) -> String {
Expand Down
30 changes: 16 additions & 14 deletions src/gui/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2988,26 +2988,28 @@ mod tests {
app.user_config.behavior.language = Language::ZhHant;
app.refresh_language();
let changed = pushed(&diff(&before, &app, Language::En), "playlist_sync");
assert!(changed["payload"]["links"][0]["last_line"]
.as_str()
.unwrap()
.contains("{added} 五月天"));
assert_eq!(
changed["payload"]["links"][0]["last_line"],
"{added} 五月天:已新增 2 首,已移除 1 首,3 首未對應"
);
assert_eq!(
changed["payload"]["last_summary"],
"播放清單同步(模擬執行):新增 2 首,移除 1 首,未對應 3 首"
);

let mut failed = report;
failed.error = Some("{added} HTTP 503".to_string());
failed.links[0].outcome = LinkOutcome::Failed("{name} HTTP 503".into());
app.finish_playlist_sync(failed);
let localized = playlist_sync(&app);
assert!(localized
.last_summary
.as_deref()
.unwrap()
.contains("{added} HTTP 503"));
assert!(localized.links[0]
.last_line
.as_deref()
.unwrap()
.contains("{name} HTTP 503"));
assert_eq!(
localized.last_summary.as_deref(),
Some("播放清單同步失敗:{added} HTTP 503")
);
assert_eq!(
localized.links[0].last_line.as_deref(),
Some("{added} 五月天:失敗({name} HTTP 503)")
);
}

#[test]
Expand Down
9 changes: 8 additions & 1 deletion src/infra/dj/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ mod tests {
if language == Language::En {
assert_eq!(result.display_text(language), result.text);
assert_eq!(status, format!("MCP: {}", result.text));
} else {
assert!(!status.contains("DJ vibe set to"));
assert!(status.contains(if language == Language::ZhHans {
"DJ 氛围已设为"
} else {
"DJ 氛圍已設為"
}));
}
}
}
Expand All @@ -164,7 +171,7 @@ mod tests {
);
assert_eq!(
result.display_text(Language::ZhHant),
crate::core::i18n::ZH_HANT.status.tools.shutting_down
"spotatui 正在關閉,未執行此請求"
);
}

Expand Down
5 changes: 5 additions & 0 deletions src/infra/dj/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1657,5 +1657,10 @@ mod tests {
assert!(claude.label_with(strings).contains("claude"));
assert!(claude.note_with(strings).contains("PATH"));
}
assert_eq!(claude.label_with(&ZH_HANS), "claude(无需 API key)");
assert_eq!(
claude.note_with(&ZH_HANT),
"使用你的 Claude Pro/Max 方案 · 不在 PATH 中"
);
}
}
10 changes: 9 additions & 1 deletion src/infra/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3754,7 +3754,15 @@ mod tests {
assert_eq!(tracks[0].uri, english[0].uri);
assert_eq!(tracks[0].value, 3_720_000);
}

assert_eq!(
aggregate_top_tracks_with(&records, 10, &ZH_HANS)[0].detail,
"2 次播放 · 1 小时 2 分钟"
);
assert_eq!(
aggregate_top_artists_with(&records, 10, &ZH_HANT)[0].detail,
"曲目播放 2 次 · 1 小時 2 分鐘"
);
assert_eq!(period_label(RecapPeriod::Year), "This Year");
assert_eq!(period_label_with(RecapPeriod::Year, &ZH_HANT), "今年");
}
}
12 changes: 2 additions & 10 deletions src/infra/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -723,16 +723,8 @@ mod tests {
let cached = futures::executor::block_on(LocalSource::new(root).playlists()).unwrap();
for (strings, root_label, unknown_track) in [
(&EN, "(root)", "Unknown Track"),
(
&ZH_HANS,
ZH_HANS.status.infra.local_root_folder,
ZH_HANS.status.infra.local_unknown_track,
),
(
&ZH_HANT,
ZH_HANT.status.infra.local_root_folder,
ZH_HANT.status.infra.local_unknown_track,
),
(&ZH_HANS, "(根目录)", "未知曲目"),
(&ZH_HANT, "(根目錄)", "未知曲目"),
] {
let source = LocalSource::new(root);
let playlists = futures::executor::block_on(source.playlists()).unwrap();
Expand Down
4 changes: 2 additions & 2 deletions src/infra/network/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3260,8 +3260,8 @@ mod folder_label_tests {
let cached = structurize_playlist_folders(&nodes, &[]);
for (strings, fallback) in [
(&EN, "Unnamed Folder"),
(&ZH_HANS, ZH_HANS.status.infra.unnamed_folder),
(&ZH_HANT, ZH_HANT.status.infra.unnamed_folder),
(&ZH_HANS, "未命名文件夹"),
(&ZH_HANT, "未命名資料夾"),
] {
assert_eq!(
names(&cached, strings),
Expand Down
14 changes: 2 additions & 12 deletions src/infra/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2816,12 +2816,7 @@ mod tests {
.expect("reading the table must release the lock before writing status");
assert_eq!(
app.lock().await.status_message(),
Some(
crate::core::i18n::ZH_HANT
.status
.infra
.spotify_not_connected
)
Some("Spotify 未連線。請按 `d` 並選擇 Spotify 登入。")
);
{
let mut app = app.lock().await;
Expand All @@ -2831,12 +2826,7 @@ mod tests {
network.start_party(sync::ControlMode::HostOnly).await;
assert_eq!(
app.lock().await.status_message(),
Some(
crate::core::i18n::ZH_HANS
.status
.infra
.spotify_not_connected
)
Some("Spotify 未连接。请按 `d` 并选择 Spotify 登录。")
);
app.lock().await.use_english_messages();
network.start_party(sync::ControlMode::HostOnly).await;
Expand Down
6 changes: 3 additions & 3 deletions src/infra/network/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@ mod tests {
assert!(text.contains("客廳 {body}"));
assert!(text.contains("raw {status}"));
assert!(text.ends_with("[PUT /v1/me/player?device_id=客廳]"));

assert!(!text.contains("did not join after its login"));
assert_eq!(error.to_string(), english);
}
}
Expand All @@ -819,11 +819,11 @@ mod tests {
);
assert_eq!(
error_text(&error, Language::ZhHans),
crate::core::i18n::ZH_HANS.status.infra.playlist_changed
"播放列表在加载后发生了变化,请重新打开后再试"
);
assert_eq!(
error_text(&error, Language::ZhHant),
crate::core::i18n::ZH_HANT.status.infra.playlist_changed
"播放清單在載入後發生了變更,請重新開啟後再試"
);
let raw = anyhow!("unknown speaker reply {name}", name = "{type}");
assert_eq!(
Expand Down
Loading