Internationalize UI text w/ german as first second lang

Adds rust-i18n and refactors all of the text copy in the app to use the
translation function so that the UI language can be dynamically updated
in the settings.
This commit is contained in:
2026-03-17 04:29:25 +00:00
parent 895e04d6ce
commit 6d5de33f55
24 changed files with 2924 additions and 820 deletions

457
locales/en.yml Normal file
View File

@@ -0,0 +1,457 @@
# Main menu
menu:
subtitle: 'Terminal Typing Tutor'
adaptive_drill: 'Adaptive Drill'
adaptive_drill_desc: 'Phonetic words with adaptive letter unlocking'
code_drill: 'Code Drill'
code_drill_desc: 'Practice typing code syntax'
passage_drill: 'Passage Drill'
passage_drill_desc: 'Type passages from books'
skill_tree: 'Skill Tree'
skill_tree_desc: 'View progression branches and launch drills'
keyboard: 'Keyboard'
keyboard_desc: 'Explore keyboard layout and key statistics'
statistics: 'Statistics'
statistics_desc: 'View your typing statistics'
settings: 'Settings'
settings_desc: 'Configure keydr'
day_streak: ' | %{days} day streak'
key_progress: ' Key Progress %{unlocked}/%{total} (%{mastered} mastered) | Target %{target} WPM%{streak}'
hint_start: '[1-3] Start'
hint_skill_tree: '[t] Skill Tree'
hint_keyboard: '[b] Keyboard'
hint_stats: '[s] Stats'
hint_settings: '[c] Settings'
hint_quit: '[q] Quit'
# Drill screen
drill:
title: ' Drill '
mode_adaptive: 'Adaptive'
mode_code: 'Code (Unranked)'
mode_passage: 'Passage (Unranked)'
focus_char: 'Focus: ''%{ch}'''
focus_bigram: 'Focus: "%{bigram}"'
focus_both: 'Focus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Acc'
header_err: 'Err'
code_source: ' Code source '
passage_source: ' Passage source '
footer: '[ESC] End drill [Backspace] Delete'
keys_reenabled: 'Keys re-enabled in %{ms}ms'
hint_end: '[ESC] End drill'
hint_backspace: '[Backspace] Delete'
# Dashboard / drill result
dashboard:
title: ' Drill Complete '
results: 'Results'
unranked_note_prefix: ' (Unranked'
unranked_note_suffix: ' does not count toward skill tree)'
speed: ' Speed: '
accuracy_label: ' Accuracy: '
time_label: ' Time: '
errors_label: ' Errors: '
correct_detail: ' (%{correct}/%{total} correct)'
input_blocked: ' Input temporarily blocked '
input_blocked_ms: '(%{ms}ms remaining)'
hint_continue: '[c/Enter/Space] Continue'
hint_retry: '[r] Retry'
hint_menu: '[q] Menu'
hint_stats: '[s] Stats'
hint_delete: '[x] Delete'
# Stats sidebar (during drill)
sidebar:
title: ' Stats '
wpm: 'WPM: '
target: 'Target: '
target_wpm: '%{wpm} WPM'
accuracy: 'Accuracy: '
progress: 'Progress: '
correct: 'Correct: '
errors: 'Errors: '
time: 'Time: '
last_drill: ' Last Drill '
vs_avg: ' vs avg: '
# Statistics dashboard
stats:
title: ' Statistics '
empty: 'No drills completed yet. Start typing!'
tab_dashboard: '[1] Dashboard'
tab_history: '[2] History'
tab_activity: '[3] Activity'
tab_accuracy: '[4] Accuracy'
tab_timing: '[5] Timing'
tab_ngrams: '[6] N-grams'
hint_back: '[ESC] Back'
hint_next_tab: '[Tab] Next tab'
hint_switch_tab: '[1-6] Switch tab'
hint_navigate: '[j/k] Navigate'
hint_page: '[PgUp/PgDn] Page'
hint_delete: '[x] Delete'
summary_title: ' Summary '
drills: ' Drills: '
avg_wpm: ' Avg WPM: '
best_wpm: ' Best WPM: '
accuracy_label: ' Accuracy: '
total_time: ' Total time: '
wpm_chart_title: ' WPM per Drill (Last 20, Target: %{target}) '
accuracy_chart_title: ' Accuracy %% (Last 50 Drills) '
chart_drill: 'Drill #'
chart_accuracy_pct: 'Accuracy %%'
sessions_title: ' Recent Sessions '
session_header: ' # WPM Raw Acc%% Time Date/Time Mode Ranked Partial'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Delete session #%{idx}? (y/n)'
confirm_title: ' Confirm '
yes: 'yes'
no: 'no'
keyboard_accuracy_title: ' Keyboard Accuracy %% '
keyboard_timing_title: ' Keyboard Timing (ms) '
slowest_keys_title: ' Slowest Keys (ms) '
fastest_keys_title: ' Fastest Keys (ms) '
worst_accuracy_title: ' Worst Accuracy (%%) '
best_accuracy_title: ' Best Accuracy (%%) '
not_enough_data: ' Not enough data'
streaks_title: ' Streaks '
current_streak: ' Current: '
best_streak: ' Best: '
active_days: ' Active Days: '
top_days_none: ' Top Days: none'
top_days: ' Top Days: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Acc: %{pct}%%'
keys_label: ' Keys: %{unlocked}/%{total} (%{mastered} mastered)'
ngram_empty: 'Complete some adaptive drills to see n-gram data'
ngram_header_speed_narrow: ' Bgrm Speed Expct Anom%'
ngram_header_error_narrow: ' Bgrm Err Smp Rate Exp Anom%'
ngram_header_speed: ' Bigram Speed Expect Samples Anom%'
ngram_header_error: ' Bigram Errors Samples Rate Expect Anom%'
focus_title: ' Active Focus '
focus_char_label: ' Focus: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'error'
anomaly_speed: 'speed'
focus_detail_both: ' Char ''%{ch}'': weakest key | Bigram %{label}: %{type} anomaly %{pct}%%'
focus_detail_char_only: ' Char ''%{ch}'': weakest key, no confirmed bigram anomalies'
focus_detail_bigram_only: ' (%{type} anomaly: %{pct}%%)'
focus_empty: ' Complete some adaptive drills to see focus data'
error_anomalies_title: ' Error Anomalies (%{count}) '
no_error_anomalies: ' No error anomalies detected'
speed_anomalies_title: ' Speed Anomalies (%{count}) '
no_speed_anomalies: ' No speed anomalies detected'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
tri_label: ' | Tri: %{count}'
hes_label: ' | Hes: >%{ms}ms'
gain_label: ' | Gain: %{value}'
gain_interval: ' (every 50)'
focus_char_value: 'Char ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Daily Activity (Sessions per Day) '
jan: 'Jan'
feb: 'Feb'
mar: 'Mar'
apr: 'Apr'
may: 'May'
jun: 'Jun'
jul: 'Jul'
aug: 'Aug'
sep: 'Sep'
oct: 'Oct'
nov: 'Nov'
dec: 'Dec'
# Chart
chart:
wpm_over_time: ' WPM Over Time '
drill_number: 'Drill #'
# Settings
settings:
title: ' Settings '
subtitle: 'Use arrows to navigate, Enter/Right to change, ESC to save & exit'
target_wpm: 'Target WPM'
theme: 'Theme'
word_count: 'Word Count'
ui_language: 'UI Language'
dictionary_language: 'Dictionary Language'
keyboard_layout: 'Keyboard Layout'
code_language: 'Code Language'
code_downloads: 'Code Downloads'
on: 'On'
off: 'Off'
code_download_dir: 'Code Download Dir'
snippets_per_repo: 'Snippets per Repo'
unlimited: 'Unlimited'
download_code_now: 'Download Code Now'
run_downloader: 'Run downloader'
passage_downloads: 'Passage Downloads'
passage_download_dir: 'Passage Download Dir'
paragraphs_per_book: 'Paragraphs per Book'
whole_book: 'Whole book'
download_passages_now: 'Download Passages Now'
export_path: 'Export Path'
export_data: 'Export Data'
export_now: 'Export now'
import_path: 'Import Path'
import_data: 'Import Data'
import_now: 'Import now'
hint_save_back: '[ESC] Save & back'
hint_change_value: '[Enter/arrows] Change value'
hint_edit_path: '[Enter on path] Edit'
hint_move: '[←→] Move'
hint_tab_complete: '[Tab] Complete (at end)'
hint_confirm: '[Enter] Confirm'
hint_cancel: '[Esc] Cancel'
success_title: ' Success '
error_title: ' Error '
press_any_key: 'Press any key'
file_exists_title: ' File Exists '
file_exists: 'A file already exists at this path.'
overwrite_rename: '[d] Overwrite [r] Rename [Esc] Cancel'
erase_warning: 'This will erase your current data.'
export_first: 'Export first if you want to keep it.'
proceed_yn: 'Proceed? (y/n)'
confirm_import_title: ' Confirm Import '
# Selection screens
select:
dictionary_language_title: ' Select Dictionary Language '
keyboard_layout_title: ' Select Keyboard Layout '
code_language_title: ' Select Code Language '
passage_source_title: ' Select Passage Source '
ui_language_title: ' Select UI Language '
more_above: '... %{count} more above ...'
more_below: '... %{count} more below ...'
current: ' (current)'
disabled: ' (disabled)'
enabled_default: ' (enabled, default: %{layout})'
enabled: ' (enabled)'
disabled_blocked: ' (disabled: blocked)'
built_in: ' (built-in)'
cached: ' (cached)'
disabled_download: ' (disabled: download required)'
download_required: ' (download required)'
hint_navigate: '[Up/Down/PgUp/PgDn] Navigate'
hint_confirm: '[Enter] Confirm'
hint_back: '[ESC] Back'
language_resets_layout: 'Selecting a language resets keyboard layout to that language''s default.'
layout_no_language_change: 'Layout changes do not change dictionary language.'
disabled_network_notice: 'Some languages are disabled: enable network downloads in intro/settings.'
disabled_sources_notice: 'Some sources are disabled: enable network downloads in intro/settings.'
passage_all: 'All (Built-in + all books)'
passage_builtin: 'Built-in passages only'
passage_book_prefix: 'Book: %{title}'
# Progress
progress:
overall_key_progress: 'Overall Key Progress'
unlocked_mastered: '%{unlocked}/%{total} unlocked (%{mastered} mastered)'
# Skill tree
skill_tree:
title: ' Skill Tree '
locked: 'Locked'
unlocked: 'unlocked'
mastered: 'mastered'
in_progress: 'in progress'
complete: 'complete'
locked_status: 'locked'
locked_notice: 'Complete %{count} primary letters to unlock branches'
branches_separator: 'Branches (available after %{count} primary letters)'
unlocked_letters: 'Unlocked %{unlocked}/%{total} letters'
level: 'Level %{current}/%{total}'
level_zero: 'Level 0/%{total}'
in_focus: ' in focus'
hint_navigate: '[↑↓/jk] Navigate'
hint_scroll: '[PgUp/PgDn or Ctrl+U/Ctrl+D] Scroll'
hint_back: '[q] Back'
hint_unlock: '[Enter] Unlock'
hint_start_drill: '[Enter] Start Drill'
unlock_msg_1: 'Once unlocked, the default adaptive drill will mix in keys in this branch that are unlocked.'
unlock_msg_2: 'If you want to focus only on this branch, launch a drill directly from this branch in the Skill Tree.'
confirm_unlock: 'Unlock %{branch}?'
confirm_yn: '[y] Unlock [n/ESC] Cancel'
lvl_prefix: 'Lvl'
branch_primary_letters: 'Primary Letters'
branch_capital_letters: 'Capital Letters'
branch_numbers: 'Numbers 0-9'
branch_prose_punctuation: 'Prose Punctuation'
branch_whitespace: 'Whitespace'
branch_code_symbols: 'Code Symbols'
level_frequency_order: 'Frequency Order'
level_common_sentence_capitals: 'Common Sentence Capitals'
level_name_capitals: 'Name Capitals'
level_remaining_capitals: 'Remaining Capitals'
level_common_digits: 'Common Digits'
level_all_digits: 'All Digits'
level_essential: 'Essential'
level_common: 'Common'
level_expressive: 'Expressive'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Indent'
level_arithmetic_assignment: 'Arithmetic & Assignment'
level_grouping: 'Grouping'
level_logic_reference: 'Logic & Reference'
level_special: 'Special'
# Milestones
milestones:
unlock_title: ' Key Unlocked! '
mastery_title: ' Key Mastered! '
branches_title: ' New Skill Branches Available! '
branch_complete_title: ' Branch Complete! '
all_unlocked_title: ' Every Key Unlocked! '
all_mastered_title: ' Full Keyboard Mastery! '
unlocked: 'unlocked'
mastered: 'mastered'
use_finger: 'Use your %{finger}'
hold_right_shift: 'Hold Right Shift (right pinky)'
hold_left_shift: 'Hold Left Shift (left pinky)'
congratulations_all_letters: 'Congratulations! You''ve mastered all %{count} primary letters'
new_branches_available: 'New skill branches are now available:'
visit_skill_tree: 'Visit the Skill Tree to unlock a new branch'
and_start_training: 'and start training!'
open_skill_tree: 'Press [t] to open the Skill Tree now'
branch_complete_msg: 'You''ve completed the %{branch} branch!'
all_levels_mastered: 'All %{count} levels mastered.'
all_keys_confident: 'Every key in this branch is at full confidence.'
all_unlocked_msg: 'You''ve unlocked every key on the keyboard!'
all_unlocked_desc: 'Every character, symbol, and modifier is now available in your drills.'
keep_practicing_mastery: 'Keep practicing to build mastery — once every key reaches full'
confidence_complete: 'confidence, you''ll have achieved complete keyboard mastery!'
all_mastered_msg: 'Congratulations — you''ve reached full keyboard mastery!'
all_mastered_desc: 'Every key on the keyboard is at maximum confidence.'
mastery_takes_practice: 'Mastery is not a destination — it takes ongoing practice.'
keep_drilling: 'Keep drilling to maintain your edge.'
hint_skill_tree_continue: '[t] Open Skill Tree [Any other key] Continue'
hint_any_key: 'Press any key to continue'
input_blocked: 'Input temporarily blocked (%{ms}ms remaining)'
unlock_msg_1: 'Nice work! Keep building your typing skills.'
unlock_msg_2: 'Another key added to your arsenal!'
unlock_msg_3: 'Your keyboard is growing! Keep it up.'
unlock_msg_4: 'One step closer to full keyboard mastery!'
mastery_msg_1: 'This key is now at full confidence!'
mastery_msg_2: 'You''ve got this key down pat!'
mastery_msg_3: 'Muscle memory locked in!'
mastery_msg_4: 'One more key conquered!'
# Keyboard explorer
keyboard:
title: ' Keyboard '
subtitle: 'Press any key or click a key'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigate'
hint_back: '[q/ESC] Back'
key_label: 'Key: '
finger_label: 'Finger: '
hand_left: 'Left'
hand_right: 'Right'
finger_index: 'Index'
finger_middle: 'Middle'
finger_ring: 'Ring'
finger_pinky: 'Pinky'
finger_thumb: 'Thumb'
overall_accuracy: ' Overall accuracy: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Ranked accuracy: %{correct}/%{total} (%{pct}%%)'
confidence: 'Confidence: '
no_data: 'No data yet'
no_data_short: 'No data'
key_details: ' Key Details '
key_details_char: ' Key Details: ''%{ch}'' '
key_details_name: ' Key Details: %{name} '
press_key_hint: 'Press a key to see its details'
shift_label: 'Shift: '
shift_no: 'No'
overall_avg_time: 'Overall Avg Time: '
overall_best_time: 'Overall Best Time: '
overall_samples: 'Overall Samples: '
overall_accuracy_label: 'Overall Accuracy: '
branch_label: 'Branch: '
level_label: 'Level: '
built_in_key: 'Built-in Key'
unlocked_label: 'Unlocked: '
yes: 'Yes'
no: 'No'
in_focus_label: 'In Focus?: '
mastery_label: 'Mastery: '
mastery_locked: 'Locked'
ranked_avg_time: 'Ranked Avg Time: '
ranked_best_time: 'Ranked Best Time: '
ranked_samples: 'Ranked Samples: '
ranked_accuracy_label: 'Ranked Accuracy: '
# Intro dialogs
intro:
passage_title: ' Passage Downloads Setup '
code_title: ' Code Downloads Setup '
enable_downloads: 'Enable network downloads'
download_dir: 'Download directory'
paragraphs_per_book: 'Paragraphs per book (0 = whole)'
whole_book: 'whole book'
snippets_per_repo: 'Snippets per repo (0 = unlimited)'
unlimited: 'unlimited'
start_passage_drill: 'Start passage drill'
start_code_drill: 'Start code drill'
confirm: 'Confirm'
hint_navigate: '[Up/Down] Navigate'
hint_adjust: '[Left/Right] Adjust'
hint_edit: '[Type/Backspace] Edit'
hint_confirm: '[Enter] Confirm'
hint_cancel: '[ESC] Cancel'
preparing_download: 'Preparing download...'
download_passage_title: ' Downloading Passage Source '
download_code_title: ' Downloading Code Source '
book_label: ' Book: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bytes'
downloaded_bytes: 'Downloaded: %{bytes} bytes'
downloading_book_progress: 'Downloading current book: [%{bar}] %{downloaded}/%{total} bytes'
downloading_book_bytes: 'Downloading current book: %{bytes} bytes'
downloading_code_progress: 'Downloading: [%{bar}] %{downloaded}/%{total} bytes'
downloading_code_bytes: 'Downloading: %{bytes} bytes'
current_book: 'Current: %{name} (book %{done}/%{total})'
current_repo: 'Current: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr can download passages from Project Gutenberg for typing practice.'
passage_instructions_2: 'Books are downloaded once and cached locally.'
passage_instructions_3: 'Configure download settings below, then start a passage drill.'
code_instructions_1: 'keydr can download open-source code from GitHub for typing practice.'
code_instructions_2: 'Code is downloaded once and cached locally.'
code_instructions_3: 'Configure download settings below, then start a code drill.'
# Status messages (from app.rs)
status:
recovery_files: 'Recovery files found from interrupted import. Data may be inconsistent — consider re-importing.'
dir_not_exist: 'Directory does not exist: %{path}'
no_data_store: 'No data store available'
serialization_error: 'Serialization error: %{error}'
exported_to: 'Exported to %{path}'
export_failed: 'Export failed: %{error}'
could_not_read: 'Could not read file: %{error}'
invalid_export: 'Invalid export file: %{error}'
unsupported_version: 'Unsupported export version: %{got} (expected %{expected})'
import_failed: 'Import failed: %{error}'
imported_theme_fallback: 'Imported successfully (theme ''%{theme}'' not found, using default)'
imported_success: 'Imported successfully'
adaptive_unavailable: 'Adaptive ranked mode unavailable: %{error}'
switched_to: 'Switched to %{name}'
layout_changed: 'Layout changed to %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Unknown language: %{key}'
unknown_layout: 'Unknown keyboard layout: %{key}'
unsupported_pair: 'Unsupported language/layout pair: %{language} + %{layout}'
language_blocked: 'Language is blocked by support level: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Back'