1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6pub enum Language {
7 English,
8 Chinese,
9}
10
11impl Language {
12 pub fn code(&self) -> &'static str {
14 match self {
15 Language::English => "en",
16 Language::Chinese => "zh",
17 }
18 }
19
20 pub fn name(&self) -> &'static str {
22 match self {
23 Language::English => "English",
24 Language::Chinese => "中文",
25 }
26 }
27
28 pub fn all() -> Vec<Self> {
30 vec![Language::English, Language::Chinese]
31 }
32
33 pub fn from_code(code: &str) -> Option<Self> {
35 match code {
36 "en" | "en-US" | "en-GB" => Some(Language::English),
37 "zh" | "zh-CN" | "zh-TW" => Some(Language::Chinese),
38 _ => None,
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub enum TranslationKey {
46 Title,
48 Score,
49 Best,
50 Moves,
51 Time,
52 NewGame,
53 Undo,
54 GameOver,
55 Congratulations,
56 YouWon,
57 PressRToRestart,
58 ContinuePlaying,
59
60 Controls,
62 MoveTiles,
63 Restart,
64 UndoMove,
65 CycleTheme,
66 SelectTheme,
67 ThemeHelp,
68 ReplayMode,
69 StatisticsCharts,
70 AIMode,
71 Help,
72 Quit,
73
74 ReplayModeTitle,
76 StartRecording,
77 LoadReplay,
78 ListReplays,
79 BackToMenu,
80 PlayPause,
81 StepThrough,
82 AdjustSpeed,
83 StopRecording,
84
85 AIModeTitle,
87 ToggleAutoPlay,
88 SwitchAlgorithm,
89 AdjustSpeedAI,
90 ExitImmediately,
91 Greedy,
92 Expectimax,
93 MCTS,
94
95 ChartsTitle,
97 Summary,
98 ScoreTrend,
99 EfficiencyTrend,
100 TileAchievements,
101 RecentGames,
102 NavigateCharts,
103 ToggleCharts,
104
105 Statistics,
107 GamesPlayed,
108 GamesWon,
109 WinRate,
110 HighestScore,
111 AverageScore,
112 TotalMoves,
113 AverageMoves,
114 TotalPlayTime,
115 AverageDuration,
116 HighestTile,
117 ScoreDistribution,
118 LowScore,
119 MediumScore,
120 HighScore,
121 VeryHighScore,
122 NoDataAvailable,
123 NoGamesPlayed,
124 NoRecentGames,
125
126 ThemeClassic,
128 ThemeDark,
129 ThemeNeon,
130 ThemeRetro,
131 ThemePastel,
132 AvailableThemes,
133 PressTToCycle,
134 PressNumbersToSelect,
135
136 Loading,
138 Error,
139 Success,
140 Warning,
141 Info,
142
143 Hours,
145 Minutes,
146 Seconds,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct TranslationData {
152 pub language: Language,
153 pub translations: HashMap<String, String>,
154}
155
156impl TranslationData {
157 pub fn english() -> Self {
159 let mut translations = HashMap::new();
160
161 translations.insert("title".to_string(), "Rusty2048".to_string());
163 translations.insert("score".to_string(), "Score".to_string());
164 translations.insert("best".to_string(), "Best".to_string());
165 translations.insert("moves".to_string(), "Moves".to_string());
166 translations.insert("time".to_string(), "Time".to_string());
167 translations.insert("new_game".to_string(), "New Game".to_string());
168 translations.insert("undo".to_string(), "Undo".to_string());
169 translations.insert("game_over".to_string(), "Game Over!".to_string());
170 translations.insert(
171 "congratulations".to_string(),
172 "🎉 Congratulations!".to_string(),
173 );
174 translations.insert("you_won".to_string(), "You won!".to_string());
175 translations.insert(
176 "press_r_to_restart".to_string(),
177 "Press R to restart".to_string(),
178 );
179 translations.insert(
180 "continue_playing".to_string(),
181 "or continue playing".to_string(),
182 );
183
184 translations.insert("controls".to_string(), "Controls".to_string());
186 translations.insert("move_tiles".to_string(), "WASD/Arrow Keys".to_string());
187 translations.insert("restart".to_string(), "R".to_string());
188 translations.insert("undo_move".to_string(), "U".to_string());
189 translations.insert("cycle_theme".to_string(), "T".to_string());
190 translations.insert("select_theme".to_string(), "1-5".to_string());
191 translations.insert("theme_help".to_string(), "H".to_string());
192 translations.insert("replay_mode".to_string(), "Replay".to_string());
193 translations.insert("statistics_charts".to_string(), "Charts".to_string());
194 translations.insert("ai_mode".to_string(), "AI".to_string());
195 translations.insert("help".to_string(), "Help".to_string());
196 translations.insert("quit".to_string(), "Quit".to_string());
197
198 translations.insert("replay_mode_title".to_string(), "Replay Mode".to_string());
200 translations.insert("start_recording".to_string(), "Start Recording".to_string());
201 translations.insert("load_replay".to_string(), "Load Replay".to_string());
202 translations.insert("list_replays".to_string(), "List Replays".to_string());
203 translations.insert("back_to_menu".to_string(), "Back to Menu".to_string());
204 translations.insert("play_pause".to_string(), "Space".to_string());
205 translations.insert("step_through".to_string(), "Left/Right".to_string());
206 translations.insert("adjust_speed".to_string(), "+/-".to_string());
207 translations.insert("stop_recording".to_string(), "S".to_string());
208
209 translations.insert("ai_mode_title".to_string(), "AI Mode".to_string());
211 translations.insert("toggle_auto_play".to_string(), "O".to_string());
212 translations.insert("switch_algorithm".to_string(), "[ ]".to_string());
213 translations.insert("adjust_speed_ai".to_string(), "+/-".to_string());
214 translations.insert("exit_immediately".to_string(), "Q/ESC".to_string());
215 translations.insert("greedy".to_string(), "Greedy".to_string());
216 translations.insert("expectimax".to_string(), "Expectimax".to_string());
217 translations.insert("mcts".to_string(), "MCTS".to_string());
218
219 translations.insert("charts_title".to_string(), "Statistics Charts".to_string());
221 translations.insert("summary".to_string(), "Summary".to_string());
222 translations.insert("score_trend".to_string(), "Score Trend".to_string());
223 translations.insert(
224 "efficiency_trend".to_string(),
225 "Efficiency Trend".to_string(),
226 );
227 translations.insert(
228 "tile_achievements".to_string(),
229 "Tile Achievements".to_string(),
230 );
231 translations.insert("recent_games".to_string(), "Recent Games".to_string());
232 translations.insert("navigate_charts".to_string(), "Left/Right".to_string());
233 translations.insert("toggle_charts".to_string(), "C".to_string());
234
235 translations.insert("statistics".to_string(), "Statistics".to_string());
237 translations.insert("games_played".to_string(), "Games Played".to_string());
238 translations.insert("games_won".to_string(), "Won".to_string());
239 translations.insert("win_rate".to_string(), "Win Rate".to_string());
240 translations.insert("highest_score".to_string(), "Highest Score".to_string());
241 translations.insert("average_score".to_string(), "Average Score".to_string());
242 translations.insert("total_moves".to_string(), "Total Moves".to_string());
243 translations.insert("average_moves".to_string(), "Avg Moves".to_string());
244 translations.insert("total_play_time".to_string(), "Total Play Time".to_string());
245 translations.insert("average_duration".to_string(), "Avg Duration".to_string());
246 translations.insert("highest_tile".to_string(), "Highest Tile".to_string());
247 translations.insert(
248 "score_distribution".to_string(),
249 "Score Distribution".to_string(),
250 );
251 translations.insert("low_score".to_string(), "0-1000".to_string());
252 translations.insert("medium_score".to_string(), "1001-5000".to_string());
253 translations.insert("high_score".to_string(), "5001-10000".to_string());
254 translations.insert("very_high_score".to_string(), "10001+".to_string());
255 translations.insert(
256 "no_data_available".to_string(),
257 "No data available".to_string(),
258 );
259 translations.insert(
260 "no_games_played".to_string(),
261 "No games played yet!".to_string(),
262 );
263 translations.insert("no_recent_games".to_string(), "No recent games".to_string());
264
265 translations.insert("theme_classic".to_string(), "Classic".to_string());
267 translations.insert("theme_dark".to_string(), "Dark".to_string());
268 translations.insert("theme_neon".to_string(), "Neon".to_string());
269 translations.insert("theme_retro".to_string(), "Retro".to_string());
270 translations.insert("theme_pastel".to_string(), "Pastel".to_string());
271 translations.insert(
272 "available_themes".to_string(),
273 "Available Themes".to_string(),
274 );
275 translations.insert(
276 "press_t_to_cycle".to_string(),
277 "Press T to cycle themes".to_string(),
278 );
279 translations.insert(
280 "press_numbers_to_select".to_string(),
281 "or number keys 1-5 to select directly".to_string(),
282 );
283
284 translations.insert("loading".to_string(), "Loading...".to_string());
286 translations.insert("error".to_string(), "Error".to_string());
287 translations.insert("success".to_string(), "Success".to_string());
288 translations.insert("warning".to_string(), "Warning".to_string());
289 translations.insert("info".to_string(), "Info".to_string());
290
291 translations.insert("hours".to_string(), "h".to_string());
293 translations.insert("minutes".to_string(), "m".to_string());
294 translations.insert("seconds".to_string(), "s".to_string());
295
296 Self {
297 language: Language::English,
298 translations,
299 }
300 }
301
302 pub fn chinese() -> Self {
304 let mut translations = HashMap::new();
305
306 translations.insert("title".to_string(), "Rusty2048".to_string());
308 translations.insert("score".to_string(), "分数".to_string());
309 translations.insert("best".to_string(), "最高分".to_string());
310 translations.insert("moves".to_string(), "步数".to_string());
311 translations.insert("time".to_string(), "时间".to_string());
312 translations.insert("new_game".to_string(), "新游戏".to_string());
313 translations.insert("undo".to_string(), "撤销".to_string());
314 translations.insert("game_over".to_string(), "游戏结束!".to_string());
315 translations.insert("congratulations".to_string(), "🎉 恭喜!".to_string());
316 translations.insert("you_won".to_string(), "你赢了!".to_string());
317 translations.insert("press_r_to_restart".to_string(), "按R重新开始".to_string());
318 translations.insert("continue_playing".to_string(), "或继续游戏".to_string());
319
320 translations.insert("controls".to_string(), "控制".to_string());
322 translations.insert("move_tiles".to_string(), "WASD/方向键".to_string());
323 translations.insert("restart".to_string(), "R".to_string());
324 translations.insert("undo_move".to_string(), "U".to_string());
325 translations.insert("cycle_theme".to_string(), "T".to_string());
326 translations.insert("select_theme".to_string(), "1-5".to_string());
327 translations.insert("theme_help".to_string(), "H".to_string());
328 translations.insert("replay_mode".to_string(), "回放".to_string());
329 translations.insert("statistics_charts".to_string(), "图表".to_string());
330 translations.insert("ai_mode".to_string(), "AI".to_string());
331 translations.insert("help".to_string(), "帮助".to_string());
332 translations.insert("quit".to_string(), "退出".to_string());
333
334 translations.insert("replay_mode_title".to_string(), "回放模式".to_string());
336 translations.insert("start_recording".to_string(), "开始录制".to_string());
337 translations.insert("load_replay".to_string(), "加载回放".to_string());
338 translations.insert("list_replays".to_string(), "回放列表".to_string());
339 translations.insert("back_to_menu".to_string(), "返回菜单".to_string());
340 translations.insert("play_pause".to_string(), "空格".to_string());
341 translations.insert("step_through".to_string(), "左右键".to_string());
342 translations.insert("adjust_speed".to_string(), "+/-".to_string());
343 translations.insert("stop_recording".to_string(), "S".to_string());
344
345 translations.insert("ai_mode_title".to_string(), "AI模式".to_string());
347 translations.insert("toggle_auto_play".to_string(), "O".to_string());
348 translations.insert("switch_algorithm".to_string(), "[ ]".to_string());
349 translations.insert("adjust_speed_ai".to_string(), "+/-".to_string());
350 translations.insert("exit_immediately".to_string(), "Q/ESC".to_string());
351 translations.insert("greedy".to_string(), "贪心".to_string());
352 translations.insert("expectimax".to_string(), "期望最大化".to_string());
353 translations.insert("mcts".to_string(), "蒙特卡洛".to_string());
354
355 translations.insert("charts_title".to_string(), "统计图表".to_string());
357 translations.insert("summary".to_string(), "摘要".to_string());
358 translations.insert("score_trend".to_string(), "分数趋势".to_string());
359 translations.insert("efficiency_trend".to_string(), "效率趋势".to_string());
360 translations.insert("tile_achievements".to_string(), "瓦片成就".to_string());
361 translations.insert("recent_games".to_string(), "最近游戏".to_string());
362 translations.insert("navigate_charts".to_string(), "左右键".to_string());
363 translations.insert("toggle_charts".to_string(), "C".to_string());
364
365 translations.insert("statistics".to_string(), "统计".to_string());
367 translations.insert("games_played".to_string(), "游戏局数".to_string());
368 translations.insert("games_won".to_string(), "胜利".to_string());
369 translations.insert("win_rate".to_string(), "胜率".to_string());
370 translations.insert("highest_score".to_string(), "最高分".to_string());
371 translations.insert("average_score".to_string(), "平均分".to_string());
372 translations.insert("total_moves".to_string(), "总步数".to_string());
373 translations.insert("average_moves".to_string(), "平均步数".to_string());
374 translations.insert("total_play_time".to_string(), "总游戏时间".to_string());
375 translations.insert("average_duration".to_string(), "平均时长".to_string());
376 translations.insert("highest_tile".to_string(), "最高瓦片".to_string());
377 translations.insert("score_distribution".to_string(), "分数分布".to_string());
378 translations.insert("low_score".to_string(), "0-1000".to_string());
379 translations.insert("medium_score".to_string(), "1001-5000".to_string());
380 translations.insert("high_score".to_string(), "5001-10000".to_string());
381 translations.insert("very_high_score".to_string(), "10001+".to_string());
382 translations.insert("no_data_available".to_string(), "暂无数据".to_string());
383 translations.insert(
384 "no_games_played".to_string(),
385 "还没有玩过游戏!".to_string(),
386 );
387 translations.insert("no_recent_games".to_string(), "没有最近游戏".to_string());
388
389 translations.insert("theme_classic".to_string(), "经典".to_string());
391 translations.insert("theme_dark".to_string(), "暗黑".to_string());
392 translations.insert("theme_neon".to_string(), "霓虹".to_string());
393 translations.insert("theme_retro".to_string(), "复古".to_string());
394 translations.insert("theme_pastel".to_string(), "粉彩".to_string());
395 translations.insert("available_themes".to_string(), "可用主题".to_string());
396 translations.insert(
397 "press_t_to_cycle".to_string(),
398 "按T循环切换主题".to_string(),
399 );
400 translations.insert(
401 "press_numbers_to_select".to_string(),
402 "或按数字键1-5直接选择".to_string(),
403 );
404
405 translations.insert("loading".to_string(), "加载中...".to_string());
407 translations.insert("error".to_string(), "错误".to_string());
408 translations.insert("success".to_string(), "成功".to_string());
409 translations.insert("warning".to_string(), "警告".to_string());
410 translations.insert("info".to_string(), "信息".to_string());
411
412 translations.insert("hours".to_string(), "时".to_string());
414 translations.insert("minutes".to_string(), "分".to_string());
415 translations.insert("seconds".to_string(), "秒".to_string());
416
417 Self {
418 language: Language::Chinese,
419 translations,
420 }
421 }
422}
423
424#[derive(Debug, Clone)]
426pub struct I18n {
427 current_language: Language,
428 translations: HashMap<Language, TranslationData>,
429}
430
431impl I18n {
432 pub fn new() -> Self {
434 let mut translations = HashMap::new();
435 translations.insert(Language::English, TranslationData::english());
436 translations.insert(Language::Chinese, TranslationData::chinese());
437
438 Self {
439 current_language: Language::English,
440 translations,
441 }
442 }
443
444 pub fn set_language(&mut self, language: Language) {
446 self.current_language = language;
447 }
448
449 pub fn current_language(&self) -> Language {
451 self.current_language
452 }
453
454 pub fn t(&self, key: &TranslationKey) -> String {
456 let key_str = self.key_to_string(key);
457 if let Some(translation_data) = self.translations.get(&self.current_language) {
458 if let Some(translation) = translation_data.translations.get(&key_str) {
459 return translation.clone();
460 }
461 }
462
463 if let Some(translation_data) = self.translations.get(&Language::English) {
465 if let Some(translation) = translation_data.translations.get(&key_str) {
466 return translation.clone();
467 }
468 }
469
470 key_str
472 }
473
474 pub fn t_with_params(&self, key: &TranslationKey, params: &[(&str, &str)]) -> String {
476 let mut text = self.t(key);
477 for (param, value) in params {
478 text = text.replace(&format!("{{{}}}", param), value);
479 }
480 text
481 }
482
483 fn key_to_string(&self, key: &TranslationKey) -> String {
485 match key {
486 TranslationKey::Title => "title".to_string(),
487 TranslationKey::Score => "score".to_string(),
488 TranslationKey::Best => "best".to_string(),
489 TranslationKey::Moves => "moves".to_string(),
490 TranslationKey::Time => "time".to_string(),
491 TranslationKey::NewGame => "new_game".to_string(),
492 TranslationKey::Undo => "undo".to_string(),
493 TranslationKey::GameOver => "game_over".to_string(),
494 TranslationKey::Congratulations => "congratulations".to_string(),
495 TranslationKey::YouWon => "you_won".to_string(),
496 TranslationKey::PressRToRestart => "press_r_to_restart".to_string(),
497 TranslationKey::ContinuePlaying => "continue_playing".to_string(),
498 TranslationKey::Controls => "controls".to_string(),
499 TranslationKey::MoveTiles => "move_tiles".to_string(),
500 TranslationKey::Restart => "restart".to_string(),
501 TranslationKey::UndoMove => "undo_move".to_string(),
502 TranslationKey::CycleTheme => "cycle_theme".to_string(),
503 TranslationKey::SelectTheme => "select_theme".to_string(),
504 TranslationKey::ThemeHelp => "theme_help".to_string(),
505 TranslationKey::ReplayMode => "replay_mode".to_string(),
506 TranslationKey::StatisticsCharts => "statistics_charts".to_string(),
507 TranslationKey::AIMode => "ai_mode".to_string(),
508 TranslationKey::Help => "help".to_string(),
509 TranslationKey::Quit => "quit".to_string(),
510 TranslationKey::ReplayModeTitle => "replay_mode_title".to_string(),
511 TranslationKey::StartRecording => "start_recording".to_string(),
512 TranslationKey::LoadReplay => "load_replay".to_string(),
513 TranslationKey::ListReplays => "list_replays".to_string(),
514 TranslationKey::BackToMenu => "back_to_menu".to_string(),
515 TranslationKey::PlayPause => "play_pause".to_string(),
516 TranslationKey::StepThrough => "step_through".to_string(),
517 TranslationKey::AdjustSpeed => "adjust_speed".to_string(),
518 TranslationKey::StopRecording => "stop_recording".to_string(),
519 TranslationKey::AIModeTitle => "ai_mode_title".to_string(),
520 TranslationKey::ToggleAutoPlay => "toggle_auto_play".to_string(),
521 TranslationKey::SwitchAlgorithm => "switch_algorithm".to_string(),
522 TranslationKey::AdjustSpeedAI => "adjust_speed_ai".to_string(),
523 TranslationKey::ExitImmediately => "exit_immediately".to_string(),
524 TranslationKey::Greedy => "greedy".to_string(),
525 TranslationKey::Expectimax => "expectimax".to_string(),
526 TranslationKey::MCTS => "mcts".to_string(),
527 TranslationKey::ChartsTitle => "charts_title".to_string(),
528 TranslationKey::Summary => "summary".to_string(),
529 TranslationKey::ScoreTrend => "score_trend".to_string(),
530 TranslationKey::EfficiencyTrend => "efficiency_trend".to_string(),
531 TranslationKey::TileAchievements => "tile_achievements".to_string(),
532 TranslationKey::RecentGames => "recent_games".to_string(),
533 TranslationKey::NavigateCharts => "navigate_charts".to_string(),
534 TranslationKey::ToggleCharts => "toggle_charts".to_string(),
535 TranslationKey::Statistics => "statistics".to_string(),
536 TranslationKey::GamesPlayed => "games_played".to_string(),
537 TranslationKey::GamesWon => "games_won".to_string(),
538 TranslationKey::WinRate => "win_rate".to_string(),
539 TranslationKey::HighestScore => "highest_score".to_string(),
540 TranslationKey::AverageScore => "average_score".to_string(),
541 TranslationKey::TotalMoves => "total_moves".to_string(),
542 TranslationKey::AverageMoves => "average_moves".to_string(),
543 TranslationKey::TotalPlayTime => "total_play_time".to_string(),
544 TranslationKey::AverageDuration => "average_duration".to_string(),
545 TranslationKey::HighestTile => "highest_tile".to_string(),
546 TranslationKey::ScoreDistribution => "score_distribution".to_string(),
547 TranslationKey::LowScore => "low_score".to_string(),
548 TranslationKey::MediumScore => "medium_score".to_string(),
549 TranslationKey::HighScore => "high_score".to_string(),
550 TranslationKey::VeryHighScore => "very_high_score".to_string(),
551 TranslationKey::NoDataAvailable => "no_data_available".to_string(),
552 TranslationKey::NoGamesPlayed => "no_games_played".to_string(),
553 TranslationKey::NoRecentGames => "no_recent_games".to_string(),
554 TranslationKey::ThemeClassic => "theme_classic".to_string(),
555 TranslationKey::ThemeDark => "theme_dark".to_string(),
556 TranslationKey::ThemeNeon => "theme_neon".to_string(),
557 TranslationKey::ThemeRetro => "theme_retro".to_string(),
558 TranslationKey::ThemePastel => "theme_pastel".to_string(),
559 TranslationKey::AvailableThemes => "available_themes".to_string(),
560 TranslationKey::PressTToCycle => "press_t_to_cycle".to_string(),
561 TranslationKey::PressNumbersToSelect => "press_numbers_to_select".to_string(),
562 TranslationKey::Loading => "loading".to_string(),
563 TranslationKey::Error => "error".to_string(),
564 TranslationKey::Success => "success".to_string(),
565 TranslationKey::Warning => "warning".to_string(),
566 TranslationKey::Info => "info".to_string(),
567 TranslationKey::Hours => "hours".to_string(),
568 TranslationKey::Minutes => "minutes".to_string(),
569 TranslationKey::Seconds => "seconds".to_string(),
570 }
571 }
572
573 pub fn supported_languages(&self) -> Vec<Language> {
575 Language::all()
576 }
577
578 pub fn format_duration(&self, seconds: u64) -> String {
580 let hours = seconds / 3600;
581 let minutes = (seconds % 3600) / 60;
582 let secs = seconds % 60;
583
584 if hours > 0 {
585 format!(
586 "{}{}{:02}{}{:02}{}",
587 hours,
588 self.t(&TranslationKey::Hours),
589 minutes,
590 self.t(&TranslationKey::Minutes),
591 secs,
592 self.t(&TranslationKey::Seconds)
593 )
594 } else {
595 format!(
596 "{}{}{:02}{}",
597 minutes,
598 self.t(&TranslationKey::Minutes),
599 secs,
600 self.t(&TranslationKey::Seconds)
601 )
602 }
603 }
604}
605
606impl Default for I18n {
607 fn default() -> Self {
608 Self::new()
609 }
610}