Compare commits

..

30 Commits

Author SHA1 Message Date
f855fa5606 Fix i18n of key unlock finger hint 2026-04-12 02:57:59 +00:00
232f93e054 Remove trigram anamoly calculation
Decided that this wasn't worth it and bigram anamolies are enough.
2026-04-11 00:11:45 +00:00
d1fde5c0c1 Use better tab indicator characters
Works in more fonts/terminals.
2026-04-10 20:16:12 +00:00
7e13f73b8c Improve repo diversity in some code syntax languages 2026-03-31 05:18:42 +00:00
eeb48157c5 Increase code syntax diversity, use github permalinks 2026-03-31 04:55:55 +00:00
79021db57f Fix random cycling of ALL code lang/book snippets/passages 2026-03-21 19:44:15 +00:00
01fc609f8f Fix terminal-default cursor color
Some terminal colors made it impossible to see the character underneath
the cursor.
2026-03-21 19:16:01 +00:00
84f4aabdff Add full set of locale translations
Generated using Claude Code / Codex so there may be errors
2026-03-17 05:05:32 +00:00
6d5de33f55 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.
2026-03-17 04:29:25 +00:00
895e04d6ce Multilingual dictionaries and keyboard layouts 2026-03-06 04:49:51 +00:00
f20fa6110d Licence compliance stuff 2026-03-01 05:13:29 +00:00
5c56a9c3c6 More balanced adaptive drill generation, Tab fixes, mouse control tweaks 2026-02-28 21:11:11 +00:00
8b8703b9b9 Mouse input improvements 2026-02-28 17:56:09 +00:00
7c1aad84af Mouse support & branch milestone popups 2026-02-28 07:25:40 +00:00
8e4f9bf064 Skill Tree page UI tweaks and improvements 2026-02-28 06:03:20 +00:00
b37dc72b45 Various UI fixes, better capital letter injection, paginated history 2026-02-28 05:07:33 +00:00
c67ddf577a Improve synthetic data in test profiles 2026-02-28 03:41:28 +00:00
de236284ea Enhanced paith input with cursor navigation and tab completion in settings import/export 2026-02-28 03:19:38 +00:00
ca2a3507f4 Create test user profiles to test skill progression 2026-02-28 02:02:39 +00:00
da907c0f46 Prevent tests from writing to user data 2026-02-27 05:39:33 +00:00
a088075924 Adaptive auto-continue input lock overlay 2026-02-27 02:31:37 +00:00
3ef433404e Increase adaptive drill word diversity 2026-02-26 21:33:16 +00:00
54ddebf054 N-gram metrics overhaul & UI improvements 2026-02-26 01:26:25 -05:00
e7f57dd497 N-gram error tracking for adaptive drill selection 2026-02-24 14:55:51 -05:00
0c5a70d5c4 Fix some theme colors & drill summary delete continue 2026-02-22 21:23:13 -05:00
f8bcad247b Fix kitty protocol, caps lock, code source desc 2026-02-22 16:51:34 -05:00
9d59c265dd Tweak value display in statistics keyboard visualizer 2026-02-22 15:28:05 -05:00
9deffc3d1d Import/export feature for config and data 2026-02-22 07:36:34 +00:00
9cc8a214ad Split up and clean up Keyboard Explorer detail stats 2026-02-20 23:44:31 +00:00
9e0411e1f4 Key milestone overlays + keyboard diagram improvements
Also splits out a separate store for ranked stats from overall key
stats.
2026-02-20 23:15:13 +00:00
134 changed files with 226379 additions and 2277 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target
/clones/
/test-profiles/

392
Cargo.lock generated
View File

@@ -26,6 +26,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "0.6.21"
@@ -82,6 +88,15 @@ version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
[[package]]
name = "arc-swap"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5"
dependencies = [
"rustversion",
]
[[package]]
name = "atomic"
version = "0.6.1"
@@ -103,6 +118,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base62"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111"
[[package]]
name = "base64"
version = "0.22.1"
@@ -145,6 +166,16 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.19.1"
@@ -163,6 +194,12 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "castaway"
version = "0.2.4"
@@ -208,6 +245,33 @@ dependencies = [
"windows-link",
]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.5.57"
@@ -302,6 +366,67 @@ dependencies = [
"libc",
]
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools 0.10.5",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools 0.10.5",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.28.1"
@@ -345,6 +470,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -691,6 +822,36 @@ dependencies = [
"wasip2",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "globset"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "globwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
"bitflags 1.3.2",
"ignore",
"walkdir",
]
[[package]]
name = "h2"
version = "0.4.13"
@@ -710,6 +871,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -727,6 +899,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
@@ -983,6 +1161,22 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "ignore"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -1031,12 +1225,41 @@ dependencies = [
"serde",
]
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
@@ -1080,14 +1303,20 @@ dependencies = [
"anyhow",
"chrono",
"clap",
"criterion",
"crossterm 0.28.1",
"dirs",
"icu_normalizer",
"rand",
"ratatui",
"regex",
"reqwest",
"rust-embed",
"rust-i18n",
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"thiserror 2.0.18",
"toml",
]
@@ -1272,6 +1501,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "normpath"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-conv"
version = "0.2.0"
@@ -1319,6 +1557,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "openssl"
version = "0.10.75"
@@ -1520,6 +1764,34 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -1628,7 +1900,7 @@ dependencies = [
"compact_str",
"hashbrown",
"indoc",
"itertools",
"itertools 0.14.0",
"kasuari",
"lru",
"strum",
@@ -1681,7 +1953,7 @@ dependencies = [
"hashbrown",
"indoc",
"instability",
"itertools",
"itertools 0.14.0",
"line-clipping",
"ratatui-core",
"strum",
@@ -1690,6 +1962,26 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -1829,6 +2121,60 @@ dependencies = [
"walkdir",
]
[[package]]
name = "rust-i18n"
version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332"
dependencies = [
"globwalk",
"once_cell",
"regex",
"rust-i18n-macro",
"rust-i18n-support",
"smallvec",
]
[[package]]
name = "rust-i18n-macro"
version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965"
dependencies = [
"glob",
"once_cell",
"proc-macro2",
"quote",
"rust-i18n-support",
"serde",
"serde_json",
"serde_yaml",
"syn 2.0.114",
]
[[package]]
name = "rust-i18n-support"
version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19"
dependencies = [
"arc-swap",
"base62",
"globwalk",
"itertools 0.11.0",
"lazy_static",
"normpath",
"once_cell",
"proc-macro2",
"regex",
"serde",
"serde_json",
"serde_yaml",
"siphasher",
"toml",
"triomphe",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -2026,6 +2372,19 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha2"
version = "0.10.9"
@@ -2357,6 +2716,16 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tokio"
version = "1.49.0"
@@ -2509,6 +2878,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "triomphe"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39"
dependencies = [
"arc-swap",
"serde",
"stable_deref_trait",
]
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -2545,7 +2925,7 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
dependencies = [
"itertools",
"itertools 0.14.0",
"unicode-segmentation",
"unicode-width",
]
@@ -2556,6 +2936,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"

View File

@@ -3,6 +3,7 @@ name = "keydr"
version = "0.1.0"
edition = "2024"
description = "Terminal typing tutor with adaptive learning"
license = "AGPL-3.0-only"
[dependencies]
ratatui = { version = "0.30", features = ["crossterm_0_28"] }
@@ -18,6 +19,22 @@ chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
thiserror = "2.0"
reqwest = { version = "0.12", features = ["blocking"], optional = true }
icu_normalizer = { version = "2.1", default-features = false, features = ["compiled_data"] }
rust-i18n = "3"
[dev-dependencies]
tempfile = "3"
serde_yaml = "0.9"
regex = "1"
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "ngram_benchmarks"
harness = false
[[bin]]
name = "generate_test_profiles"
path = "src/bin/generate_test_profiles.rs"
[features]
default = ["network"]

661
LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

9
NOTICE Normal file
View File

@@ -0,0 +1,9 @@
keydr
Copyright (C) 2026 keydr contributors
This program is licensed under the GNU Affero General Public License,
version 3 only (AGPL-3.0-only). See the LICENSE file for details.
This repository includes third-party content from keybr.com.
See THIRD_PARTY_NOTICES.md for attribution and provenance.
See docs/license-compliance.md for compliance process notes.

68
THIRD_PARTY_NOTICES.md Normal file
View File

@@ -0,0 +1,68 @@
# Third-Party Notices
## Included third-party material in this repository
### keybr.com
- Upstream project: <https://github.com/aradzie/keybr.com>
- Upstream license: GNU Affero General Public License v3.0
- Local upstream license copy (for local research clone): `clones/keybr.com/LICENSE`
1. `assets/dictionaries/words-*.json` (seeded Latin-script set)
- Sources: `clones/keybr.com/packages/keybr-content-words/lib/data/words-<lang>.json`
- Included language keys: `en, de, es, fr, it, pt, nl, sv, da, nb, fi, pl, cs, ro, hr, hu, lt, lv, sl, et, tr`
- Status: included in this repository and available to `src/generator/dictionary.rs`
- Modifications: none (byte-identical at the time of import)
- Integrity metadata:
- `assets/dictionaries/manifest.tsv` (language/file/source mapping)
- `assets/dictionaries/SHA256SUMS` (checksum manifest)
- `assets/dictionaries/words-<lang>.json.license` (per-file provenance/license sidecar)
## Local research clones (not committed to this repository)
The `clones/` directory is gitignored and used for local research only.
### keybr.com
- License file in local clone: `clones/keybr.com/LICENSE`
- Upstream states AGPLv3 in README/license materials.
### typr
- License file in local clone: `clones/typr/LICENSE`
- License text present in clone is GPLv3.
## Runtime-downloaded content (not version-controlled by default)
This project can download third-party source content at runtime:
- Code snippets from repositories listed in `src/generator/code_syntax.rs`
- Passage text from Project Gutenberg URLs in `src/generator/passage.rs`
Downloaded files are stored in user data directories by default (`dirs::data_dir()`),
not in this repository. These downloaded assets keep their original upstream licenses.
When code snippets are downloaded, keydr now writes a sidecar source manifest
(`*_*.sources.txt`) listing exact source URLs to help with attribution and compliance
if cached content is redistributed.
## Research references from planning docs (idea-only unless noted above)
The following projects are referenced in planning/research docs and were used for
architecture/algorithm ideas:
- keybr.com
- typr
- ttyper
- smassh
- gokeybr
- ivan-volnov/keybr
- keybr-code
For these references, no direct code/data inclusion is claimed in this repository
except the explicitly documented `assets/dictionaries/words-*.json` imports from keybr.com.
## Notes
This repository is licensed under AGPL-3.0-only to remain compatible with included
AGPL-licensed upstream material.

View File

@@ -0,0 +1,21 @@
30a78612b478f8f9101e200b96ddf2807720a2b513ec6d05a73abdde99354407 words-cs.json
8098e39c9deb00db59d85f82c9bc791536b51c8fa2a5b688f771f120e83bbc26 words-da.json
014d7ff2f7756b1a0775b975e325bf75076770f0d4e6f9ebed771fa6aacb7ed5 words-de.json
067adf66de5f0a7ca17f3bf187bab378d8ad71e87856e4a25a208905404b949a words-en.json
fffcb910f0012e62215bfa2a8ed34ecc3d54cbf04a658c3bce5bee8148abf634 words-es.json
bfd0d22dbc129c3d693d5afbf39aaa5506c0c723bf5bb51ef10edd2af3f1c71d words-et.json
2530c4a37311fb93d6f687edb08534eca71f4c775e1a01fca405c783361386fa words-fi.json
3b177fcca8f275cce555ac954fcbbb945b14626a8a235993ff9e9d9767005517 words-fr.json
f439f8bf16f65f8600642906a0967dc1f99992f6a2f3b830bd77554ccb6a07de words-hr.json
44ec5436364a162dc7774be3c40a4678247aa2909eaceefac7f49b3bc00811f5 words-hu.json
03361069ce40d08fa931709ce402811d0f484c32d03878706ad4dcc5e709b01a words-it.json
9239f4042d67127859b3a56da29a6f3df4cd458776483adf561b668f3e646579 words-lt.json
ad24ebd9a36c012ebb8db3db78af5e6038d26b64b3f173885c4a606ab17d3d49 words-lv.json
be83a2cff75097db957575425b4dec658006c9b9e43fdcb7a6eb92701818b752 words-nb.json
0f701c9e5c891dd557a0f4f3a6903b4c9762a2a898f749caccb59efbee189271 words-nl.json
d99e00fb85890847ba783354e148ef835d44faee95c4d7ec227d589cf5b072d3 words-pl.json
fa3009988d7be559a78b6b2c2198628750de77d77e0ee360d8bf5cc8eab84368 words-pt.json
76ec930a9b6aaa8092f2179b0918d71ec61f139843d985f5f25eae07bd7093fc words-ro.json
2960c6db414abb22505a4f78d8292df2b45d7332144302296055fc5a8ee07e23 words-sl.json
154e1b905d10130fee0160d3e2f30bd6445e8da1e3251475df37be364a81bd17 words-sv.json
95f6e867ef64d6a1ddd90f82d574d38b4a2be19d550f613ee87fc3e1701a0d8e words-tr.json

View File

@@ -0,0 +1,22 @@
# language_key file license_file source
en words-en.json words-en.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-en.json
de words-de.json words-de.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-de.json
es words-es.json words-es.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-es.json
fr words-fr.json words-fr.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-fr.json
it words-it.json words-it.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-it.json
pt words-pt.json words-pt.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-pt.json
nl words-nl.json words-nl.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-nl.json
sv words-sv.json words-sv.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-sv.json
da words-da.json words-da.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-da.json
nb words-nb.json words-nb.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-nb.json
fi words-fi.json words-fi.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-fi.json
pl words-pl.json words-pl.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-pl.json
cs words-cs.json words-cs.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-cs.json
ro words-ro.json words-ro.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-ro.json
hr words-hr.json words-hr.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-hr.json
hu words-hu.json words-hu.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-hu.json
lt words-lt.json words-lt.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-lt.json
lv words-lv.json words-lv.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-lv.json
sl words-sl.json words-sl.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-sl.json
et words-et.json words-et.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-et.json
tr words-tr.json words-tr.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-tr.json
1 # language_key file license_file source
2 en words-en.json words-en.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-en.json
3 de words-de.json words-de.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-de.json
4 es words-es.json words-es.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-es.json
5 fr words-fr.json words-fr.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-fr.json
6 it words-it.json words-it.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-it.json
7 pt words-pt.json words-pt.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-pt.json
8 nl words-nl.json words-nl.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-nl.json
9 sv words-sv.json words-sv.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-sv.json
10 da words-da.json words-da.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-da.json
11 nb words-nb.json words-nb.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-nb.json
12 fi words-fi.json words-fi.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-fi.json
13 pl words-pl.json words-pl.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-pl.json
14 cs words-cs.json words-cs.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-cs.json
15 ro words-ro.json words-ro.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-ro.json
16 hr words-hr.json words-hr.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-hr.json
17 hu words-hu.json words-hu.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-hu.json
18 lt words-lt.json words-lt.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-lt.json
19 lv words-lv.json words-lv.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-lv.json
20 sl words-sl.json words-sl.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-sl.json
21 et words-et.json words-et.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-et.json
22 tr words-tr.json words-tr.json.license clones/keybr.com/packages/keybr-content-words/lib/data/words-tr.json

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-cs.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-da.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-de.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-en.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-es.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-et.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-fi.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-fr.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-hr.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-hu.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-it.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-lt.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-lv.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-nb.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-nl.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-pl.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-pt.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-ro.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-sl.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-sv.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
This file is sourced from keybr.com:
clones/keybr.com/packages/keybr-content-words/lib/data/words-tr.json
Upstream project: https://github.com/aradzie/keybr.com
Upstream license: GNU Affero General Public License v3.0
Local project license: AGPL-3.0-only (see /LICENSE).

133
benches/ngram_benchmarks.rs Normal file
View File

@@ -0,0 +1,133 @@
use criterion::{Criterion, black_box, criterion_group, criterion_main};
use keydr::engine::key_stats::KeyStatsStore;
use keydr::engine::ngram_stats::{
BigramKey, BigramStatsStore, extract_ngram_events,
};
use keydr::session::result::KeyTime;
fn make_keystrokes(count: usize) -> Vec<KeyTime> {
let chars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
(0..count)
.map(|i| KeyTime {
key: chars[i % chars.len()],
time_ms: 200.0 + (i % 50) as f64,
correct: i % 7 != 0, // ~14% error rate
})
.collect()
}
fn bench_extraction(c: &mut Criterion) {
let keystrokes = make_keystrokes(500);
c.bench_function("extract_bigrams (500 keystrokes)", |b| {
b.iter(|| extract_ngram_events(black_box(&keystrokes), 800.0))
});
}
fn bench_update(c: &mut Criterion) {
let keystrokes = make_keystrokes(500);
let bigram_events = extract_ngram_events(&keystrokes, 800.0);
c.bench_function("bigram_stats update (400 events)", |b| {
b.iter(|| {
let mut store = BigramStatsStore::default();
for ev in bigram_events.iter().take(400) {
store.update(
black_box(ev.key.clone()),
black_box(ev.total_time_ms),
black_box(ev.correct),
black_box(ev.has_hesitation),
0,
);
}
store
})
});
}
fn bench_focus_selection(c: &mut Criterion) {
// Use a-z + A-Z + 0-9 = 62 chars for up to 3844 unique bigrams
let all_chars: Vec<char> = ('a'..='z').chain('A'..='Z').chain('0'..='9').collect();
let mut bigram_stats = BigramStatsStore::default();
let mut char_stats = KeyStatsStore::default();
for &ch in &all_chars {
let stat = char_stats.stats.entry(ch).or_default();
stat.confidence = 0.8;
stat.filtered_time_ms = 430.0;
stat.sample_count = 50;
stat.total_count = 50;
stat.error_count = 3;
}
let mut count: usize = 0;
for &a in &all_chars {
for &b in &all_chars {
if bigram_stats.stats.len() >= 3000 {
break;
}
let key = BigramKey([a, b]);
let stat = bigram_stats.stats.entry(key).or_default();
stat.confidence = 0.5 + (count % 50) as f64 * 0.01;
stat.sample_count = 25 + count % 30;
stat.error_count = 5 + count % 10;
stat.redundancy_streak = if count % 3 == 0 { 3 } else { 1 };
count += 1;
}
}
assert_eq!(bigram_stats.stats.len(), 3000);
let unlocked: Vec<char> = all_chars;
c.bench_function("weakest_bigram (3K entries)", |b| {
b.iter(|| bigram_stats.weakest_bigram(black_box(&char_stats), black_box(&unlocked)))
});
}
fn bench_history_replay(c: &mut Criterion) {
// Build 500 drills of ~300 keystrokes each
let drills: Vec<Vec<KeyTime>> = (0..500).map(|_| make_keystrokes(300)).collect();
c.bench_function("history replay (500 drills x 300 keystrokes)", |b| {
b.iter(|| {
let mut bigram_stats = BigramStatsStore::default();
let mut key_stats = KeyStatsStore::default();
for (drill_idx, keystrokes) in drills.iter().enumerate() {
let bigram_events = extract_ngram_events(keystrokes, 800.0);
for kt in keystrokes {
if kt.correct {
let stat = key_stats.stats.entry(kt.key).or_default();
stat.total_count += 1;
} else {
key_stats.update_key_error(kt.key);
}
}
for ev in &bigram_events {
bigram_stats.update(
ev.key.clone(),
ev.total_time_ms,
ev.correct,
ev.has_hesitation,
drill_idx as u32,
);
}
}
(bigram_stats, key_stats)
})
});
}
criterion_group!(
benches,
bench_extraction,
bench_update,
bench_focus_selection,
bench_history_replay,
);
criterion_main!(benches);

View File

@@ -0,0 +1,42 @@
# License Compliance Notes
This repository includes AGPL-licensed upstream material and is licensed as
`AGPL-3.0-only`.
## What is included in-repo
- `assets/dictionaries/words-*.json` are imported from keybr.com and tracked in
`THIRD_PARTY_NOTICES.md`.
- `assets/dictionaries/words-<lang>.json.license` records source and license for
each imported dictionary file.
- `assets/dictionaries/manifest.tsv` maps language keys to imported files/sources.
- `assets/dictionaries/SHA256SUMS` stores dictionary checksums for integrity verification.
- `scripts/validate_dictionary_manifest.sh` validates manifest entries, sidecars,
and checksums.
- `scripts/derive_primary_letter_sequences.py` derives per-language primary-letter
sequence seed data from dictionary frequency.
- `assets/dictionaries/primary-letter-sequences.tsv` stores the current derived output.
- `docs/unicode-normalization-policy.md` documents NFC normalization policy and
equivalence expectations.
## What is research-only
- The `clones/` directory is gitignored and used for local analysis only.
- References in `docs/plans/` to third-party projects are primarily idea-level
research unless explicitly documented as imported content.
## Runtime downloads
- Code drills can download source files from upstream GitHub repositories.
- Passage drills can download texts from Project Gutenberg.
- Downloaded content is cached in user data directories by default, not in this repo.
- Downloaded code snippet caches include a `*.sources.txt` sidecar with source URLs.
## Ongoing checklist for compliance
1. If you import any third-party file into the repository, add it to
`THIRD_PARTY_NOTICES.md`.
2. Add a sidecar `filename.license` (or equivalent) with source and license.
3. Keep the project license compatible with imported copyleft obligations.
4. If you later commit downloaded snippet caches or passage corpora, include
attribution and the relevant upstream license terms for those files.

View File

@@ -0,0 +1,188 @@
# Import/Export Feature Plan
## Context
Users need a way to back up and transfer their keydr data between machines. Currently, data is spread across `~/.config/keydr/config.toml` (config) and `~/.local/share/keydr/*.json` (profile, key stats, drill history). This feature adds Export and Import actions to the Settings page, producing/consuming a single combined JSON file.
## Export Format
Canonical filename: `keydr-export-2026-02-21.json` (date is `Utc::now()`).
```json
{
"keydr_export_version": 1,
"exported_at": "2026-02-21T12:00:00Z",
"config": { ... },
"profile": { ... },
"key_stats": { ... },
"ranked_key_stats": { ... },
"drill_history": { ... }
}
```
- `exported_at` uses `DateTime<Utc>` (chrono, serialized as RFC3339).
- On import, `keydr_export_version` is checked: if it does not equal the current supported version (1), import is rejected with the error `"Unsupported export version: {v} (expected 1)"`. Future versions can add migration functions as needed.
## Import Scope
Import applies **everything except machine-local path fields**:
- **Imported**: target_wpm, theme, keyboard_layout, word_count, code_language, passage_book, download toggle booleans, snippets_per_repo, paragraphs_per_book, onboarding flags, and all progress data (profile, key stats, drill history).
- **Preserved from current config**: `code_download_dir`, `passage_download_dir` (machine-local paths stay as-is).
- Theme and keyboard_layout are imported as-is. If the imported theme is unavailable on the target machine, `Theme::load()` falls back to `terminal-default` and the success message includes a note: `"Imported successfully (theme '{name}' not found, using default)"`.
## Changes
### 1. Add export data struct (`src/store/schema.rs`)
Add an `ExportData` struct with all the fields above, deriving `Serialize`/`Deserialize`. Include `keydr_export_version: u32` and `exported_at: DateTime<Utc>` metadata fields.
### 2. Add export/import methods to `JsonStore` (`src/store/json_store.rs`)
- `export_all(&self, config: &Config) -> Result<ExportData>` — loads all data files and bundles with config into `ExportData`.
- `import_all(&self, data: &ExportData) -> Result<()>`**transactional two-phase write** with best-effort rollback:
1. **Stage phase**: write each data file to a `.tmp` sibling (profile.json.tmp, key_stats.json.tmp, etc.). If any `.tmp` write fails, delete all `.tmp` files created so far and return an error. Originals are untouched.
2. **Commit phase**: for each file, rename the existing original to `.bak`, then rename `.tmp` to final. If any rename fails mid-sequence, **rollback**: restore all `.bak` files back to their original names and clean up remaining `.tmp` files. After successful commit, delete all `.bak` files.
**Contract**: this is best-effort, not strictly atomic. If the process is killed or the disk fails during the commit phase, `.bak` files may be left behind. On next app startup, if `.bak` files are detected in the data directory, show a warning in the status message: `"Recovery files found from interrupted import. Data may be inconsistent — consider re-importing."` and clean up the `.bak` files.
### 3. Add config validation on import (`src/config.rs`)
Add a `Config::validate(&mut self, valid_language_keys: &[&str])` method that:
- Clamps `target_wpm` to 10..=200
- Clamps `word_count` to 5..=100
- Calls `normalize_code_language()` for code language validation
- Falls back to defaults for unrecognized theme names (via `Theme::load()` fallback, already handled)
This is called after merging imported config fields, before saving.
### 4. Add status message enum and app state fields (`src/app.rs`)
Add a structured status type:
```rust
pub enum StatusKind { Success, Error }
pub struct StatusMessage { pub kind: StatusKind, pub text: String }
```
New fields on `App`:
- `pub settings_confirm_import: bool` — controls the import warning dialog
- `pub settings_export_conflict: bool` — controls the export overwrite conflict dialog
- `pub settings_status_message: Option<StatusMessage>` — transient status, cleared on next keypress
- `pub settings_export_path: String` — editable export destination path
- `pub settings_import_path: String` — editable import source path
- `pub settings_editing_export_path: bool` — whether export path is being edited
- `pub settings_editing_import_path: bool` — whether import path is being edited
**Invariant**: at most one modal/edit state is active at a time. When entering any modal (confirm_import, export_conflict) or edit mode, clear all other modal/edit flags first.
Default export path: `dirs::download_dir()` / `keydr-export-{YYYY-MM-DD}.json`.
Default import path: same canonical filename (`dirs::download_dir()` / `keydr-export-{YYYY-MM-DD}.json`), editable.
If `dirs::download_dir()` returns `None`, fall back to `dirs::home_dir()`, then `"."`. On export, if the parent directory of the target path doesn't exist, return an error `"Directory does not exist: {parent}"` rather than silently creating it.
### 5. Add app methods (`src/app.rs`)
- `export_data()` — builds `ExportData` from current state, writes JSON to `settings_export_path` via **atomic write** (write to `.tmp` in same directory, then rename to final path). If file already exists at that path, sets `settings_export_conflict = true` instead of writing. Sets `StatusMessage` on success/error.
- `export_data_overwrite()` — calls the same atomic-write logic without the existence check. The rename atomically replaces the old file; no pre-delete needed.
- `export_data_rename()` — delegates to `next_available_path()`, a free function that implements **conditional suffix normalization**: strips a trailing `-N` suffix only when the base file (without suffix) exists in the same directory. This prevents accidental stripping of intrinsic name components (e.g. date segments like `-01`). Then scans for the lowest unused `-N` suffix. Works for any filename. E.g. if `my-backup.json` and `my-backup-1.json` exist, picks `my-backup-2.json`. If called with `my-backup-1.json` (and `my-backup.json` exists), normalizes to `my-backup` then picks `-2`. Updates `settings_export_path` and writes via atomic write.
- `import_data()` — reads file at `settings_import_path`, validates `keydr_export_version` (reject if != 1 with error message), calls `store.import_all()`, then reloads all in-memory state (config with path fields preserved, profile, key_stats, ranked_key_stats, drill_history, skill_tree). Calls `Config::validate()` and `Config::save()`. Checks if imported theme loaded successfully and appends fallback note to success message if not. Sets `StatusMessage` on success/error.
### 6. Add settings entries (`src/main.rs` — `render_settings`)
Add four new rows at the bottom of the settings field list:
- **"Export Path"** — editable path field, shows `settings_export_path` (same pattern as Code Download Dir)
- **"Export Data"** — action button, label: `"Export now"`
- **"Import Path"** — editable path field, shows `settings_import_path`
- **"Import Data"** — action button, label: `"Import now"`
Update `MAX_SETTINGS` accordingly in `handle_settings_key`.
### 7. Handle key input (`src/main.rs` — `handle_settings_key`)
**Priority order at top of function:**
1. If `settings_status_message.is_some()` — any keypress clears it and returns (message dismissed).
2. If `settings_export_conflict` — handle conflict dialog:
- `'d'``export_data_overwrite()`, clear conflict flag
- `'r'``export_data_rename()`, clear conflict flag
- `Esc` → clear conflict flag
- Return early.
3. If `settings_confirm_import` — handle import confirmation:
- `'y'``import_data()`, clear flag
- `'n'` / `Esc` → clear flag
- Return early.
4. If editing export/import path — handle typing (same pattern as `settings_editing_download_dir`).
For the Enter handler on the new indices:
- Export Path → enter editing mode (clear other edit/modal flags first)
- Export Data → call `export_data()`
- Import Path → enter editing mode (clear other edit/modal flags first)
- Import Data → set `settings_confirm_import = true` (clear other flags first)
Add new indices to the exclusion lists for left/right cycling.
### 8. Render dialogs (`src/main.rs` — `render_settings`)
**Import confirmation dialog** (when `settings_confirm_import` is true):
- Dialog size: ~52x7, centered
- Border title: `" Confirm Import "`, border color: `colors.error()`
- Line 1: `"This will erase your current data."`
- Line 2: `"Export first if you want to keep it."`
- Line 3: `"Proceed? (y/n)"`
**Export conflict dialog** (when `settings_export_conflict` is true):
- Dialog size: ~52x7, centered
- Border title: `" File Exists "`, border color: `colors.error()`
- Line 1: `"A file already exists at this path."`
- Line 2: `"[d] Overwrite [r] Rename [Esc] Cancel"`
**Status message dialog** (when `settings_status_message` is `Some`):
- Small centered dialog showing the message text
- `StatusKind::Success` → accent color border. `StatusKind::Error` → error color border.
- Footer: `"Press any key"`
Dialog rendering priority: status message > export conflict > import confirmation (only one shown at a time).
### 9. Automated tests (`src/store/json_store.rs` or new test module)
Add tests for:
- **Round-trip**: export then import produces identical data
- **Transactional safety (supplemental)**: use a `tempdir`, write valid data, then import into a read-only tempdir and verify original files are unchanged
- **Staged write failure**: `import_all` with a poisoned `ExportData` (e.g. containing data that serializes but whose target path is manipulated to fail) verifies `.tmp` cleanup and original file preservation — this provides deterministic failure coverage without platform-dependent permission tricks
- **Version rejection**: import with `keydr_export_version: 99` returns error containing `"Unsupported export version"`
- **Config validation**: import with out-of-range values (target_wpm=0, word_count=999) gets clamped to valid ranges
- **Smart rename suffix**: create files `stem.json`, `stem-1.json` in a tempdir, verify rename picks `stem-2.json`; also test with custom (non-canonical) filenames
- **Modal invariant**: verify that setting any modal/edit flag clears all others
## Key Files to Modify
| File | Changes |
|------|---------|
| `src/store/schema.rs` | Add `ExportData` struct |
| `src/store/json_store.rs` | Add `export_all()`, transactional `import_all()` with rollback, `.bak` cleanup on startup, tests |
| `src/app.rs` | Add `StatusKind`/`StatusMessage`, state fields, export/import/rename methods, `.bak` check on init |
| `src/main.rs` | Settings UI entries, key handling, 3 dialog types, path editing |
| `src/config.rs` | Add `validate()` method |
## Deferred / Out of Scope
- **Settings enum refactor**: The hard-coded index pattern is pre-existing across the entire settings system. Refactoring to an enum/action map is worthwhile but out of scope for this feature.
- **Splitting config into portable vs machine-local structs**: Handled pragmatically by preserving path fields during import rather than restructuring Config.
- **IO abstraction for injectable writers**: The existing codebase uses direct `fs` calls throughout. Adding a trait-based abstraction for testability is a larger refactor. We use a poisoned-data test and a supplemental read-only tempdir test instead.
## Verification
1. `cargo build` — compiles without errors
2. `cargo test` — all new tests pass (round-trip, staged failure, version rejection, validation, rename suffix, modal invariant)
3. Launch app → Settings → verify Export Path / Export Data / Import Path / Import Data rows appear
4. Edit export path → verify typing/backspace works
5. Export → verify JSON file created at specified path with correct structure
6. Export again same day → verify conflict dialog appears; `d` overwrites atomically, `r` renames to `-1`
7. Export a third time → verify `r` renames to `-2` (smart suffix increment)
8. Export with custom filename → verify rename appends `-1` correctly
9. Import with bad version → verify error: `"Unsupported export version: 99 (expected 1)"`
10. Import → verify warning dialog appears; `n`/`Esc` cancels without changes
11. Import → `y` → verify data loaded, config preferences updated, paths preserved
12. Import with unavailable theme → verify success message includes fallback note
13. Verify only one modal/edit state can be active: e.g. while editing export path, pressing a key that would open import confirm does not open it
14. Round-trip: export, change settings, do a drill, import the export, verify original state restored

View File

@@ -0,0 +1,530 @@
# Plan: Key Milestone Overlays + Keyboard Diagram Improvements
## Context
The app progressively unlocks keys as users master them via the skill tree system. Currently, when a key is unlocked or mastered, there's no celebratory feedback. This plan adds encouraging milestone overlays with keyboard visualization and finger guidance. It also improves the keyboard diagram to render modifier keys (shift, tab, enter, space, backspace) as interactive keys rather than static labels, and adds a new Keyboard Explorer screen.
## Implementation Phases
This plan is structured into 5 independent phases that can be implemented and validated separately to reduce regression risk.
---
## Phase 0: Key Display Adapter (prerequisite for all phases)
**File: `src/keyboard/display.rs` (new)**
Add a thin adapter module that centralizes all sentinel-char ↔ display-name conversions. This isolates encoding concerns so that UI, stats, and rendering code never directly match on sentinel chars.
```rust
/// Human-readable display name for a key character (including sentinels).
pub fn key_display_name(ch: char) -> &'static str {
match ch {
'\x08' => "Backspace",
'\t' => "Tab",
'\n' => "Enter",
' ' => "Space",
_ => "", // caller uses ch.to_string() for printable chars
}
}
/// Short label for compact UI contexts (heatmaps, compact keyboard).
pub fn key_short_label(ch: char) -> &'static str {
match ch {
'\x08' => "Bksp",
'\t' => "Tab",
'\n' => "Ent",
' ' => "Spc",
_ => "",
}
}
/// All sentinel chars used for non-printable keys.
pub const MODIFIER_SENTINELS: &[char] = &['\x08', '\t', '\n'];
```
Register in `src/keyboard/mod.rs` with `pub mod display;`.
All subsequent phases use these functions instead of inline sentinel matching. This makes future migration to a typed `KeyId` a single-module change.
**Sentinel boundary policy:** Sentinel chars (`'\x08'`, `'\t'`, `'\n'`) are allowed only at two boundaries:
1. **Input boundary**`handle_key` in `src/main.rs` converts `KeyCode::Backspace/Tab/Enter` to sentinels for `depressed_keys` and drill input.
2. **Storage boundary**`KeyStatsStore` and `drill_history` store sentinels as `char` keys.
All UI rendering, stats display, and business logic must consume the adapter functions (`key_display_name`, `key_short_label`, `MODIFIER_SENTINELS`) rather than matching sentinels directly. Add a code comment at the top of `display.rs` documenting this policy.
**Enforcement:** Add a `#[test]` in `display.rs` that runs `grep -rn '\\\\x08\|\\\\t.*=>\|\\\\n.*=>' src/` (or equivalent) and asserts that direct sentinel matches only appear in allowed files (`display.rs`, `main.rs` input handling, `key_stats.rs`). This is a lightweight lint that catches accidental sentinel leakage in UI/business logic during `cargo test` without requiring CI changes.
---
## Phase 1: Keyboard Diagram — Add Missing Keys & Shift Support
### 1a. Track modifier keys as depressed keys
**File: `src/main.rs` — `handle_key` function (~line 155)**
Currently only `KeyCode::Char(ch)` inserts into `depressed_keys`. Add tracking for:
- `KeyCode::Backspace` → insert `'\x08'` into `depressed_keys`
- `KeyCode::Tab` → insert `'\t'`
- `KeyCode::Enter` → insert `'\n'`
- Shift state is already tracked via `app.shift_held`
On `Release` events, remove these sentinels similarly to how `Char` releases work. The tick-based fallback clear (line 134-143) already handles `depressed_keys.clear()` which covers these sentinels too.
### 1b. Render modifier keys in the keyboard diagram
**File: `src/ui/components/keyboard_diagram.rs`**
Currently, modifiers are rendered as plain text labels (lines 253-286). Change them to rendered key boxes that participate in the highlight/depress system:
- **Row 0 (number row):** Add `[Bksp]` key after `=`/`+`. Highlight when `'\x08'` is in `depressed_keys`. Finger color: Right Pinky.
- **Row 1 (top row):** Add `[Tab]` key before `q`. Highlight when `'\t'` is in `depressed_keys`. Finger color: Left Pinky.
- **Row 2 (home row):** Add `[Enter]` key after `'`/`"`. Highlight when `'\n'` is in `depressed_keys`. Finger color: Right Pinky.
- **Row 3 (bottom row):** Add `[Shft]` at start and end. Highlight when `shift_held` is true. Left Shift = Left Pinky finger color, Right Shift = Right Pinky finger color.
- **Row 4 (new row):** Add `[ Space ]` centered. Highlight when `' '` is in `depressed_keys`. Finger color: Thumb.
Full layout visualization (full mode):
```
[ ` ][ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][ 0 ][ - ][ = ][Bksp]
[Tab][ q ][ w ][ e ][ r ][ t ][ y ][ u ][ i ][ o ][ p ][ [ ][ ] ][ \ ]
[ ][ a ][ s ][ d ][ f ][ g ][ h ][ j ][ k ][ l ][ ; ][ ' ][Enter]
[Shft][ z ][ x ][ c ][ v ][ b ][ n ][ m ][ , ][ . ][ / ][Shft]
[ Space ]
```
Note: Row 2 position before `a` renders as `[ ]` (caps lock, unused).
When `shift_held` is true:
- Shift keys light up with their finger color (brightened)
- All character keys show shifted variants (already implemented via `shift_held` field)
Compact mode: add `[S]` on each side of bottom row, and abbreviated `[T]`, `[E]`, `[B]` for Tab/Enter/Backspace where space permits.
Adaptive breakpoints for overlay/small terminal: if inner height < 6, skip space row; if < 5, use compact mode.
### 1c. Height adjustments
**File: `src/main.rs` — `render_drill` (~line 1011-1019)**
The `kbd_height` calculation needs to increase by 1-2 rows for the space row and modifier keys in full mode. Update:
- Full mode: `kbd_height = 8` (5 rows + 2 border + 1 spacing)
- Compact mode: `kbd_height = 6` (4 rows + 2 border)
### Phase 1 Verification
- `cargo build && cargo test`
- Press backspace during drill → verify `[Bksp]` lights up
- Press tab → verify `[Tab]` lights up
- Press enter → verify `[Enter]` lights up
- Press shift → verify both `[Shft]` keys light up and all keys show shifted variants
- Type space → verify space bar lights up
- Verify compact mode works on narrow terminals
---
## Phase 2: Key Milestone Detection
### 2a. Return change events from `SkillTree::update`
**File: `src/engine/skill_tree.rs`**
Add a return type:
```rust
pub struct SkillTreeUpdate {
pub newly_unlocked: Vec<char>,
pub newly_mastered: Vec<char>,
}
```
Modify `update()` to:
1. Snapshot current unlocked keys (via `unlocked_keys(DrillScope::Global)`) as a `HashSet<char>` before changes
2. Snapshot per-key confidence before changes (for keys currently unlocked)
3. Perform existing update logic
4. Snapshot unlocked keys after
5. `newly_unlocked` = keys in after but not in before
6. `newly_mastered` = keys where confidence was < 1.0 before but >= 1.0 after (only check keys in the before set)
### 2b. Finger info text generation
**File: `src/keyboard/finger.rs`**
Add `description()` method to `FingerAssignment`:
```rust
pub fn description(&self) -> &'static str {
match (self.hand, self.finger) {
(Hand::Left, Finger::Pinky) => "left pinky",
(Hand::Left, Finger::Ring) => "left ring finger",
(Hand::Left, Finger::Middle) => "left middle finger",
(Hand::Left, Finger::Index) => "left index finger",
(Hand::Left, Finger::Thumb) => "left thumb",
(Hand::Right, Finger::Pinky) => "right pinky",
(Hand::Right, Finger::Ring) => "right ring finger",
(Hand::Right, Finger::Middle)=> "right middle finger",
(Hand::Right, Finger::Index) => "right index finger",
(Hand::Right, Finger::Thumb) => "right thumb",
}
}
```
Finger info is looked up via `KeyboardModel::finger_for_char(ch)` which uses position-based mapping that works across all layouts (QWERTY, Dvorak, Colemak).
### 2c. Find key's skill tree location
**File: `src/engine/skill_tree.rs`**
Add helper:
```rust
pub fn find_key_branch(ch: char) -> Option<(&'static BranchDefinition, &'static str, usize)> {
// Returns (branch_def, level_name, 1-based position_in_level)
for branch in ALL_BRANCHES {
for level in branch.levels {
if let Some(pos) = level.keys.iter().position(|&k| k == ch) {
return Some((branch, level.name, pos + 1));
}
}
}
None
}
```
### Phase 2 Verification
- `cargo test` — existing tests pass
- Add unit test: `update()` returns correct `newly_unlocked` when keys are unlocked
- Add unit test: `update()` returns correct `newly_mastered` when confidence crosses 1.0
- Add unit test: `find_key_branch('e')` returns `(Lowercase, "Frequency Order", 1)`
---
## Phase 3: Milestone Overlay UI
### 3a. Milestone data structures
**File: `src/app.rs`**
Add to `App`:
```rust
pub milestone_queue: VecDeque<KeyMilestonePopup>,
```
Types (can live in `app.rs` or a new `src/milestone.rs`):
```rust
pub struct KeyMilestonePopup {
pub kind: MilestoneKind,
pub keys: Vec<char>,
pub finger_info: Vec<(char, String)>, // (key, "left ring finger")
}
pub enum MilestoneKind {
Unlock,
Mastery,
}
```
### 3b. Capture milestone events in `finish_drill`
**File: `src/app.rs` — `finish_drill` (~line 485)**
After `self.skill_tree.update(&self.key_stats)` (line 502), capture the `SkillTreeUpdate`. If `newly_unlocked` is non-empty, push an Unlock milestone to the queue with finger info for each key. If `newly_mastered` is non-empty, push a Mastery milestone to the queue. Both can be queued — they'll show one at a time.
Build finger info using `self.keyboard_model.finger_for_char(ch).description()`.
**Multi-key milestones:** Each `KeyMilestonePopup` can contain multiple keys (e.g., if 3 keys unlock in one drill completion). The overlay shows all keys together: "You unlocked: 'e', 'r', 'i'" with finger info for each. This is preferred over one overlay per key to avoid a long queue of nearly identical overlays. If both unlocks and masteries occur, they are separate milestones in the queue (one Unlock overlay, one Mastery overlay).
For shifted characters, also include shift key guidance:
- Left-hand characters → "Hold Right Shift (right pinky)"
- Right-hand characters → "Hold Left Shift (left pinky)"
### 3c. Milestone overlay rendering
**File: `src/main.rs` — `render_drill`**
After rendering the drill screen, check `app.milestone_queue.front()`. If present, render a centered overlay using `Clear` + bordered block. Layout adapts to terminal size:
- Large terminal (height >= 25): Full keyboard diagram + text
- Medium (height >= 15): Compact keyboard + text
- Small (height < 15): Text only, no keyboard diagram
Overlay content:
- Title: "Key Unlocked!" or "Key Mastered!"
- Key display: "You unlocked: 's'" / "You mastered: 's'"
- Finger info (unlock only): "Use your left ring finger"
- Encouraging message (randomly selected from pool)
- Keyboard diagram with `focused_key` set to the **first key** in the milestone's key list. For multi-key milestones, only the first key is highlighted on the diagram; all keys are listed textually above.
- For shifted characters: `shift_held = true` on diagram
- Footer: "Press any key to continue (Backspace dismisses only)"
Encouraging message pools:
**Unlock:**
- "Nice work! Keep building your typing skills."
- "Another key added to your arsenal!"
- "Your keyboard is growing! Keep it up."
- "One step closer to full keyboard mastery!"
**Mastery:**
- "This key is now at full confidence!"
- "You've got this key down pat!"
- "Muscle memory locked in!"
- "One more key conquered!"
### 3d. Milestone dismissal — per-key-class behavior
**File: `src/main.rs` — `handle_drill_key`**
At the top of `handle_drill_key`, check `app.milestone_queue.front()`. If present, pop the front milestone from the queue, then handle the dismissing key based on its class:
| Key class | Dismiss? | Replay into drill? | Notes |
|---|---|---|---|
| `KeyCode::Char(ch)` | Yes | Yes — fall through to normal input | Most common case; no keystrokes lost |
| `KeyCode::Tab` | Yes | Yes — fall through to tab handling | Tab is valid drill input |
| `KeyCode::Enter` | Yes | Yes — fall through to enter handling | Enter is valid drill input |
| `KeyCode::Backspace` | Yes | No — dismiss only | Replaying backspace would delete progress the user didn't intend to undo |
| `KeyCode::Esc` | Yes | Yes — Esc falls through to drill exit | Clears entire milestone queue and exits drill immediately |
| Other (arrows, etc.) | Yes | No — dismiss only | Non-drill keys just dismiss |
Implementation: after popping the milestone, check the key code. For `Char`, `Tab`, `Enter`, and `Esc`, let the key continue through the existing `handle_drill_key` logic. For `Backspace` and all other keys, return early after dismissal.
### Phase 3 Verification
- Start fresh, type until 7th key unlocks → milestone overlay appears
- Press a letter key → overlay disappears AND that letter is typed into the drill
- Press Tab during overlay → overlay disappears AND tab is processed as drill input
- Press Enter during overlay → overlay disappears AND enter is processed as drill input
- Press Backspace during overlay → overlay disappears, no drill input change
- Press Esc during overlay → overlay disappears AND drill exits
- Master a key → mastery overlay appears
- Multiple milestones in one drill → overlays show sequentially
- Verify correct finger info text
- Shifted character unlock → shift keys highlighted on diagram
- Small terminal → verify overlay degrades gracefully
- Small terminal + multi-key milestone → verify text-only layout shows all keys and finger info without overflow
- Encouraging messages: assert from message pool membership (not exact string) in any UI tests to avoid flaky assertions from randomness
- Multi-key milestone → verify first key is highlighted on keyboard diagram, all keys listed textually
---
## Phase 4: Stats Dashboard — Add Modifier Key Stats
### 4a. Add modifier key stats to keyboard heatmaps
**File: `src/ui/components/stats_dashboard.rs`**
In `render_keyboard_heatmap` (line 654) and `render_keyboard_timing` (line 768), after rendering the 4 character rows, render modifier key stats:
- **Backspace** (`'\x08'`): After number row, render `Bksp` + stat value
- **Tab** (`'\t'`): Before top row, render `Tab` + stat value
- **Enter** (`'\n'`): After home row, render `Ent` + stat value
- **Space** (`' '`): Below bottom row, render `Spc` + stat value
Use the same `get_key_accuracy` / `get_key_time_ms` methods (they work with any `char`).
### 4b. Include modifier keys in key ranking lists
In `render_worst_accuracy_keys` (line 957) and `render_best_accuracy_keys` (line 1030), add `' '`, `'\t'`, `'\n'` to the `all_keys` set so these keys appear in accuracy rankings. The `render_slowest_keys`/`render_fastest_keys` already pull from `key_stats.stats` which includes these keys automatically.
### Phase 4 Verification
- Open Stats → Accuracy tab → keyboard heatmap shows Tab, Enter, Space with stats
- Open Stats → Timing tab → same
- Tab/Space appear in worst/best accuracy lists when they have data
---
## Phase 5: Keyboard Explorer Screen
### 5a. Add `AppScreen::Keyboard` and menu item
**File: `src/app.rs`**
Add `Keyboard` to `AppScreen` enum. Add field:
```rust
pub keyboard_explorer_selected: Option<char>,
```
**File: `src/ui/components/menu.rs`**
Add menu item with key `"b"` (not `"k"` which conflicts with j/k vim navigation):
```rust
MenuItem {
key: "b".to_string(),
label: "Keyboard".to_string(),
description: "Explore keyboard layout and key statistics".to_string(),
}
```
Insert between "Skill Tree" and "Statistics". Final menu order:
- 0: `[1]` Adaptive Drill
- 1: `[2]` Code Drill
- 2: `[3]` Passage Drill
- 3: `[t]` Skill Tree
- 4: `[b]` Keyboard
- 5: `[s]` Statistics
- 6: `[c]` Settings
### 5b. Menu routing
**File: `src/main.rs` — `handle_menu_key`**
Add `KeyCode::Char('b')``app.screen = AppScreen::Keyboard; app.keyboard_explorer_selected = None`. Update Enter handler indices: 4 → Keyboard, 5 → Stats, 6 → Settings.
Update footer hint: `" [1-3] Start [t] Skill Tree [b] Keyboard [s] Stats [c] Settings [q] Quit "`.
### 5c. Keyboard Explorer rendering
**File: `src/main.rs`**
Add `render_keyboard_explorer` function. Layout:
1. **Header** (3 lines): " Keyboard Explorer " + "Press any key to see details"
2. **Keyboard diagram** (8 lines): Full `KeyboardDiagram` with:
- `focused_key`: `app.keyboard_explorer_selected`
- `next_key`: None
- `unlocked_keys`: `app.skill_tree.unlocked_keys(DrillScope::Global)`
- `depressed_keys`: `&app.depressed_keys`
- `shift_held`: `app.shift_held`
3. **Key detail panel** (remaining space): Bordered block showing stats for selected key
4. **Footer** (1 line): "[ESC] Back"
Key detail panel content (when a key is selected):
```
┌─ Key Details: 's' ──────────────────────────────┐
│ Finger: Left ring finger │
│ Unlocked: Yes │
│ Mastery: 87% confidence │
│ Branch: Lowercase a-z │
│ Level: Frequency Order (key #7) │
│ Avg Time: 245ms (best: 198ms) │
│ Accuracy: 96.2% (385/400 correct) │
│ Samples: 400 │
└──────────────────────────────────────────────────┘
```
Data sources:
- Finger: `keyboard_model.finger_for_char(ch).description()`
- Unlocked: check if `ch` is in `skill_tree.unlocked_keys(DrillScope::Global)`
- Mastery: `key_stats.get_confidence(ch)` formatted as percentage
- Branch/Level: `find_key_branch(ch)` from Phase 2
- Avg Time / Best: `key_stats.get_stat(ch)``filtered_time_ms`, `best_time_ms`
- Accuracy: precomputed (see 5e)
- Samples: `key_stats.get_stat(ch)``sample_count`
### 5d. Key handling
**File: `src/main.rs`**
Add `handle_keyboard_explorer_key`:
- `Esc` → go to menu
- `KeyCode::Char('q')` when no key selected → go to menu; when key selected → select 'q' (so user can explore 'q')
- `KeyCode::Char(ch)` → set `keyboard_explorer_selected = Some(ch)` (see normalization below)
- `KeyCode::Tab` → set selected to `'\t'`
- `KeyCode::Enter` → set selected to `'\n'`
- `KeyCode::Backspace` → set selected to `'\x08'`
**Shifted character normalization strategy:** Store the literal `ch` value from the `KeyCode::Char(ch)` event as-is. Do NOT transform using `shift_held` state. crossterm delivers the already-shifted character in the event (e.g., Shift+a → `KeyCode::Char('A')`, Shift+1 → `KeyCode::Char('!')`), so the event `ch` is the correct key identity. The `shift_held` flag is used only for keyboard diagram rendering (to show shifted labels on all keys), not for determining which key was selected. Show shift guidance in the detail panel for any shifted character (uppercase or symbol) using `keyboard_model.finger_for_char(ch)` to determine hand and thus which shift key to recommend.
For Keyboard Explorer, also show shift key guidance for shifted keys in the detail panel:
- Left-hand characters → "Hold Right Shift (right pinky)"
- Right-hand characters → "Hold Left Shift (left pinky)"
### 5e. Precomputed accuracy for explorer
**File: `src/app.rs`**
Add a cached accuracy field to `App`:
```rust
pub explorer_accuracy_cache: Option<(char, usize, usize)>, // (cached_key, correct, total)
```
Add a method `App::key_accuracy(ch: char) -> (usize, usize)` that checks the cache first. If `cached_key == ch`, return cached values. Otherwise, perform a single linear scan of `drill_history`, cache the result, and return it. The cache is invalidated automatically when `keyboard_explorer_selected` changes (set cache to `None` in the key handler). This avoids redundant O(n) scans on every render frame during key hold or rapid redraw.
### Phase 5 Verification
- `cargo build && cargo test`
- Open Keyboard from menu via `b` key → verify diagram shown
- Press any letter → detail panel shows finger, branch, level, stats
- Press shift → shift keys light up, all keys show shifted variants
- Press shifted key (e.g. Shift+a → 'A') → detail panel shows shifted character info with shift key guidance
- Tab/Enter/Backspace/Space → light up and show details
- Key with no stats → "No data yet"
- Esc → return to menu
- Verify `j`/`k` still work for menu navigation (no hotkey conflict)
---
## Finger Assignment Reference Data (informational)
The existing `KeyboardModel::finger_for_position` method (in `src/keyboard/model.rs`) handles finger assignments by physical position for all layouts. The table below is for reference only — the implementation in `finger_for_position` is the source of truth. Add unit tests against that method to validate correctness rather than maintaining this table. **Shifted characters use the same finger as their base key.**
### QWERTY — All 96 Keys by Finger
**Left Pinky (11 keys):**
- Base: `` ` `` `1` `q` `a` `z`
- Shifted: `~` `!` `Q` `A` `Z`
- Modifier: Tab (`\t`)
**Left Ring (8 keys):**
- Base: `2` `w` `s` `x`
- Shifted: `@` `W` `S` `X`
**Left Middle (8 keys):**
- Base: `3` `e` `d` `c`
- Shifted: `#` `E` `D` `C`
**Left Index (16 keys):**
- Base: `4` `5` `r` `t` `f` `g` `v` `b`
- Shifted: `$` `%` `R` `T` `F` `G` `V` `B`
**Right Index (16 keys):**
- Base: `6` `7` `y` `u` `h` `j` `n` `m`
- Shifted: `^` `&` `Y` `U` `H` `J` `N` `M`
**Right Middle (8 keys):**
- Base: `8` `i` `k` `,`
- Shifted: `*` `I` `K` `<`
**Right Ring (8 keys):**
- Base: `9` `o` `l` `.`
- Shifted: `(` `O` `L` `>`
**Right Pinky (21 keys):**
- Base: `0` `-` `=` `p` `[` `]` `\` `;` `'` `/`
- Shifted: `)` `_` `+` `P` `{` `}` `|` `:` `"` `?`
- Modifiers: Backspace (`\x08`), Enter (`\n`)
**Thumb (1 key):**
- Space (` `)
### Dvorak & Colemak
Finger assignments are **position-based** — the same physical key positions use the same fingers. `KeyboardModel::finger_for_char(ch)` looks up a character's physical position via `find_key_position` then calls `finger_for_position`, so it returns the correct finger for any layout automatically.
### Shift Key Guidance for Shifted Characters
- **Left-hand characters**: Hold **Right Shift** (right pinky)
- **Right-hand characters**: Hold **Left Shift** (left pinky)
---
## Critical Files to Modify
1. **`src/keyboard/display.rs`** (new) — Centralized key display adapter for sentinel ↔ display name conversions (Phase 0)
2. **`src/keyboard/finger.rs`** — Add `description()` method (Phase 2)
3. **`src/engine/skill_tree.rs`** — Add `SkillTreeUpdate` return type, `find_key_branch()` helper (Phase 2)
4. **`src/app.rs`** — Add `milestone_queue`, `keyboard_explorer_selected`, `AppScreen::Keyboard`, milestone structs (Phases 3, 5)
5. **`src/ui/components/keyboard_diagram.rs`** — Render Tab, Enter, Shift, Space, Backspace as interactive keys (Phase 1)
6. **`src/main.rs`** — Modifier depressed state tracking, milestone overlay, keyboard explorer screen, menu routing (Phases 1, 3, 5)
7. **`src/ui/components/stats_dashboard.rs`** — Add modifier keys to keyboard heatmaps and ranking lists (Phase 4)
8. **`src/ui/components/menu.rs`** — Add "Keyboard" menu item with key `b` (Phase 5)
## Terminology
Throughout the implementation, use consistent terminology:
- "Milestone" for the unlock/mastery event system (not "popup" or "notification")
- "Milestone overlay" for the UI element shown during a milestone (not "pop-up", "modal", or "dialog")
- "Enter" (not "Return") for the Enter key
- "Keyboard Explorer" for the new menu screen
## Scope Boundaries
- Non-US layouts beyond QWERTY/Dvorak/Colemak are out of scope for this plan
- The `KeyDisplay` adapter (Phase 0) is intentionally thin — a full typed `KeyId` enum migration is deferred to a future plan
- Left/right shift distinction is not tracked separately (both display as "Shift")

View File

@@ -0,0 +1,338 @@
# N-gram Error Tracking for Adaptive Drill Selection
## Context
keydr currently tracks typing errors at the single-character level only. The adaptive algorithm picks the weakest character by confidence score and biases drill text to include words containing that character. This misses **transition difficulties** -- sequences where individual characters are easy but the combination is hard (e.g., same-finger bigrams, awkward hand transitions). Research strongly supports that these transition effects are real and distinct from single-character difficulty.
**Goal:** Add bigram (n=2) and trigram (n=3) error tracking, with a redundancy detection formula that distinguishes genuine transition difficulties from errors that are just proxies for single-character weakness. Integrate problematic bigrams into the adaptive drill selection pipeline. Trigrams are tracked for observation only and not used for drill generation until empirically proven useful.
---
## Research Summary
1. **N-gram tracking is genuinely novel** -- No existing typing tutor does comprehensive n-gram *error* tracking with adaptive drill selection.
2. **Bigrams capture real, distinct information** -- The 136M Keystrokes study (Dhakal et al., CHI 2018) found letter pairs typed by different hands are more predictive of speed than character repetitions. This cannot be inferred from single-char data.
3. **Motor chunking is real** -- The motor cortex plans keystrokes in chunks, not individually. Single-character optimization misses this.
4. **Bigrams are the sweet spot** -- Nearly all keyboard layout research focuses on bigrams. Trigrams likely offer diminishing returns.
---
## Core Innovation: Redundancy Detection
The key question: "Is a high-error bigram just a proxy for a high-error character?"
### Error Rate Estimation (Laplace-smoothed)
Raw error rates are unstable at low sample counts. All error rates use Laplace smoothing:
```
smoothed_error_rate(errors, samples) = (errors + 1) / (samples + 2)
```
This gives a Bayesian prior of 50% error rate that gets pulled toward the true rate as samples accumulate. At 10 samples with 3 errors, this yields 0.333 instead of raw 0.3 -- a small correction. At 2 samples with 1 error, it yields 0.5 instead of raw 0.5 -- stabilizing the estimate.
### Bigram Redundancy Formula
For bigram "ab" with characters `a` and `b`:
```
e_a = smoothed_error_rate(char_a.errors, char_a.samples)
e_b = smoothed_error_rate(char_b.errors, char_b.samples)
e_ab = smoothed_error_rate(bigram_ab.errors, bigram_ab.samples)
expected_ab = 1.0 - (1.0 - e_a) * (1.0 - e_b)
redundancy_ab = e_ab / max(expected_ab, 0.01)
```
### Trigram Redundancy Formula
For trigram "abc", redundancy is computed against BOTH individual chars AND constituent bigrams:
```
// Expected from chars alone (independence assumption)
expected_from_chars = 1.0 - (1.0 - e_a) * (1.0 - e_b) * (1.0 - e_c)
// Expected from bigrams (takes the max -- if either bigram explains the error, no trigram signal)
expected_from_bigrams = max(e_ab, e_bc)
// Use the higher expectation (harder to exceed = more conservative)
expected_abc = max(expected_from_chars, expected_from_bigrams)
redundancy_abc = e_abc / max(expected_abc, 0.01)
```
This ensures trigrams only flag as informative when NEITHER the individual characters NOR constituent bigrams explain the difficulty.
### Focus Eligibility (Stability-Gated)
An n-gram becomes eligible for focus only when ALL conditions hold:
1. `sample_count >= 20` -- minimum statistical reliability
2. `redundancy > 1.5` -- genuine transition difficulty, not a proxy
3. `redundancy_stable == true` -- the redundancy score has been > 1.5 for the last 3 consecutive update checks (prevents focus flapping from noisy estimates)
The **difficulty score** for ranking eligible n-grams:
```
ngram_difficulty = (1.0 - confidence) * redundancy
```
### Worked Examples
**Example 1 -- Proxy (should NOT focus):** User struggles with 's'. `e_s = 0.25`, `e_i = 0.03`. Expected bigram "is" error: `1 - 0.75 * 0.97 = 0.273`. Observed "is" error: `0.28`. Redundancy: `0.28 / 0.273 = 1.03`. This is ~1.0, confirming "is" errors are just 's' errors. Not eligible.
**Example 2 -- Genuine difficulty (should focus):** User is fine with 'e' and 'd' individually. `e_e = 0.04`, `e_d = 0.05`. Expected "ed" error: `1 - 0.96 * 0.95 = 0.088`. Observed "ed" error: `0.22`. Redundancy: `0.22 / 0.088 = 2.5`. This exceeds 1.5 -- the "ed" transition is genuinely hard. Eligible for focus.
**Example 3 -- Trigram vs bigram:** `e_t = 0.03`, `e_h = 0.04`, `e_e = 0.04`. Bigram `e_th = 0.15` (genuine difficulty). Expected trigram "the" from chars: `0.107`. Expected from bigrams: `max(0.15, 0.04) = 0.15`. Observed "the" error: `0.16`. Redundancy: `0.16 / 0.15 = 1.07`. Not significant -- the "th" bigram already explains the trigram difficulty. Trigram NOT eligible.
---
## Confidence Scale
`NgramStat.confidence` uses the same formula as `KeyStat.confidence`:
```
target_time_ms = 60000.0 / target_cpm // 342.86ms at 175 CPM
confidence = target_time_ms / filtered_time_ms
```
- `confidence < 1.0`: Slower than target (needs practice)
- `confidence == 1.0`: Exactly at target speed
- `confidence > 1.0`: Faster than target (mastered)
For n-grams, `target_time_ms` scales linearly with order: a bigram target is `2 * single_char_target`, a trigram target is `3 * single_char_target`. This is approximate but consistent.
---
## Hesitation Tracking
Hesitations indicate cognitive uncertainty even when the correct key is pressed. The threshold is **relative to the user's rolling baseline**:
```
hesitation_threshold = max(800.0, 2.5 * user_median_transition_ms)
```
Where `user_median_transition_ms` is the median of the user's last 200 inter-keystroke intervals across all drills. The 800ms absolute floor prevents the threshold from being too low for fast typists. The 2.5x multiplier flags transitions that are notably slower than the user's norm.
`user_median_transition_ms` is stored as a single rolling value on the App struct, updated from `per_key_times` after each drill.
---
## N-gram Key Representation
N-gram keys use typed arrays instead of strings to avoid encoding/canonicalization issues:
```rust
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BigramKey(pub [char; 2]);
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TrigramKey(pub [char; 3]);
```
**Normalization rules** (applied at extraction boundary in `extract_ngram_events`):
- All characters are Unicode scalar values (Rust `char`) -- no grapheme cluster handling needed since the app only supports ASCII typing
- No case folding -- 'A' and 'a' are distinct (they require different motor actions: shift+a vs a)
- Punctuation is included (transitions to/from punctuation are legitimate motor sequences)
- BACKSPACE characters are filtered out before windowing
- Space characters split windows (no cross-word-boundary n-grams)
---
## Implementation
### Phase 1: Core Data Structures & Extraction
**New file: `src/engine/ngram_stats.rs`**
- `BigramKey(pub [char; 2])` and `TrigramKey(pub [char; 3])` -- typed keys with Hash/Eq/Serialize
- `NgramStat` struct:
- `filtered_time_ms: f64` -- EMA-smoothed transition time (alpha=0.1)
- `best_time_ms: f64` -- personal best EMA time
- `confidence: f64` -- `(target_time_ms * order) / filtered_time_ms`
- `sample_count: usize` -- total observations
- `error_count: usize` -- total errors (mistype or hesitation)
- `hesitation_count: usize` -- total hesitations specifically
- `recent_times: Vec<f64>` -- last 30 observations
- `recent_correct: Vec<bool>` -- last 30 correctness values
- `redundancy_streak: u8` -- consecutive updates where redundancy > 1.5 (for stability gate, max 255)
- `BigramStatsStore` -- `HashMap<BigramKey, NgramStat>` (concrete, not generic)
- `update(&mut self, key: BigramKey, time_ms: f64, correct: bool, hesitation: bool)`
- `get_confidence(&self, key: &BigramKey) -> f64`
- `smoothed_error_rate(&self, key: &BigramKey) -> f64` -- Laplace-smoothed
- `redundancy_score(&self, key: &BigramKey, char_stats: &KeyStatsStore) -> f64`
- `weakest_bigram(&self, char_stats: &KeyStatsStore, unlocked: &[char]) -> Option<(BigramKey, f64)>` -- stability-gated
- `TrigramStatsStore` -- `HashMap<TrigramKey, NgramStat>` (concrete, not generic)
- Same update/query methods as BigramStatsStore
- `prune(&mut self, max_entries: usize)` -- composite utility pruning (see below)
- Internal: shared helper functions/trait for the common EMA update logic to avoid duplication between bigram and trigram stores
- `BigramEvent` / `TrigramEvent` structs -- `{ key, total_time_ms, correct, has_hesitation }`
- `extract_ngram_events(per_key_times: &[KeyTime], hesitation_threshold: f64) -> (Vec<BigramEvent>, Vec<TrigramEvent>)` -- single pass, returns both orders
- `FocusTarget` enum -- `Char(char) | Bigram(BigramKey)` -- lives in `src/engine/ngram_stats.rs`, re-exported from `src/engine/mod.rs`
**Note:** `KeyStatsStore` needs a new method `smoothed_error_rate(key: char) -> f64` to provide Laplace-smoothed error rates. This requires adding `error_count` to `KeyStat`. Currently `KeyStat` only tracks timing for correct keystrokes -- we need to also count errors. Add `error_count: usize` and `total_count: usize` fields to `KeyStat`, increment in `update_key()`. Use `#[serde(default)]` for backward compat on deserialization.
**Modify: `src/engine/key_stats.rs`** (additive)
- Add `error_count: usize` and `total_count: usize` to `KeyStat` with `#[serde(default)]`
- Add `update_key_error(&mut self, key: char)` -- increments error/total counts without updating timing
- Add `smoothed_error_rate(&self, key: char) -> f64` -- Laplace-smoothed
**Modify: `src/engine/mod.rs`** (additive) -- add `pub mod ngram_stats`, re-export `FocusTarget`
**Extraction detail:** For bigram "th", transition time = `window[1].time_ms`. For trigram "the", transition time = `window[1].time_ms + window[2].time_ms`. The first element's `time_ms` is the transition FROM the previous character and is NOT part of this n-gram.
### Phase 2: Persistence (Replay-Only, No Caching)
**Architecture:** `drill_history` (lesson_history.json) is the **sole source of truth**. N-gram stats are **always rebuilt from drill history** on startup. There are no separate n-gram cache files in this initial implementation. This eliminates all cache coherency concerns at the cost of ~200-500ms startup replay. Caching can be added later as an optimization if rebuild latency becomes problematic.
**Modify: `src/store/schema.rs`** (additive)
- Add concrete `BigramStatsData { stats: BigramStatsStore }` with Default impl
- Add concrete `TrigramStatsData { stats: TrigramStatsStore }` with Default impl
- These types are used for export/import serialization only, not for runtime caching
**Modify: `src/app.rs`** (additive + modify existing)
- Add 4 fields to `App`: `bigram_stats`, `ranked_bigram_stats`, `trigram_stats`, `ranked_trigram_stats`
- Add `user_median_transition_ms: f64` and `transition_buffer: Vec<f64>` (rolling last 200 intervals)
- On startup: rebuild all n-gram stats + hesitation baseline by replaying `drill_history`
- `save_data()`: no n-gram files to save (stats are always derived)
**Trigram pruning:** Max 5,000 entries. Prune by composite utility score after history replay:
```
utility = recency_weight * (1.0 / (drills_since_last_seen + 1))
+ signal_weight * redundancy_score.min(3.0)
+ data_weight * (sample_count as f64).ln()
```
Where `recency_weight=0.3`, `signal_weight=0.5`, `data_weight=0.2`. Entries with highest utility are kept. This preserves rare-but-informative trigrams over frequent-but-noisy ones.
### Phase 3: Drill Integration
**Modify: `src/app.rs` -- `finish_drill()`** (modify existing, after line 847)
- Compute `hesitation_threshold = max(800.0, 2.5 * self.user_median_transition_ms)`
- Call `extract_ngram_events(&result.per_key_times, hesitation_threshold)`
- Update `bigram_stats` and `trigram_stats` with each event
- For incorrect keystrokes: also call `self.key_stats.update_key_error(kt.key)` to build char-level error counts
- Same pattern for ranked stats in the ranked block (after line 854)
- Update `transition_buffer` and recompute `user_median_transition_ms`
**Modify: `src/app.rs` -- `finish_partial_drill()`** -- same pattern
**Hesitation baseline rebuild:** During startup history replay, also accumulate transition times into `transition_buffer` to rebuild `user_median_transition_ms`. This ensures the hesitation threshold is consistent across restarts.
### Phase 4: Adaptive Focus Selection (Bigram Only)
The focus pipeline uses a **thin adapter at the App boundary** rather than changing generator signatures directly. This minimizes cross-cutting risk.
**Modify: `src/app.rs` -- `generate_text()`** (modify existing, line 628)
```rust
// Adapter: compute focus target, then decompose into existing generator knobs
let focus_target = select_focus_target(
&self.skill_tree, scope, &self.ranked_key_stats, &self.ranked_bigram_stats
);
let (focused_char, focused_bigram) = match &focus_target {
FocusTarget::Char(ch) => (Some(*ch), None),
FocusTarget::Bigram(key) => (Some(key.0[0]), Some(key.clone())),
};
// Existing generators use focused_char unchanged
let mut text = generator.generate(&filter, lowercase_focused_char, word_count);
// ... existing capitalize/punctuate/numbers pipeline unchanged ...
// After all generation: if bigram focus, swap some words for bigram-containing words
if let Some(ref bigram) = focused_bigram {
text = self.apply_bigram_focus(&text, &filter, bigram);
}
```
**New method on `App`: `apply_bigram_focus()`**
- Scans generated words, replaces up to 40% with dictionary words containing the target bigram
- Only replaces when suitable alternatives exist and pass the CharFilter
- Maintains word count and approximate text length
- **Diversity cap:** No more than 3 consecutive bigram-focused words to prevent repetitive feel
This approach keeps ALL existing generator APIs unchanged. If the adapter proves insufficient (e.g., bigram-focused words are too rare in dictionary), we can widen generator APIs in a follow-up.
**Focus selection logic** (new function `select_focus_target()` in `src/engine/ngram_stats.rs`):
1. Compute weakest single character via existing `focused_key()`
2. Compute weakest eligible bigram via `weakest_bigram()` (stability-gated: sample >= 20, redundancy > 1.5 for 3 consecutive checks)
3. If bigram `ngram_difficulty > char_difficulty * 0.8`, focus on bigram
4. Otherwise, fall back to single-char focus
### Phase 5: Information Gain Analysis (Trigram Observation)
**Add to `src/engine/ngram_stats.rs`:**
```rust
pub fn trigram_marginal_gain(
trigram_stats: &TrigramStatsStore,
bigram_stats: &BigramStatsStore,
char_stats: &KeyStatsStore,
) -> f64
```
Computes what fraction of trigrams with >= 20 samples have `redundancy > 1.5` vs their constituent bigrams. Returns a value in `[0.0, 1.0]`.
- Called every 50 drills, result logged to a `trigram_gain_history: Vec<f64>` on the App
- If the most recent 3 measurements all show gain > 10%, trigrams could be promoted to active focus (future work)
- This metric is primarily for analysis -- it answers "are trigrams adding value beyond bigrams for this user?"
### Phase 6: Export/Import
**Modify: `src/store/schema.rs`** (additive) -- add n-gram fields to `ExportData` with `#[serde(default)]`
**Modify: `src/store/json_store.rs`** (additive) -- update `export_all()` to serialize n-gram stats from memory; `import_all()` imports them into drill_history replay pipeline
---
## Performance Budgets
| Operation | Budget | Notes |
|-----------|--------|-------|
| N-gram extraction per drill | < 1ms | Linear scan of ~200-500 keystrokes |
| Stats update per drill | < 1ms | ~400 bigram + ~300 trigram hash map inserts |
| Focus selection | < 5ms | Iterate all bigrams (~2K), filter + rank |
| History replay (full rebuild) | < 500ms | Replay 500 drills x extraction + update (fixture: 500 drills, 300 keystrokes each) |
| Memory for n-gram stores | < 5MB | ~3K bigrams + 5K trigrams x ~200 bytes each |
Benchmark tests enforce extraction (<1ms for 500 keystrokes), update (<1ms for 400 events), and focus selection (<5ms for 3K bigrams) budgets.
---
## Files Summary
| File | Action | Breaking? | What Changes |
|------|--------|-----------|-------------|
| `src/engine/ngram_stats.rs` | **New** | No | All n-gram structs, extraction, redundancy formula, FocusTarget, focus selection |
| `src/engine/mod.rs` | Modify | No (additive) | Add `pub mod ngram_stats`, re-export `FocusTarget` |
| `src/engine/key_stats.rs` | Modify | No (additive) | Add `error_count`/`total_count` to `KeyStat` with `#[serde(default)]`, add `smoothed_error_rate()` |
| `src/store/schema.rs` | Modify | No (additive) | `BigramStatsData`/`TrigramStatsData` types, `ExportData` update with `#[serde(default)]` |
| `src/store/json_store.rs` | Modify | No (additive) | Export/import n-gram data |
| `src/app.rs` | Modify | No (internal) | App fields, `finish_drill()` n-gram extraction, `generate_text()` adapter + `apply_bigram_focus()`, startup replay |
| `src/generator/dictionary.rs` | Unchanged | - | Existing `find_matching` used as-is via adapter |
| `src/generator/phonetic.rs` | Unchanged | - | Existing API used as-is via adapter |
---
## Verification
1. **Unit tests** for `extract_ngram_events` -- verify bigram/trigram extraction from known keystroke sequences, BACKSPACE filtering, space-boundary skipping, hesitation detection at threshold boundary
2. **Unit tests** for `redundancy_score` -- the 3 worked examples above as test cases, plus edge cases (zero samples, all errors, no errors)
3. **Unit tests** for Laplace smoothing -- verify convergence behavior at low and high sample counts
4. **Unit tests** for stability gate -- verify `redundancy_streak` increments/resets correctly, focus eligibility requires 3 consecutive hits
5. **Deterministic integration tests** for focus selection -- seed `SmallRng` with fixed seed, verify tie-breaking behavior between char and bigram focus, verify fallback when no bigrams are eligible
6. **Regression test** -- verify existing single-character focus works unchanged when no bigrams have sufficient samples (cold start path)
7. **Benchmark tests** (non-blocking, `#[bench]` or criterion):
- Extraction: < 1ms for 500 `KeyTime` entries
- Update: < 1ms for 400 bigram events
- Focus selection: < 5ms for 3,000 bigram entries
- History replay: < 500ms for 500 drills of 300 keystrokes each
8. **Manual test** -- deliberately mistype a specific bigram repeatedly, verify it becomes the focus target and subsequent drills contain words with that bigram
## Future Considerations (Not in Scope)
- **N-gram cache files** for faster startup if replay latency becomes problematic (hybrid append-only cursor approach)
- **Per-order empirical confidence targets** instead of linear scaling (calibrate from user data, log diagnostics)
- **Bigram placement control** in phonetic generator (prefix/medial/suffix weighting) if adapter approach proves insufficient
- **Trigram-driven focus** if marginal gain metric consistently shows > 10% incremental value

View File

@@ -0,0 +1,221 @@
# Plan: N-grams Statistics Tab
## Context
The n-gram error tracking system (last commit `e7f57dd`) tracks bigram/trigram transition difficulties and uses them to adapt drill selection. However, there's no visibility into what the system has identified as weak or how it's influencing drills. This plan adds a **[6] N-grams** tab to the Statistics page to surface this data.
---
## Layout
```
[1] Dashboard [2] History [3] Activity [4] Accuracy [5] Timing [6] N-grams
┌─ Active Focus ──────────────────────────────────────────────────────────────┐
│ Focus: Bigram "th" (difficulty: 1.24) │
│ Bigram diff 1.24 > char 'n' diff 0.50 x 0.8 threshold │
└─────────────────────────────────────────────────────────────────────────────┘
┌─ Eligible Bigrams (3) ────────────────┐┌─ Watchlist ─────────────────────────┐
│ Pair Diff Err% Exp% Red Conf N ││ Pair Red Samples Streak │
│ th 1.24 18% 7% 2.10 0.41 32 ││ er 1.82 14/20 2/3 │
│ ed 0.89 22% 9% 1.90 0.53 28 ││ in 1.61 8/20 1/3 │
│ ng 0.72 14% 8% 1.72 0.58 24 ││ ou 1.53 18/20 1/3 │
└────────────────────────────────────────┘└───────────────────────────────────┘
Scope: Global | Bigrams: 142 | Trigrams: 387 | Hesitation: >832ms | Tri-gain: 12.0%
[ESC] Back [Tab] Next tab [1-6] Switch tab
```
---
## Scope Decisions
- **Drill scope**: Tab shows data for `app.drill_scope` (current adaptive scope). A scope label in the summary line makes this explicit (e.g., "Scope: Global" or "Scope: Branch: lowercase").
- **Trigram gain**: Sourced from `app.trigram_gain_history` (computed every 50 ranked drills). Always from ranked stats, consistent with bigram/trigram counts shown. The value is a fraction in `[0.0, 1.0]` (count of signal trigrams / total qualified trigrams), so it is mathematically non-negative. Format: `X.X%` (one decimal). When empty: `--` with note "(computed every 50 drills)".
- **Eligible vs Watchlist**: Strictly disjoint by construction. Watchlist filter explicitly excludes bigrams that pass all eligibility gates.
---
## Layer Boundaries
Domain logic (engine) and presentation (UI) are separated:
- **Engine** (`ngram_stats.rs`): Owns `FocusReasoning` (domain decision explanation), `select_focus_target_with_reasoning()`, filtering/gating/sorting logic for eligible and watchlist bigrams. Returns domain-oriented results.
- **UI** (`stats_dashboard.rs`): Owns `NgramTabData`, `EligibleBigramRow`, `WatchlistBigramRow` (view model structs tailored for rendering columns).
- **Adapter** (`main.rs`): `build_ngram_tab_data()` is the single point that translates engine output → UI view models. All stats store lookups for display columns happen here.
---
## Files to Modify
### 1. `src/engine/ngram_stats.rs` — Domain logic + focus reasoning
**`FocusReasoning` enum** (domain concept — why the target was selected):
```rust
pub enum FocusReasoning {
BigramWins {
bigram_difficulty: f64,
char_difficulty: f64,
char_key: Option<char>, // None when no focused char exists
},
CharWins {
char_key: char,
char_difficulty: f64,
bigram_best: Option<(BigramKey, f64)>,
},
NoBigrams { char_key: char },
Fallback,
}
```
**`select_focus_target_with_reasoning()`** — Unified function returning `(FocusTarget, FocusReasoning)`. Internally calls `focused_key()` and `weakest_bigram()` once. Handles all four match arms without synthetic values.
**`focus_eligible_bigrams()`** on `BigramStatsStore` — Returns `Vec<(BigramKey, f64 /*difficulty*/, f64 /*redundancy*/)>` sorted by `(difficulty desc, redundancy desc, key lexical asc)`. Same gating as `weakest_bigram()`: sample >= `MIN_SAMPLES_FOR_FOCUS`, streak >= `STABILITY_STREAK_REQUIRED`, redundancy > `STABILITY_THRESHOLD`, difficulty > 0. Returns ALL qualifying entries (no truncation — UI handles truncation to available height).
**`watchlist_bigrams()`** on `BigramStatsStore` — Returns `Vec<(BigramKey, f64 /*redundancy*/)>` sorted by `(redundancy desc, key lexical asc)`. Criteria: redundancy > `STABILITY_THRESHOLD`, sample_count >= 3 (noise floor), AND NOT fully eligible. Returns ALL qualifying entries.
**Export constants** — Make `MIN_SAMPLES_FOR_FOCUS` and `STABILITY_STREAK_REQUIRED` `pub(crate)` so the adapter in `main.rs` can pass them into `NgramTabData` without duplicating values.
### 2. `src/ui/components/stats_dashboard.rs` — View models + rendering
**View model structs** (presentation-oriented, mapped from engine data by adapter):
```rust
pub struct EligibleBigramRow {
pub pair: String, // e.g., "th"
pub difficulty: f64,
pub error_rate_pct: f64, // smoothed, as percentage
pub expected_rate_pct: f64,// from char independence, as percentage
pub redundancy: f64,
pub confidence: f64,
pub sample_count: usize,
}
pub struct WatchlistBigramRow {
pub pair: String,
pub redundancy: f64,
pub sample_count: usize,
pub redundancy_streak: u8,
}
```
**`NgramTabData` struct** (assembled by `build_ngram_tab_data()` in main.rs):
```rust
pub struct NgramTabData {
pub focus_target: FocusTarget,
pub focus_reasoning: FocusReasoning,
pub eligible: Vec<EligibleBigramRow>,
pub watchlist: Vec<WatchlistBigramRow>,
pub total_bigrams: usize,
pub total_trigrams: usize,
pub hesitation_threshold_ms: f64,
pub latest_trigram_gain: Option<f64>,
pub scope_label: String,
// Engine thresholds for watchlist progress denominators:
pub min_samples_for_focus: usize, // from ngram_stats::MIN_SAMPLES_FOR_FOCUS
pub stability_streak_required: u8, // from ngram_stats::STABILITY_STREAK_REQUIRED
}
```
**Add field** to `StatsDashboard`: `ngram_data: Option<&'a NgramTabData>`
**Update constructor**, tab header (add `"[6] N-grams"`), footer (`[1-6]`), `render_tab()` dispatch.
**Rendering methods:**
- **`render_ngram_tab()`** — Vertical layout: focus (4 lines), lists (Min 5), summary (2 lines).
- **`render_ngram_focus()`** — Bordered "Active Focus" block.
- Line 1: target name in `colors.focused_key()` + bold
- Line 2: reasoning in `colors.text_pending()`
- When BigramWins + char_key is None: "Bigram selected (no individual char weakness found)"
- Empty state: "Complete some adaptive drills to see focus data"
- **`render_eligible_bigrams()`** — Bordered "Eligible Bigrams (N)" block.
- Header in `colors.accent()` + bold
- Rows colored by difficulty: `error()` (>1.0), `warning()` (>0.5), `success()` (<=0.5)
- Columns: `Pair Diff Err% Exp% Red Conf N`
- Narrow (<38 inner): drop Exp% and Conf
- Truncate rows to available height
- Empty state: "No bigrams meet focus criteria yet"
- **`render_watchlist_bigrams()`** — Bordered "Watchlist" block.
- Columns: `Pair Red Samples Streak`
- Samples rendered as `n/{data.min_samples_for_focus}`, Streak as `n/{data.stability_streak_required}` — denominators sourced from `NgramTabData` (engine constants), never hardcoded in UI
- All rows in `colors.warning()`
- Truncate rows to available height
- Empty state: "No approaching bigrams"
- **`render_ngram_summary()`** — Single line: scope label, bigram/trigram counts, hesitation threshold, trigram gain.
### 3. `src/main.rs` — Input handling + adapter
**`handle_stats_key()`**:
- `STATS_TAB_COUNT`: 5 → 6
- Add `KeyCode::Char('6') => app.stats_tab = 5` in both branches
**`build_ngram_tab_data(app: &App) -> NgramTabData`** — Dedicated adapter function (single point of engine→UI translation):
- Calls `select_focus_target_with_reasoning()`
- Calls `focus_eligible_bigrams()` and `watchlist_bigrams()`
- Maps engine results to `EligibleBigramRow`/`WatchlistBigramRow` by looking up additional per-bigram stats (error rate, expected rate, confidence, streak) from `app.ranked_bigram_stats` and `app.ranked_key_stats`
- Builds scope label from `app.drill_scope`
- Only called when `app.stats_tab == 5`
**`render_stats()`**: Call `build_ngram_tab_data()` when on tab 5, pass `Some(&data)` to StatsDashboard.
---
## Implementation Order
1. Add `FocusReasoning` enum and `select_focus_target_with_reasoning()` to `ngram_stats.rs`
2. Add `focus_eligible_bigrams()` and `watchlist_bigrams()` to `BigramStatsStore`
3. Add unit tests for steps 1-2
4. Add view model structs (`EligibleBigramRow`, `WatchlistBigramRow`, `NgramTabData`) and `ngram_data` field to `stats_dashboard.rs`
5. Add all rendering methods to `stats_dashboard.rs`
6. Update tab header, footer, `render_tab()` dispatch in `stats_dashboard.rs`
7. Add `build_ngram_tab_data()` adapter + update `render_stats()` in `main.rs`
8. Update `handle_stats_key()` in `main.rs`
---
## Verification
### Unit Tests (in `ngram_stats.rs` test module)
**`test_focus_eligible_bigrams_gating`** — BigramStatsStore with bigrams at boundary conditions:
- sample=25, streak=3, redundancy=2.0 → eligible
- sample=15, streak=3, redundancy=2.0 → excluded (samples < 20)
- sample=25, streak=2, redundancy=2.0 → excluded (streak < 3)
- sample=25, streak=3, redundancy=1.2 → excluded (redundancy <= 1.5)
- sample=25, streak=3, redundancy=2.0, confidence=1.5 → excluded (difficulty <= 0)
**`test_focus_eligible_bigrams_ordering_and_tiebreak`** — 3 eligible bigrams: two with same difficulty but different redundancy, one with lower difficulty. Verify sorted by (difficulty desc, redundancy desc, key lexical asc).
**`test_watchlist_bigrams_gating`** — Bigrams at boundary:
- Fully eligible (sample=25, streak=3) → excluded (goes to eligible list)
- High redundancy, low samples (sample=10) → included
- High redundancy, low streak (sample=25, streak=1) → included
- Low redundancy (1.3) → excluded
- Very few samples (sample=2) → excluded (< 3 noise floor)
**`test_watchlist_bigrams_ordering_and_tiebreak`** — 3 watchlist entries: two with same redundancy. Verify sorted by (redundancy desc, key lexical asc).
**`test_select_focus_with_reasoning_bigram_wins`** — Bigram difficulty > char difficulty * 0.8. Returns `BigramWins` with correct values and `char_key: Some(ch)`.
**`test_select_focus_with_reasoning_char_wins`** — Char difficulty high, bigram < threshold. Returns `CharWins` with `bigram_best` populated.
**`test_select_focus_with_reasoning_no_bigrams`** — No eligible bigrams. Returns `NoBigrams`.
**`test_select_focus_with_reasoning_bigram_only`** — No focused char, bigram exists. Returns `BigramWins` with `char_key: None`.
### Build & Existing Tests
- `cargo build` — no compile errors
- `cargo test` — all existing + new tests pass
### Manual Testing
- Navigate to Statistics → press [6] → see N-grams tab
- Tab/BackTab cycles through all 6 tabs
- With no drill history: empty states shown for all panels
- After several adaptive drills: eligible bigrams appear with plausible data
- Scope label reflects current drill scope
- Verify layout at 80x24 terminal size — confirm column drop at narrow widths keeps header/data aligned

View File

@@ -0,0 +1,265 @@
# Plan: Bigram Metrics Overhaul — Error Anomaly & Speed Anomaly
## Context
The current bigram metrics use `difficulty = (1 - confidence) * redundancy` to gate eligibility and focus. This is fundamentally broken: when a user types faster than target WPM (`confidence > 1.0`), difficulty goes negative — even for bigrams with 100% error rate. The root cause is that "confidence" (a speed-vs-target ratio) and "redundancy" (an error-rate ratio) are conflated into a single metric that can cancel out genuine problems.
This overhaul replaces the conflated system with two orthogonal anomaly metrics:
- **`error_anomaly`** — how much worse a bigram's error rate is compared to what's expected from its constituent characters (same math as current `redundancy_score`, reframed as a percentage)
- **`speed_anomaly`** — how much slower a bigram transition is compared to the user's normal speed typing the second character (user-relative, no target WPM dependency)
Both are displayed as percentages where positive = worse than expected. The UI shows two side-by-side columns, one per anomaly type, with confirmed problems highlighted.
---
## Persistence / Migration
**NgramStat is NOT persisted to disk.** N-gram stores are rebuilt from drill history on every startup (see `json_store.rs:104` comment: "N-gram stats are not included — they are always rebuilt from drill history", and `app.rs:1152` `rebuild_ngram_stats()`). The stores are never saved via `save_data()` — only `profile`, `key_stats`, `ranked_key_stats`, and `drill_history` are persisted.
Therefore:
- No serde migration, `#[serde(alias)]`, or backward-compat handling is needed for NgramStat field renames/removals
- `#[serde(default)]` annotations on NgramStat fields are vestigial (the derive exists for in-memory cloning, not disk persistence) but harmless to leave
- The `Serialize`/`Deserialize` derives on NgramStat can stay (used by BigramStatsStore/TrigramStatsStore types which derive them transitively, though the stores themselves are also not persisted)
**KeyStat IS persisted**`confidence` on KeyStat is NOT being changed (used by skill_tree progression). No migration needed there.
---
## Changes
### 1. `src/engine/ngram_stats.rs` — Metrics engine overhaul
**NgramStat struct** (line 34):
- Remove `confidence: f64` field
- Rename `redundancy_streak: u8``error_anomaly_streak: u8`
- Add `speed_anomaly_streak: u8` with `#[serde(default)]`
- **Preserved fields** (explicitly unchanged): `filtered_time_ms`, `best_time_ms`, `sample_count`, `error_count`, `hesitation_count`, `recent_times`, `recent_correct`, `last_seen_drill_index` — all remain and continue to be updated by `update_stat()`
**`update_stat()`** (line 65):
- Remove `confidence = target_time_ms / stat.filtered_time_ms` computation (line 82)
- Remove `target_time_ms` parameter (no longer needed)
- **Keep** `hesitation` parameter and `drill_index` parameter — these update `hesitation_count` (line 72) and `last_seen_drill_index` (line 66) which are used by trigram pruning and other downstream logic
- New signature (module-private, matching current visibility): `fn update_stat(stat: &mut NgramStat, time_ms: f64, correct: bool, hesitation: bool, drill_index: u32)`
- All other field updates remain identical (EMA on filtered_time_ms, best_time_ms, recent_times, recent_correct, error_count, sample_count)
**Constants** (lines 10-16):
- Rename `STABILITY_THRESHOLD``ERROR_ANOMALY_RATIO_THRESHOLD` (value stays 1.5)
- Rename `STABILITY_STREAK_REQUIRED``ANOMALY_STREAK_REQUIRED` (value stays 3)
- Rename `WATCHLIST_MIN_SAMPLES``ANOMALY_MIN_SAMPLES` (value stays 3)
- Add `SPEED_ANOMALY_PCT_THRESHOLD: f64 = 50.0` (50% slower than expected)
- Add `MIN_CHAR_SAMPLES_FOR_SPEED: usize = 10` (EMA alpha=0.1 needs ~10 samples for initial value to decay to ~35% influence; 5 samples still has ~59% initial-value bias, too noisy for baseline)
- Remove `DEFAULT_TARGET_CPM` (no longer used by update_stat or stores)
**`BigramStatsStore` struct** (line 102):
- Remove `target_cpm: f64` field and `default_target_cpm()` helper
- `BigramStatsStore::update()` (line 114): Remove `target_time_ms` calculation. Pass-through to `update_stat()` without it.
**`TrigramStatsStore` struct** (line 285):
- Remove `target_cpm: f64` field
- `TrigramStatsStore::update()` (line 293): Remove `target_time_ms` calculation. Pass-through to `update_stat()` without it.
**Remove `get_confidence()`** methods on both stores (lines 121, 300) — they read the deleted `confidence` field. Both are `#[allow(dead_code)]` already.
**Rename `redundancy_score()`****`error_anomaly_ratio()`** (line 132):
- Same math internally, just renamed. Returns `e_ab / expected_ab`.
**New methods on `BigramStatsStore`**:
```rust
/// Error anomaly as percentage: (ratio - 1.0) * 100
/// Returns None if bigram has no stats.
pub fn error_anomaly_pct(&self, key: &BigramKey, char_stats: &KeyStatsStore) -> Option<f64> {
let _stat = self.stats.get(key)?;
let ratio = self.error_anomaly_ratio(key, char_stats);
Some((ratio - 1.0) * 100.0)
}
/// Speed anomaly: % slower than user types char_b in isolation.
/// Compares bigram filtered_time_ms to char_b's filtered_time_ms.
/// Returns None if bigram has no stats or char_b has < MIN_CHAR_SAMPLES_FOR_SPEED samples.
pub fn speed_anomaly_pct(&self, key: &BigramKey, char_stats: &KeyStatsStore) -> Option<f64> {
let stat = self.stats.get(key)?;
let char_b_stat = char_stats.stats.get(&key.0[1])?;
if char_b_stat.sample_count < MIN_CHAR_SAMPLES_FOR_SPEED { return None; }
let ratio = stat.filtered_time_ms / char_b_stat.filtered_time_ms;
Some((ratio - 1.0) * 100.0)
}
```
**Rename `update_redundancy_streak()`****`update_error_anomaly_streak()`** (line 142):
- Same logic, uses renamed constant and renamed field
**New `update_speed_anomaly_streak()`**:
- Same pattern as error streak: call `speed_anomaly_pct()`, compare against `SPEED_ANOMALY_PCT_THRESHOLD`
- If `speed_anomaly_pct()` returns `None` (char baseline unavailable/under-sampled), **hold previous streak value** — don't reset or increment. The bigram simply can't be evaluated for speed yet.
- Requires both bigram samples >= `ANOMALY_MIN_SAMPLES` AND char_b samples >= `MIN_CHAR_SAMPLES_FOR_SPEED` before any streak update occurs.
**New `BigramAnomaly` struct**:
```rust
pub struct BigramAnomaly {
pub key: BigramKey,
pub anomaly_pct: f64,
pub sample_count: usize,
pub streak: u8,
pub confirmed: bool, // streak >= ANOMALY_STREAK_REQUIRED && samples >= MIN_SAMPLES_FOR_FOCUS
}
```
**Replace `focus_eligible_bigrams()` + `watchlist_bigrams()`** with:
- **`error_anomaly_bigrams(&self, char_stats: &KeyStatsStore, unlocked: &[char]) -> Vec<BigramAnomaly>`** — All bigrams with `error_anomaly_ratio > ERROR_ANOMALY_RATIO_THRESHOLD` and `samples >= ANOMALY_MIN_SAMPLES`, sorted by anomaly_pct desc. Each entry's `confirmed` flag = `error_anomaly_streak >= ANOMALY_STREAK_REQUIRED && samples >= MIN_SAMPLES_FOR_FOCUS`.
- **`speed_anomaly_bigrams(&self, char_stats: &KeyStatsStore, unlocked: &[char]) -> Vec<BigramAnomaly>`** — All bigrams where `speed_anomaly_pct() > Some(SPEED_ANOMALY_PCT_THRESHOLD)` and `samples >= ANOMALY_MIN_SAMPLES`, sorted by anomaly_pct desc. Same confirmed logic using `speed_anomaly_streak`.
**Replace `weakest_bigram()`** with **`worst_confirmed_anomaly()`**:
- Takes `char_stats: &KeyStatsStore` and `unlocked: &[char]`
- Collects all confirmed error anomalies and confirmed speed anomalies into a single candidate pool
- Each candidate is `(BigramKey, anomaly_pct, anomaly_type)` where type is `Error` or `Speed`
- **Dedup per bigram**: If a bigram appears in both error and speed lists, keep whichever has higher anomaly_pct (or prefer error on tie)
- Return the single bigram with highest anomaly_pct, or None if no confirmed anomalies
- This eliminates ambiguity about same-bigram-in-both-lists — each bigram gets at most one candidacy
**Update `FocusReasoning` enum** (line 471):
Current variants are: `BigramWins { bigram_difficulty, char_difficulty, char_key }`, `CharWins { char_key, char_difficulty, bigram_best }`, `NoBigrams { char_key }`, `Fallback`.
Replace with:
```rust
pub enum FocusReasoning {
BigramWins {
bigram_anomaly_pct: f64,
anomaly_type: AnomalyType, // Error or Speed
char_key: Option<char>,
},
CharWins {
char_key: char,
bigram_best: Option<(BigramKey, f64)>,
},
NoBigrams {
char_key: char,
},
Fallback,
}
pub enum AnomalyType { Error, Speed }
```
**Update `select_focus_target_with_reasoning()`** (line 489):
- Call `worst_confirmed_anomaly()` instead of `weakest_bigram()`
- **Focus priority rule**: Any confirmed bigram anomaly always wins over char focus. Rationale: char focus is the default skill-tree progression mechanism; confirmed bigram anomalies are exceptional problems that survived a conservative gate (3 consecutive drills above threshold + 20 samples). No cross-scale score comparison needed — confirmation itself is the signal.
- When no confirmed bigram anomalies exist, fall back to char focus as before.
- Anomaly_pct is unbounded (e.g. 200% = 3x worse than expected) — this is fine because confirmation gating prevents transient spikes from stealing focus, and the value is only used for ranking among confirmed anomalies, not for threshold comparison against char scores.
**Update `select_focus_target()`** (line 545):
- Same delegation change, pass `char_stats` through
### 2. `src/app.rs` — Streak update call sites & store cleanup
**`target_cpm` removal checklist** (complete audit of all references):
| Location | What | Action |
|---|---|---|
| `ngram_stats.rs:105-106` | `BigramStatsStore.target_cpm` field + serde attr | Remove field |
| `ngram_stats.rs:288-289` | `TrigramStatsStore.target_cpm` field + serde attr | Remove field |
| `ngram_stats.rs:109-111` | `fn default_target_cpm()` helper | Remove function |
| `ngram_stats.rs:11` | `const DEFAULT_TARGET_CPM` | Remove constant |
| `ngram_stats.rs:115` | `BigramStatsStore::update()` target_time_ms calc | Remove line |
| `ngram_stats.rs:294` | `TrigramStatsStore::update()` target_time_ms calc | Remove line |
| `ngram_stats.rs:1386` | Test helper `bigram_stats.target_cpm = DEFAULT_TARGET_CPM` | Remove line |
| `app.rs:1155` | `self.bigram_stats.target_cpm = ...` in rebuild_ngram_stats | Remove line |
| `app.rs:1157` | `self.ranked_bigram_stats.target_cpm = ...` | Remove line |
| `app.rs:1159` | `self.trigram_stats.target_cpm = ...` | Remove line |
| `app.rs:1161` | `self.ranked_trigram_stats.target_cpm = ...` | Remove line |
| `key_stats.rs:37` | `KeyStatsStore.target_cpm` | **KEEP** — used by `update_key()` for char confidence |
| `app.rs:330,332,609,611,1320,1322,1897-1898,1964-1965` | `key_stats.target_cpm = ...` | **KEEP** — KeyStatsStore still uses target_cpm |
| `config.rs:142` | `fn target_cpm()` | **KEEP** — still used by KeyStatsStore |
**At all 6 `update_redundancy_streak` call sites** (lines 899, 915, 1044, 1195, 1212, plus rebuild):
- Rename to `update_error_anomaly_streak()`
- Add parallel call to `update_speed_anomaly_streak()` passing the appropriate `&KeyStatsStore`:
- `&self.key_stats` for `self.bigram_stats` updates
- `&self.ranked_key_stats` for `self.ranked_bigram_stats` updates
**Update `select_focus_target` calls** in `generate_drill` (line ~663) and drill header in main.rs:
- Add `ranked_key_stats` parameter (already available at call sites)
### 3. `src/ui/components/stats_dashboard.rs` — Two-column anomaly display
**Replace data structs**:
- Remove `EligibleBigramRow` (line 20) and `WatchlistBigramRow` (line 30)
- Add single `AnomalyBigramRow`:
```rust
pub struct AnomalyBigramRow {
pub pair: String,
pub anomaly_pct: f64,
pub sample_count: usize,
pub streak: u8,
pub confirmed: bool,
}
```
**Replace `NgramTabData` fields** (line 39):
- Remove `eligible_bigrams: Vec<EligibleBigramRow>` and `watchlist_bigrams: Vec<WatchlistBigramRow>`
- Add `error_anomalies: Vec<AnomalyBigramRow>` and `speed_anomalies: Vec<AnomalyBigramRow>`
**Replace render functions**:
- Remove `render_eligible_bigrams()` (line 1473) and `render_watchlist_bigrams()` (line 1560)
- Add `render_error_anomalies()` and `render_speed_anomalies()`
- Each renders a table with columns: `Pair | Anomaly% | Samples | Streak`
- Confirmed rows (`.confirmed == true`) use highlight/accent color
- Unconfirmed rows use dimmer/warning color
- Column titles: `" Error Anomalies ({}) "` and `" Speed Anomalies ({}) "`
- Empty states: `" No error anomalies detected"` / `" No speed anomalies detected"`
**Narrow-width adaptation**:
- Wide mode (width >= 60): 50/50 horizontal split, full columns `Pair | Anomaly% | Samples | Streak`
- Narrow mode (width < 60): Stack vertically (error on top, speed below). Compact columns: `Pair | Anom% | Smp`
- Drop `Streak` column
- Abbreviate headers
- This mirrors the existing pattern used by the current eligible/watchlist tables
- **Vertical space budget** (stacked mode): Each panel gets a minimum of 3 data rows (+ 1 header + 1 border = 5 lines). Remaining vertical space is split evenly. If total available height < 10 lines, show only error anomalies panel (speed anomalies are less actionable). This prevents one panel from starving the other.
**Update `render_ngram_tab()`** (line 1308):
- Split the bottom section into two horizontal chunks (50/50)
- Left: `render_error_anomalies()`, Right: `render_speed_anomalies()`
- On narrow terminals (width < 60), stack vertically instead
### 4. `src/main.rs` — Bridge adapter
**`build_ngram_tab_data()`** (~line 2232):
- Call `error_anomaly_bigrams()` and `speed_anomaly_bigrams()` instead of old functions
- Map `BigramAnomaly` → `AnomalyBigramRow`
- Pass `&ranked_key_stats` for speed anomaly computation
**Drill header** (~line 1133): `select_focus_target()` signature change (adding `char_stats` param) will require updating the call here.
---
## Files Modified
1. **`src/engine/ngram_stats.rs`** — Core metrics overhaul (remove confidence from NgramStat, remove target_cpm from stores, add two anomaly systems, new query functions)
2. **`src/app.rs`** — Update streak calls, remove target_cpm initialization, update select_focus_target calls
3. **`src/ui/components/stats_dashboard.rs`** — Two-column anomaly display, new data structs, narrow-width adaptation
4. **`src/main.rs`** — Bridge adapter, select_focus_target call update
---
## Test Updates
- **Rewrite `test_focus_eligible_bigrams_gating`** → `test_error_anomaly_bigrams`: Test that bigrams above error threshold with sufficient samples appear; confirmed flag set correctly based on streak + samples
- **Rewrite `test_watchlist_bigrams_gating`** → split into `test_error_anomaly_confirmation` and `test_speed_anomaly_bigrams`
- **New `test_speed_anomaly_pct`**: Verify speed anomaly calculation against mock char stats; verify None returned when char_b has < MIN_CHAR_SAMPLES_FOR_SPEED (10) samples; verify correct result at exactly 10 samples (boundary)
- **New `test_speed_anomaly_streak_holds_when_char_unavailable`**: Verify streak is not reset when char baseline is insufficient (samples 0, 5, 9 — all below threshold)
- **New `test_speed_anomaly_borderline_baseline`**: Verify behavior at sample count transitions (9 → None, 10 → Some) and that early-session noise at exactly 10 samples produces reasonable anomaly values (not extreme outliers from EMA initialization bias)
- **Update `test_weakest_bigram*`** → `test_worst_confirmed_anomaly*`: Verify it picks highest anomaly across both types, deduplicates per bigram preferring higher pct (error on tie), returns None when nothing confirmed
- **Update focus reasoning tests**: Update `FocusReasoning` variants to new names (`BigramWins` now carries `anomaly_pct` and `anomaly_type` instead of `bigram_difficulty`)
- **Update `build_ngram_tab_data_maps_fields_correctly`**: Check `error_anomalies`/`speed_anomalies` fields with `AnomalyBigramRow` assertions
---
## Verification
1. `cargo build` — no compile errors
2. `cargo test` — all tests pass
3. Manual: N-grams tab shows two columns (Error Anomalies / Speed Anomalies)
4. Manual: Confirmed problem bigrams appear highlighted; unconfirmed appear dimmer
5. Manual: Drill header still shows `Focus: "th"` for bigram focus
6. Manual: Bigrams previously stuck on watchlist due to negative difficulty now appear as confirmed error anomalies
7. Manual: On narrow terminal (< 60 cols), columns stack vertically with compact headers

View File

@@ -0,0 +1,351 @@
# Plan: EMA Error Decay + Integrated Bigram/Char Focus Generation
## Context
Two problems with the current n-gram focus system:
1. **Focus stickiness**: Bigram anomaly uses cumulative `(error_count+1)/(sample_count+2)` Laplace smoothing. A bigram with 20 errors / 25 samples would need ~54 consecutive correct strokes to drop below the 1.5x threshold. Once confirmed, a bigram dominates focus for many drills even as the user visibly improves, while worse bigrams can't take over.
2. **Post-processing bigram focus causes repetition**: When a bigram is in focus, `apply_bigram_focus()` post-processes finished text by replacing 40% of words with dictionary words containing the bigram. This selects randomly from candidates with no duplicate tracking, causing repeated words. It also means the bigram doesn't influence the actual word selection — it's bolted on after generation and overrides the focused char (the weakest char gets replaced by bigram[0]).
This plan addresses both: (A) switch error rate to EMA so anomalies respond to recent performance, and (B) integrate bigram focus directly into the word selection algorithm alongside char focus, enabling both to be active simultaneously.
---
## Part A: EMA Error Rate Decay
### Approach
Add an `error_rate_ema: f64` field to both `NgramStat` and `KeyStat`, updated via exponential moving average on each keystroke (same pattern as existing `filtered_time_ms`). Use this EMA for all anomaly computations instead of cumulative `(error_count+1)/(sample_count+2)`.
Both bigram AND char error rates must use EMA — `error_anomaly_ratio` divides one by the other, so asymmetric decay would distort the comparison.
**Alpha = 0.1** (same as timing EMA). Half-life ~7 samples. A bigram at 30% error rate recovering with all-correct strokes: drops below 1.5x threshold after ~15 correct (~2 drills). This is responsive without being twitchy.
### Changes
#### `src/engine/ngram_stats.rs`
**NgramStat struct** (line 34):
- Add `error_rate_ema: f64` with `#[serde(default = "default_error_rate_ema")]` and default value `0.5`
- Add `fn default_error_rate_ema() -> f64 { 0.5 }` (Laplace-equivalent neutral prior)
- Remove `recent_correct: Vec<bool>` — superseded by EMA and never read
**`update_stat()`** (line 67):
- After existing `error_count` increment, add EMA update:
```rust
let error_signal = if correct { 0.0 } else { 1.0 };
if stat.sample_count == 1 {
stat.error_rate_ema = error_signal;
} else {
stat.error_rate_ema = EMA_ALPHA * error_signal + (1.0 - EMA_ALPHA) * stat.error_rate_ema;
}
```
- Remove `recent_correct` push/trim logic (lines 89-92)
- Keep `error_count` and `sample_count` (needed for gating thresholds and display)
**`smoothed_error_rate_raw()`** (line 95): Remove. After `smoothed_error_rate()` on both BigramStatsStore and TrigramStatsStore switch to `error_rate_ema`, this function has no callers.
**`BigramStatsStore::smoothed_error_rate()`** (line 120): Change to return `stat.error_rate_ema` instead of `smoothed_error_rate_raw(stat.error_count, stat.sample_count)`.
**`TrigramStatsStore::smoothed_error_rate()`** (line 333): Same change — return `stat.error_rate_ema`.
**`error_anomaly_ratio()`** (line 123): No changes needed — it calls `self.smoothed_error_rate()` and `char_stats.smoothed_error_rate()`, which now both return EMA values.
**Default for NgramStat** (line 50): Set `error_rate_ema: 0.5` (neutral — same as Laplace `(0+1)/(0+2)`).
#### `src/engine/key_stats.rs`
**KeyStat struct** (line 7):
- Add `error_rate_ema: f64` with `#[serde(default = "default_error_rate_ema")]` and default value `0.5`
- Add `fn default_error_rate_ema() -> f64 { 0.5 }` helper
- **Note**: KeyStat IS persisted to disk. The `#[serde(default)]` ensures backward compat — existing data without the field gets 0.5.
**`update_key()`** (line 50) — called for correct strokes:
- Add EMA update: `stat.error_rate_ema = if stat.total_count == 1 { 0.0 } else { EMA_ALPHA * 0.0 + (1.0 - EMA_ALPHA) * stat.error_rate_ema }`
- Use `total_count` (already incremented on the line before) to detect first sample
**`update_key_error()`** (line 83) — called for error strokes:
- Add EMA update: `stat.error_rate_ema = if stat.total_count == 1 { 1.0 } else { EMA_ALPHA * 1.0 + (1.0 - EMA_ALPHA) * stat.error_rate_ema }`
**`smoothed_error_rate()`** (line 90): Change to return `stat.error_rate_ema` (or 0.5 for missing keys).
#### `src/app.rs`
**`rebuild_ngram_stats()`** (line 1155):
- Reset `error_rate_ema` to `0.5` alongside `error_count` and `total_count` for KeyStat stores (lines 1165-1172)
- NgramStat stores already reset to `Default` which has `error_rate_ema: 0.5`
- The replay loop (line 1177) naturally rebuilds EMA by calling `update_stat()` and `update_key()`/`update_key_error()` in order
No other app.rs changes needed — the streak update and focus selection code reads through `error_anomaly_ratio()` which now uses EMA values transparently.
---
## Part B: Integrated Bigram + Char Focus Generation
### Approach
Replace the exclusive `FocusTarget` enum (either char OR bigram) with a `FocusSelection` struct that carries both independently. The weakest char comes from skill_tree progression; the worst bigram anomaly comes from the anomaly system. Both feed into the `PhoneticGenerator` simultaneously. Remove `apply_bigram_focus()` post-processing entirely.
### Changes
#### `src/engine/ngram_stats.rs` — Focus selection
**Replace `FocusTarget` enum** (line 510):
```rust
// Old
pub enum FocusTarget { Char(char), Bigram(BigramKey) }
// New
#[derive(Clone, Debug, PartialEq)]
pub struct FocusSelection {
pub char_focus: Option<char>,
pub bigram_focus: Option<(BigramKey, f64, AnomalyType)>,
}
```
**Replace `FocusReasoning` enum** (line 523):
```rust
// Old
pub enum FocusReasoning {
BigramWins { bigram_anomaly_pct: f64, anomaly_type: AnomalyType, char_key: Option<char> },
CharWins { char_key: char, bigram_best: Option<(BigramKey, f64)> },
NoBigrams { char_key: char },
Fallback,
}
// New — reasoning is now just the selection itself (both fields self-describe)
// FocusReasoning is removed; FocusSelection carries all needed info.
```
**Simplify `select_focus_target_with_reasoning()`** → **`select_focus()`**:
```rust
pub fn select_focus(
skill_tree: &SkillTree,
scope: DrillScope,
ranked_key_stats: &KeyStatsStore,
ranked_bigram_stats: &BigramStatsStore,
) -> FocusSelection {
let unlocked = skill_tree.unlocked_keys(scope);
let char_focus = skill_tree.focused_key(scope, ranked_key_stats);
let bigram_focus = ranked_bigram_stats.worst_confirmed_anomaly(ranked_key_stats, &unlocked);
FocusSelection { char_focus, bigram_focus }
}
```
Remove `select_focus_target()` and `select_focus_target_with_reasoning()` — replaced by `select_focus()`.
#### `src/generator/mod.rs` — Trait update
**Update `TextGenerator` trait** (line 14):
```rust
pub trait TextGenerator {
fn generate(
&mut self,
filter: &CharFilter,
focused_char: Option<char>,
focused_bigram: Option<[char; 2]>,
word_count: usize,
) -> String;
}
```
#### `src/generator/phonetic.rs` — Integrated word selection
**`generate()` method** — rewrite word selection with tiered approach:
Note: `find_matching(filter, None)` is used (not `focused_char`) because we do our own tiering below. `find_matching` returns ALL words matching the CharFilter — the `focused` param only sorts, never filters — but passing `None` avoids an unnecessary sort we'd discard anyway.
```rust
fn generate(
&mut self,
filter: &CharFilter,
focused_char: Option<char>,
focused_bigram: Option<[char; 2]>,
word_count: usize,
) -> String {
let matching_words: Vec<String> = self.dictionary
.find_matching(filter, None) // no char-sort; we tier ourselves
.iter().map(|s| s.to_string()).collect();
let use_real_words = matching_words.len() >= MIN_REAL_WORDS;
// Pre-categorize words into tiers for real-word mode
let bigram_str = focused_bigram.map(|b| format!("{}{}", b[0], b[1]));
let focus_char_lower = focused_char.filter(|ch| ch.is_ascii_lowercase());
let (bigram_indices, char_indices, other_indices) = if use_real_words {
let mut bi = Vec::new();
let mut ci = Vec::new();
let mut oi = Vec::new();
for (i, w) in matching_words.iter().enumerate() {
if bigram_str.as_ref().is_some_and(|b| w.contains(b.as_str())) {
bi.push(i);
} else if focus_char_lower.is_some_and(|ch| w.contains(ch)) {
ci.push(i);
} else {
oi.push(i);
}
}
(bi, ci, oi)
} else {
(vec![], vec![], vec![])
};
let mut words: Vec<String> = Vec::new();
let mut recent: Vec<String> = Vec::new(); // anti-repeat window
for _ in 0..word_count {
if use_real_words {
let word = self.pick_tiered_word(
&matching_words,
&bigram_indices,
&char_indices,
&other_indices,
&recent,
);
recent.push(word.clone());
if recent.len() > 4 { recent.remove(0); }
words.push(word);
} else {
let word = self.generate_phonetic_word(
filter, focused_char, focused_bigram,
);
words.push(word);
}
}
words.join(" ")
}
```
**New `pick_tiered_word()` method**:
```rust
fn pick_tiered_word(
&mut self,
all_words: &[String],
bigram_indices: &[usize],
char_indices: &[usize],
other_indices: &[usize],
recent: &[String],
) -> String {
// Tier selection probabilities:
// Both available: 40% bigram, 30% char, 30% other
// Only bigram: 50% bigram, 50% other
// Only char: 70% char, 30% other (matches current behavior)
// Neither: 100% other
//
// Try up to 6 times to avoid repeating a recent word.
for _ in 0..6 {
let tier = self.select_tier(bigram_indices, char_indices, other_indices);
let idx = tier[self.rng.gen_range(0..tier.len())];
let word = &all_words[idx];
if !recent.contains(word) {
return word.clone();
}
}
// Fallback: accept any non-recent word from full pool
let idx = self.rng.gen_range(0..all_words.len());
all_words[idx].clone()
}
```
**`select_tier()` helper**: Returns reference to the tier to sample from based on availability and probability roll. Only considers a tier "available" if it has >= 2 words (prevents unavoidable repeats when a tier has just 1 word and the anti-repeat window rejects it). Falls through to the next tier when the selected tier is too small.
**`try_generate_word()` / `generate_phonetic_word()`** — add bigram awareness for Markov fallback:
- Accept `focused_bigram: Option<[char; 2]>` parameter
- Only attempt bigram forcing when both chars pass the CharFilter (avoids pathological starts when bigram chars are rare/unavailable in current filter scope)
- When eligible: 30% chance to start word with bigram[0] and force bigram[1] as second char, then continue Markov chain from `[' ', bigram[0], bigram[1]]` prefix
- Falls back to existing focused_char logic otherwise
#### `src/generator/code_syntax.rs` + `src/generator/passage.rs`
Add `_focused_bigram: Option<[char; 2]>` parameter to their `generate()` signatures (ignored, matching trait).
#### `src/app.rs` — Pipeline update
**`generate_text()`** (line 653):
- Call `select_focus()` (new function) instead of `select_focus_target()`
- Extract `focused_char` from `selection.char_focus` (the actual weakest char)
- Extract `focused_bigram` from `selection.bigram_focus.map(|(k, _, _)| k.0)`
- Pass both to `generator.generate(filter, focused_char, focused_bigram, word_count)`
- **Remove** the `apply_bigram_focus()` call (lines 784-787)
- Post-processing passes (capitalize, punctuate, numbers, code_patterns) continue to receive `focused_char` — this is now the real weakest char, not the bigram's first char
**Remove `apply_bigram_focus()`** method (lines 1087-1131) entirely.
**Store `FocusSelection`** on App:
- Add `pub current_focus: Option<FocusSelection>` field to App (default `None`)
- Set in `generate_text()` right after `select_focus()` — captures the focus that was actually used to generate the current drill's text
- **Lifecycle**: Set when drill starts (in `generate_text()`). Persists through the drill result screen (so the user sees what was in focus for the drill they just completed). Cleared to `None` when: starting the next drill (overwritten), leaving drill screen, changing drill scope/mode, or on import/reset. This is a snapshot, not live-recomputed — the header always shows what generated the current text.
- Used by drill header display in main.rs (reads `app.current_focus` instead of re-calling `select_focus()`)
#### `src/main.rs` — Drill header + stats adapter
**Drill header** (line 1134):
- Read `app.current_focus` to build focus_text (no re-computation — shows what generated the text)
- Display format: `Focus: 'n' + "th"` (both), `Focus: 'n'` (char only), `Focus: "th"` (bigram only)
- Replace the current `select_focus_target()` call with reading the stored selection
- When `current_focus` is `None`, show no focus text
**`build_ngram_tab_data()`** (line 2253):
- Call `select_focus()` instead of `select_focus_target_with_reasoning()`
- Update `NgramTabData` struct: replace `focus_target: FocusTarget` and `focus_reasoning: FocusReasoning` with `focus: FocusSelection`
#### `src/ui/components/stats_dashboard.rs` — Focus panel
**`NgramTabData`** (line 28):
- Replace `focus_target: FocusTarget` and `focus_reasoning: FocusReasoning` with `focus: FocusSelection`
- Remove `FocusTarget` and `FocusReasoning` imports
**`render_ngram_focus()`** (line 1352):
- Show both focus targets when both active:
- Line 1: `Focus: Char 'n' + Bigram "th"` (or just one if only one active)
- Line 2: Details — `Char 'n': weakest key | Bigram "th": error anomaly 250%`
- When neither active: show fallback message
- Rendering adapts based on which focuses are present
---
## Files Modified
1. **`src/engine/ngram_stats.rs`** — EMA field on NgramStat, EMA-based smoothed_error_rate, `FocusSelection` struct, `select_focus()`, remove old FocusTarget/FocusReasoning
2. **`src/engine/key_stats.rs`** — EMA field on KeyStat, EMA updates in update_key/update_key_error, EMA-based smoothed_error_rate
3. **`src/generator/mod.rs`** — TextGenerator trait: add `focused_bigram` parameter
4. **`src/generator/phonetic.rs`** — Tiered word selection with bigram+char, anti-repeat window, Markov bigram awareness
5. **`src/generator/code_syntax.rs`** — Add ignored `focused_bigram` parameter
6. **`src/generator/passage.rs`** — Add ignored `focused_bigram` parameter
7. **`src/app.rs`** — Use `select_focus()`, pass both focuses to generator, remove `apply_bigram_focus()`, store `current_focus`
8. **`src/main.rs`** — Update drill header, update `build_ngram_tab_data()` adapter
9. **`src/ui/components/stats_dashboard.rs`** — Update NgramTabData, render_ngram_focus for dual focus display
---
## Test Updates
### Part A (EMA)
- **Update `test_error_anomaly_bigrams`**: Set `error_rate_ema` directly instead of relying on cumulative error_count/sample_count for anomaly ratio computation
- **Update `test_worst_confirmed_anomaly_dedup`** and **`_prefers_error_on_tie`**: Same — set EMA values
- **New `test_error_rate_ema_decay`**: Verify that after N correct strokes, error_rate_ema drops as expected. Verify anomaly ratio crosses below threshold after reasonable recovery (~15 correct strokes from 30% error rate).
- **New `test_error_rate_ema_rebuild_from_history`**: Verify that rebuilding from drill history produces same EMA as live updates (deterministic replay)
- **New `test_ema_ranking_stability_during_recovery`**: Two bigrams both confirmed. Bigram A has higher anomaly. User corrects bigram A over several drills while bigram B stays bad. Verify that A's anomaly drops below B's and B becomes the new worst_confirmed_anomaly — clean handoff without oscillation.
- **Update key_stats tests**: Verify EMA updates in `update_key()` and `update_key_error()`, backward compat (serde default)
### Part B (Integrated focus)
- **Replace focus reasoning tests** (`test_select_focus_with_reasoning_*`): Replace with `test_select_focus_*` testing `FocusSelection` struct — verify both char_focus and bigram_focus are populated independently
- **New `test_phonetic_bigram_focus_increases_bigram_words`**: Generate 1200 words with focused_bigram, verify significantly more words contain the bigram than without
- **New `test_phonetic_dual_focus_no_excessive_repeats`**: Generate text with both focuses, verify no word appears > 3 times consecutively
- **Update `build_ngram_tab_data_maps_fields_correctly`**: Update for `FocusSelection` struct instead of FocusTarget/FocusReasoning
- **New `test_find_matching_focused_is_sort_only`** (in `dictionary.rs` or `phonetic.rs`): Verify that `find_matching(filter, Some('k'))` and `find_matching(filter, None)` return the same set of words (same membership, potentially different order). Guards against future regressions where focused param accidentally becomes a filter.
- No `apply_bigram_focus` tests exist to remove (method was untested)
---
## Verification
1. `cargo build` — no compile errors
2. `cargo test` — all tests pass
3. Manual: Start adaptive drill, observe both char and bigram appearing in focus header
4. Manual: Verify drill text contains focused bigram words AND focused char words mixed naturally
5. Manual: Verify no excessive word repetition (the old apply_bigram_focus problem)
6. Manual: Practice a bigram focus target correctly for 2-3 drills → verify it drops out of focus and a different bigram (or char-only) takes over
7. Manual: N-grams tab shows both focuses in the Active Focus panel
8. Manual: Narrow terminal (<60 cols) stacks anomaly panels vertically; very short terminal (<10 rows available for panels) shows only error anomalies panel; focus panel always shows at least line 1

View File

@@ -0,0 +1,93 @@
# Adaptive Auto-Continue Input Lock Overlay
## Context
In adaptive mode, when a drill completes with no milestone popups to show, the app auto-continues to the next drill immediately (`finish_drill``start_drill()` with no intermediate screen). The existing 800ms input lock (`POST_DRILL_INPUT_LOCK_MS`) is only armed when there IS an intermediate screen (DrillResult or milestone popup). This means trailing keystrokes from the previous drill can bleed into the next drill as unintended inputs.
The fix: arm the same 800ms lock during adaptive auto-continue, block drill input while it's active, and show a small countdown popup overlay on the drill screen so the user knows why their input is temporarily ignored.
## Changes
### 1. Arm the lock on adaptive auto-continue
**`src/app.rs``finish_drill()`**
Currently the auto-continue path does not arm the lock:
```rust
if self.drill_mode == DrillMode::Adaptive && self.milestone_queue.is_empty() {
self.start_drill();
}
```
Add `arm_post_drill_input_lock()` after `start_drill()`. It must come after because `start_drill()` calls `clear_post_drill_input_lock()` as its first action (to clear stale locks from manual continues). Re-arming immediately after means the 800ms window starts from when the new drill begins:
```rust
if self.drill_mode == DrillMode::Adaptive && self.milestone_queue.is_empty() {
self.start_drill();
self.arm_post_drill_input_lock();
}
```
**Event ordering safety**: The event loop in `run_app()` is single-threaded: `draw``events.next()``handle_key` → loop. `finish_drill()` runs inside a `handle_key()` call, so both `start_drill()` and `arm_post_drill_input_lock()` complete within the same event iteration. Any buffered key events are processed in subsequent loop iterations, where the lock is already active.
### 2. Allow Ctrl+C through the lock and add Drill screen to lock guard
**`src/main.rs``handle_key()`**
Move the Ctrl+C quit handler ABOVE the input lock guard so it always works, even during lockout. Then add `AppScreen::Drill` to the lock guard:
```rust
// Ctrl+C always quits, even during input lock
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
app.should_quit = true;
return;
}
// Briefly block all input right after a drill completes to avoid accidental
// popup dismissal or continuation from trailing keystrokes.
if app.post_drill_input_lock_remaining_ms().is_some()
&& (!app.milestone_queue.is_empty()
|| app.screen == AppScreen::DrillResult
|| app.screen == AppScreen::Drill)
{
return;
}
```
This is a behavior change for the existing DrillResult/milestone lock too: previously Ctrl+C was blocked during the 800ms window, now it passes through. All other keys remain blocked. The 800ms window is short enough that blocking everything else is not disruptive.
### 3. Render lock overlay on the drill screen
**`src/main.rs` — end of `render_drill()`**
After all existing drill UI is rendered, if the lock is active, draw a small centered popup overlay on top of the typing area:
- Check `app.post_drill_input_lock_remaining_ms()` — if `None`, skip overlay entirely
- **Size**: 3 rows tall (top border + message + bottom border), width = message length + 4 (border + padding), centered within the full `area` rect
- **Clear** the overlay rect with `ratatui::widgets::Clear`
- **Block**: `Block::bordered()` with `colors.accent()` border style and `colors.bg()` background — same pattern as `render_milestone_overlay`
- **Message**: `"Keys re-enabled in {ms}ms"` as a `Paragraph` with `colors.text_pending()` style — matches the milestone overlay footer color
- Render inside the block's `inner()` area
This overlay is intentionally small (single bordered line) since the drill content should remain visible behind it and it only appears for ≤800ms.
**Countdown repainting**: The event loop (`run_app()`) uses `EventHandler::new(Duration::from_millis(100))` which sends `AppEvent::Tick` every 100ms when idle. Each tick triggers `terminal.draw()`, which re-renders the drill screen. `post_drill_input_lock_remaining_ms()` recomputes the remaining time from `Instant::now()` on each call, so the countdown value updates every ~100ms without any additional machinery.
### 4. Tests
**`src/app.rs`** — add to the existing `#[cfg(test)] mod tests`:
1. **`adaptive_auto_continue_arms_input_lock`**: Create `App`, verify it starts in adaptive mode with a drill. Simulate completing the drill by calling `finish_drill()` (set up drill state as complete first). Assert `post_drill_input_lock_remaining_ms().is_some()` and `screen == AppScreen::Drill` after auto-continue.
2. **`adaptive_auto_continue_lock_not_armed_with_milestones`**: Same setup but push a milestone into `milestone_queue` before calling `finish_drill()`. Assert `screen == AppScreen::DrillResult` (not auto-continued) and lock is armed via the existing milestone path.
## Files to modify
- `src/app.rs` — 1-line addition in `finish_drill()` auto-continue path; 2 tests
- `src/main.rs` — extend input lock guard condition in `handle_key()`; add overlay rendering at end of `render_drill()`
## Verification
1. `cargo test` — all existing and new tests pass
2. Manual: start adaptive drill, complete it. Verify small popup appears briefly over the next drill, countdown decrements every ~100ms, then disappears and typing works normally
3. Manual: complete adaptive drill that triggers a milestone popup. Verify milestone popup still works as before (no double-lock or interference)
4. Manual: complete Code or Passage drill. Verify DrillResult screen lockout still works as before

View File

@@ -0,0 +1,90 @@
# Adaptive Drill Word Diversity
## Context
When adaptive drills focus on characters/bigrams with few matching dictionary words, the same words repeat excessively both within and across drills. Currently:
- **Within-drill dedup** uses a sliding window of only 4 words — too small when the matching word pool is small
- **Cross-drill**: no tracking at all — each drill creates a fresh `PhoneticGenerator` with no memory of previous drills
- **Dictionary vs phonetic is binary**: if `matching_words >= 15` use dictionary only, if `< 15` use phonetic only. A pool of 16 words gets 100% dictionary (lots of repeats), while 14 gets 0% dictionary
## Changes
### 1. Cross-drill word history
Add `adaptive_word_history: VecDeque<HashSet<String>>` to `App` that tracks words from the last 5 adaptive drills. Pass a flattened `HashSet<String>` into `PhoneticGenerator::new()`.
**Word normalization**: Capture words from the generator output *before* capitalization/punctuation/numbers post-processing (the `generator.generate()` call in `generate_text()` produces lowercase-only text). This means words in history are always lowercase ASCII with no punctuation — no normalization function needed since the generator already guarantees this format.
**`src/app.rs`**:
- Add `adaptive_word_history: VecDeque<HashSet<String>>` to `App` struct, initialize empty
- In `generate_text()`, before creating the generator: flatten history into `HashSet` and pass to constructor
- After `generator.generate()` returns (before capitalization/punctuation): `split_whitespace()` into a `HashSet`, push to history, pop front if `len > 5`
**Lifecycle/reset rules**:
- Clear `adaptive_word_history` when `drill_mode` changes away from `Adaptive` (i.e., switching to Code/Passage mode)
- Clear when `drill_scope` changes (switching between branches or global/branch)
- Do NOT persist across app restarts — session-local only (it's a `VecDeque`, not serialized)
- Do NOT clear on gradual key unlocks — as the skill tree progresses one key at a time, history should carry over to maintain cross-drill diversity within the same learning progression
- The effective "adaptive context key" is `(drill_mode, drill_scope)` — history clears when either changes. Other parameters (focus char, focus bigram, filter) change naturally within a learning progression and should not trigger resets
- This prevents cross-contamination between unrelated drill contexts while preserving continuity during normal adaptive flow
**`src/generator/phonetic.rs`**:
- Add `cross_drill_history: HashSet<String>` field to `PhoneticGenerator`
- Update constructor to accept it
- In `pick_tiered_word()`, use weighted suppression instead of hard exclusion:
- When selecting a candidate word, if it's in within-drill `recent`, always reject
- If it's in `cross_drill_history`, accept it with reduced probability based on pool coverage:
- Guard: if pool is empty, skip suppression logic entirely (fall through to phonetic generation in hybrid mode)
- `history_coverage = cross_drill_history.intersection(pool).count() as f64 / pool.len() as f64`
- `accept_prob = 0.15 + 0.60 * history_coverage` (range: 15% when history covers few pool words → 75% when history covers most of the pool)
- This prevents over-suppression in small pools where history covers most words, while still penalizing repeats in large pools
- Scale attempt count to `pool_size.clamp(6, 12)` with final fallback accepting any non-recent word
- Compute `accept_prob` once at the start of `generate()` alongside tier categorization (not per-attempt)
### 2. Hybrid dictionary + phonetic mode
Replace the binary threshold with a gradient that mixes dictionary and phonetic words.
**`src/generator/phonetic.rs`**:
- Change constants: `MIN_REAL_WORDS = 8` (below: phonetic only), add `FULL_DICT_THRESHOLD = 60` (above: dictionary only)
- Calculate `dict_ratio` as linear interpolation: `(count - 8) / (60 - 8)` clamped to `[0.0, 1.0]`
- In the word generation loop, for each word: roll against `dict_ratio` to decide dictionary vs phonetic
- Tier categorization still happens when `count >= MIN_REAL_WORDS` (needed for dictionary picks)
- Phonetic words also participate in the `recent` dedup window (already handled since all words push to `recent`)
### 3. Scale within-drill dedup window
Replace the fixed window of 4 with a window proportional to the **filtered dictionary match count** (the `matching_words` vec computed at the top of `generate()`):
- `pool_size <= 20`: window = `pool_size.saturating_sub(1).max(4)`
- `pool_size > 20`: window = `(pool_size / 4).min(20)`
- In hybrid mode, this is based on the dictionary pool size regardless of phonetic mixing — phonetic words add diversity naturally, so the window governs dictionary repeat pressure
### 4. Tests
All tests use seeded `SmallRng::seed_from_u64()` for determinism (existing pattern in codebase).
**Update existing tests**: Add `HashSet::new()` to `PhoneticGenerator::new()` constructor calls (3 tests).
**New tests** (all use `SmallRng::seed_from_u64()` for determinism):
1. **Cross-drill history suppresses repeats**: Generate drill 1 with seeded RNG and constrained filter (~20 matching words), collect word set. Generate drill 2 with same filter but different seed, no history — compute Jaccard index as baseline. Generate drill 2 again with drill 1's words as history — compute Jaccard index. Assert history Jaccard is at least 0.15 lower than baseline Jaccard (i.e., measurably less overlap). Use 100-word drills.
2. **Hybrid mode produces mixed output**: Use a filter that yields ~30 dictionary matches. Generate 500 words with seeded RNG. Collect output words and check against the dictionary match set. With ~30 matches, `dict_ratio ≈ 0.42`. Since the seed is fixed, the output is deterministic — the band of 25%-65% accommodates potential future seed changes rather than runtime variance. Assert dictionary word percentage is within this range, and document the actual observed value for the chosen seed in a comment.
3. **Boundary conditions**: With 5 matching words → assert 0% dictionary words (all phonetic). With 100+ matching words → assert 100% dictionary words. Seeded RNG.
4. **Weighted suppression graceful degradation**: Create a pool of 10 words with history containing 8 of them. Generate 50 words. Verify no panics, output is non-empty, and history words still appear (suppression is soft, not hard exclusion).
## Files to modify
- `src/generator/phonetic.rs` — core changes: hybrid mixing, cross-drill history field, weighted suppression in `pick_tiered_word`, dedup window scaling
- `src/app.rs` — add `adaptive_word_history` field, wire through `generate_text()`, add reset logic on mode/scope changes
- `src/generator/mod.rs` — no changes (`TextGenerator` trait signature unchanged for API stability; the `cross_drill_history` parameter is internal to `PhoneticGenerator`'s constructor, not the trait interface)
## Verification
1. `cargo test` — all existing and new tests pass
2. Manual test: start adaptive drill on an early skill tree branch (few unlocked letters, ~15-30 matching words). Run 5+ consecutive drills. Measure: unique words across 5 drills should be notably higher than before (target: >70% unique across 5 drills for pools of 20+ words)
3. Full alphabet test: with all keys unlocked, behavior should be essentially unchanged (dict_ratio ≈ 1.0, large pool, no phonetic mixing)
4. Scope change test: switch between branch drill and global drill, verify no stale history leaks

View File

@@ -0,0 +1,35 @@
# Prevent Tests from Writing to Real User Data
## Context
Two tests in `src/app.rs` (`adaptive_auto_continue_arms_input_lock` and `adaptive_does_not_auto_continue_with_milestones`) call `App::new()` which connects to the real `JsonStore` at `~/.local/share/keydr/`. When they call `finish_drill()``save_data()`, fake drill results get persisted to the user's actual history file. All other app tests also use `App::new()` but happen to not call `finish_drill()`.
## Changes
### 1. Add `#[cfg(not(test))]` gate on `App::new()` (`src/app.rs:293`)
Mark `App::new()` with `#[cfg(not(test))]` so it cannot be called from test code at all. This is a compile-time guarantee — any future test that tries `App::new()` will fail to compile.
### 2. Add `App::new_test()` (`src/app.rs`, in `#[cfg(test)]` block)
Add a `pub fn new_test()` constructor inside a `#[cfg(test)] impl App` block that mirrors `App::new()` but sets `store: None`. This prevents any persistence to disk. All existing fields get their default/empty values (no loading from disk either).
Since most test fields just need defaults and a started drill, the test constructor can be minimal:
- `Config::default()`, `Theme::default()` (leaked), `Menu::new()`, `store: None`
- Default key stats, skill tree, profile, empty drill history
- `Dictionary::load()`, `TransitionTable`, `KeyboardModel` — same as production (needed for `start_drill()`)
- Call `start_drill()` at the end (same as `App::new()`)
### 3. Update all existing tests to use `App::new_test()`
Replace every `App::new()` call in the test module with `App::new_test()`. This covers all 7 tests in `#[cfg(test)] mod tests`.
## File to Modify
- `src/app.rs` — gate `new()`, add `new_test()`, update test calls
## Verification
1. `cargo test` — all tests pass
2. `cargo build` — production build still compiles (ungated `new()` available)
3. Temporarily add `App::new()` in a test → should fail to compile

View File

@@ -0,0 +1,150 @@
# Plan: Enhanced Path Input with Cursor Navigation and Tab Completion
## Context
Settings page path fields (code download dir, passage download dir, export path, import path) currently only support appending characters and backspace — no cursor movement, no arrow keys, no tab completion. Users can't easily correct a typo in the middle of a path or navigate to an existing file for import.
## Approach: Custom `LineInput` struct + filesystem tab completion
Neither `tui-input` nor `tui-textarea` provide tab/path completion, and both have crossterm version mismatches with our deps (ratatui 0.30 + crossterm 0.28). A custom struct avoids dependency churn and gives us exactly the features we need.
## New file: `src/ui/line_input.rs`
### Struct
```rust
/// Which settings path field is being edited.
pub enum PathField {
CodeDownloadDir,
PassageDownloadDir,
ExportPath,
ImportPath,
}
pub enum InputResult {
Continue,
Submit,
Cancel,
}
pub struct LineInput {
text: String,
cursor: usize, // char index (NOT byte offset)
completions: Vec<String>,
completion_index: Option<usize>,
completion_seed: String, // text snapshot when Tab first pressed
completion_error: bool, // true if last read_dir failed
}
```
Cursor is stored as a **char index** (0 = before first char, `text.chars().count()` = after last char). Conversion to byte offset happens only at mutation boundaries via `text.char_indices()`.
### Keyboard handling
| Key | Action | Resets completion? |
|-----|--------|--------------------|
| Left | Move cursor one char left | Yes |
| Right | Move cursor one char right | Yes |
| Home / Ctrl+A | Move cursor to start | Yes |
| End / Ctrl+E | Move cursor to end | Yes |
| Backspace | Delete char before cursor | Yes |
| Delete | Delete char at cursor | Yes |
| Ctrl+U | Clear entire line | Yes |
| Ctrl+W | Delete word before cursor (see semantics below) | Yes |
| Tab | Cycle completions forward (end-of-line only) | No |
| BackTab | Cycle completions backward | No |
| Printable char | Insert at cursor | Yes |
| Esc | Return `InputResult::Cancel` | — |
| Enter | Return `InputResult::Submit` | — |
**Only Tab/BackTab preserve** the completion session. All other keys reset it.
**Ctrl+W semantics**: From cursor position, first skip any consecutive whitespace to the left, then delete the contiguous non-whitespace run. This matches standard readline/bash `unix-word-rubout` behavior. Example: `"foo bar |"` → Ctrl+W → `"foo |"`.
### Tab completion
Tab completion **only activates when cursor is at end-of-line**. If cursor is in the middle, Tab is a no-op.
1. **First Tab** (`completion_index` is `None`): Snapshot `text` as `completion_seed`. Split into (directory, partial_filename) using the last path separator. Expand leading `~` to `dirs::home_dir()` for the `read_dir` call only — preserve `~` in output text. Call `std::fs::read_dir` with a **scan budget of 1000 entries** (iterate at most 1000 `DirEntry` results). From the scanned entries, filter those whose name starts with `partial_filename` (always case-sensitive — this is an intentional simplification; case-insensitive matching on macOS HFS+/Windows NTFS is not in scope). Hidden files (names starting with `.`) only included when `partial_filename` starts with `.`. Sort matching candidates: **directories first, then files, alphabetical within each group**. Cap the final candidate list at 100. On any `read_dir` or entry I/O error, produce zero completions and set `completion_error = true` (renders `"(cannot read directory)"` in footer).
2. **Cycling**: Increment/decrement `completion_index`, wrapping. Replace `text` with selected completion. Directories get a trailing `std::path::MAIN_SEPARATOR`. Cursor moves to end.
3. **Reset**: Any non-Tab/BackTab key clears completion state **and** clears `completion_error`. This means the error hint disappears on the next keystroke (text mutation, cursor move, submit, or cancel).
4. **Mixed paths** like `~/../tmp` are not normalized — they're passed through as-is.
5. **Hidden-file filtering** (`.-prefix only` rule) applies identically on all platforms.
### Rendering
```rust
impl LineInput {
/// Returns (before_cursor, cursor_char, after_cursor) for styled rendering.
pub fn render_parts(&self) -> (&str, Option<char>, &str);
pub fn value(&self) -> &str;
}
```
When cursor is at end of text, `cursor_char` is `None` and a **space with inverted background** is rendered as the cursor (avoids font/glyph compatibility issues with block characters across terminals). When cursor is in the middle, the character at cursor position is rendered with inverted colors (swapped fg/bg).
## Changes to existing files
### `src/ui/mod.rs`
- Add `pub mod line_input;`
### `src/app.rs`
- Replace three booleans (`settings_editing_download_dir`, `settings_editing_export_path`, `settings_editing_import_path`) with:
```rust
pub settings_editing_path: Option<(PathField, LineInput)>,
```
- `settings_export_path` and `settings_import_path` remain as `String`. On editing start, `LineInput` is initialized from current value. On `Submit`, value is written back to the field identified by `PathField`.
- `clear_settings_modals()` sets `settings_editing_path` to `None`.
- Add `is_editing_path(&self) -> bool` and `is_editing_field(&self, index: usize) -> bool` helpers.
**Migration checklist** — all sites referencing the old booleans must be updated. Verify by grepping for the removed field names (`settings_editing_download_dir`, `settings_editing_export_path`, `settings_editing_import_path`) — zero hits after migration:
- `src/main.rs` handle_settings_key priority 4 block (~line 550)
- `src/main.rs` Enter handler for fields 5, 9, 12, 14 (~line 605)
- `src/main.rs` render_settings `is_editing_this_path` check (~line 2693)
- `src/main.rs` `any_path_editing` footer check (~line 2724)
- `src/app.rs` field declarations (~line 218)
- `src/app.rs` `clear_settings_modals` (~line 462)
- `src/app.rs` `Default` / `new` initialization
### `src/main.rs` — `handle_settings_key()`
- **Priority 4 block**: Replace with `if let Some((field, ref mut input)) = app.settings_editing_path`. Call `input.handle(key)` and match on result. `Submit` writes value back via `field`, `Cancel` discards.
- **Enter on path fields**: Construct `LineInput::new(current_value)` paired with appropriate `PathField` variant.
### `src/main.rs` — `render_settings()`
- When editing, render via `input.render_parts()` with cursor char in inverted style.
- Footer hints in editing state: `"[←→] Move [Tab] Complete (at end) [Enter] Confirm [Esc] Cancel"`
- If `input.completion_error` is true, append `"(cannot read directory)"` to footer. Clears on next keystroke.
## Key files
- `src/ui/line_input.rs` — new
- `src/ui/mod.rs` — add module
- `src/app.rs` — state fields, `clear_settings_modals()`, helper methods
- `src/main.rs` — key handling, rendering, footer
## Verification
1. `cargo build` — no warnings
2. `cargo test` — all existing + new unit tests pass
3. **Unit tests for `LineInput`** (in `line_input.rs`):
- Insert char at start, middle, end
- Delete/backspace at boundaries (start of line, end, empty string)
- Ctrl+W: `"foo bar "` → `"foo "`, `" foo"` → `" "`, `""` → `""`
- Cursor: left at 0 stays 0, right at end stays at end
- Home/End position correctly
- Ctrl+U clears text and cursor to 0
- Tab at end-of-line with no match → no completions, no panic
- Tab at mid-line → no-op
- Tab cycling wraps around; BackTab cycles reverse
- Non-Tab key resets completion state
- `render_parts()` returns correct slices at start, middle, end positions
4. **Grep verification**: `grep -rn 'settings_editing_download_dir\|settings_editing_export_path\|settings_editing_import_path' src/` returns zero hits
5. Manual testing:
- Navigate to Export Path, press Enter → cursor appears at end
- Arrow left/right moves cursor, Home/End work
- Backspace/Delete at cursor position, Ctrl+U/Ctrl+W
- Type partial path, Tab → completions cycle; Shift+Tab reverses
- Tab on directory appends separator, allows continued completion
- Tab on nonexistent path → footer shows "(cannot read directory)"
- Enter confirms, Esc cancels (value reverts)
- All four path fields (code dir, passage dir, export, import) work identically

View File

@@ -0,0 +1,203 @@
# Plan: Create Test User Profiles at Various Skill Tree Progression Levels
## Context
We need importable JSON test profiles representing users at every meaningful stage of skill tree progression. Each profile must have internally consistent key stats, drill history, and skill tree state so the app behaves as if a real user reached that level. The profiles will be used for manual regression testing of UI and logic at each progression stage.
## Design Decisions
- **Ranked mode**: Only profiles 5+ (multi-branch and beyond) include ranked key stats and ranked drills; earlier profiles have empty ranked stats since new users wouldn't have encountered ranked mode yet.
- **`total_score`**: Synthetic plausible value per profile (not replayed from drill history). The goal is UI/progression testing, not scoring fidelity. Score is set to produce a reasonable `level_from_score()` result for each progression stage.
- **Key stats coverage**: `KeyStatsStore` contains only keys that have been practiced (have at least one correct keystroke in drill history). Unlocked-but-unpracticed keys are absent — this is realistic since a freshly unlocked key has no stats. Locked keys are always absent.
- **Fixtures are committed assets**: Generated once, checked into `test-profiles/`. The generator binary is kept for regeneration if schema evolves. Output is deterministic (no RNG — all values computed from formulas).
- **Timestamps**: Monotonically increasing, spaced ~2 minutes apart within a day, spread across days matching `streak_days`. `last_practice_date` derived from the last drill timestamp.
## Consistency Invariants
Every generated profile must satisfy:
1. `KeyStatsStore` contains only practiced keys (subset of unlocked keys). No locked-branch keys ever appear. Every key in stats must have `sample_count > 0`.
2. `KeyStat.confidence >= 1.0` for all keys in completed levels; `< 1.0` for keys in the current in-progress level that are still being learned.
3. `ProfileData.total_drills == drill_history.drills.len()`
4. `ProfileData.total_score` is a plausible synthetic value producing a reasonable level via `level_from_score()`.
5. `ProfileData.streak_days` and `last_practice_date` are consistent with drill timestamps.
6. `DrillResult.per_key_times` only reference keys from the profile's final unlocked set. (Temporal progression fidelity within drill history is a non-goal — all drills use the final-state key pool for simplicity. The goal is testing UI/import behavior at each progression snapshot, not simulating the exact journey.)
7. `ranked_key_stats` is empty (default) for profiles 1-4; populated for profiles 5-7 with stats for keys appearing in ranked drills.
8. Branch marked `Complete` only if all keys in all levels have `confidence >= 1.0`.
9. Drill timestamps are monotonically increasing across the full history.
## Profiles
All files in `test-profiles/` at project root. Each is a valid `ExportData` JSON.
### 1. `01-brand-new.json` — Fresh Start
- **Skill tree**: Lowercase `InProgress` level 0, all others `Locked`
- **Key stats**: Empty
- **Ranked key stats**: Empty
- **Drill history**: Empty (0 drills)
- **Profile**: 0 drills, 0 score, 0 streak
- **Tests**: Initial onboarding, first-run UI, empty dashboard
### 2. `02-early-lowercase.json` — Early Lowercase (10 keys)
- **Skill tree**: Lowercase `InProgress` level 4 (6 base + 4 unlocked = 10 keys: e,t,a,o,i,n,s,h,r,d)
- **Key stats**: e,t,a,o,i,n at confidence >= 1.0 (mastered); s,h,r,d at confidence 0.3-0.7
- **Ranked key stats**: Empty
- **Drill history**: 15 adaptive drills
- **Profile**: 15 drills, synthetic score, 3-day streak
- **Tests**: Progressive lowercase unlock, focused key targeting weak keys, early dashboard
### 3. `03-mid-lowercase.json` — Mid Lowercase (18 keys)
- **Skill tree**: Lowercase `InProgress` level 12 (6 + 12 = 18 keys, through 'y')
- **Key stats**: First 14 keys mastered, next 4 at confidence 0.4-0.8
- **Ranked key stats**: Empty
- **Drill history**: 50 adaptive drills
- **Profile**: 50 drills, synthetic score, 7-day streak
- **Tests**: Many keys unlocked, skill tree partial progress display
Note: Lowercase level semantics — `current_level` = number of keys unlocked beyond the initial 6 (`LOWERCASE_MIN_KEYS`). So level 12 means 18 total keys.
### 4. `04-lowercase-complete.json` — Lowercase Complete
- **Skill tree**: Lowercase `Complete` (level 20, all 26 keys), all others `Available`
- **Key stats**: All 26 lowercase at confidence >= 1.0
- **Ranked key stats**: Empty
- **Drill history**: 100 adaptive drills
- **Profile**: 100 drills, synthetic score, 14-day streak
- **Tests**: Branch completion, all branches showing Available, branch start UI
### 5. `05-multi-branch.json` — Multiple Branches In Progress
- **Skill tree**:
- Lowercase: `Complete`
- Capitals: `InProgress` level 1 (L1 mastered, working on L2 "Name Capitals")
- Numbers: `InProgress` level 0 (working on L1 "Common Digits")
- Prose Punctuation: `InProgress` level 0 (working on L1 "Essential")
- Whitespace: `Available`
- Code Symbols: `Available`
- **Key stats**: All lowercase mastered; T,I,A,S,W,H,B,M mastered; J,D,R,C,E partial; 1,2,3 partial; period/comma/apostrophe partial
- **Ranked key stats**: Some ranked stats for lowercase keys (from ~20 ranked drills)
- **Drill history**: 200 drills (170 adaptive, 10 passage, 20 ranked adaptive)
- **Profile**: 200 drills, synthetic score, 21-day streak
- **Tests**: Multi-branch progress, branch-specific drills, global vs branch focus selection
### 6. `06-advanced.json` — Most Branches Complete
- **Skill tree**:
- Lowercase: `Complete`
- Capitals: `Complete`
- Numbers: `Complete`
- Prose Punctuation: `Complete`
- Whitespace: `Complete`
- Code Symbols: `InProgress` level 2 (L1+L2 done, working on L3 "Logic & Reference")
- **Key stats**: All mastered except Code Symbols L3 (&,|,^,~,!) at partial confidence and L4 absent
- **Ranked key stats**: Substantial ranked stats across all mastered keys
- **Drill history**: 500 drills (350 adaptive, 50 passage, 50 code, 50 ranked)
- **Profile**: 500 drills, synthetic score, 45-day streak, best_streak: 60
- **Tests**: Near-endgame, almost all keys, code symbols progression
### 7. `07-fully-complete.json` — Everything Mastered
- **Skill tree**: ALL branches `Complete`
- **Key stats**: All keys confidence >= 1.0, high sample counts, low error rates
- **Ranked key stats**: Full ranked stats for all keys
- **Drill history**: 800 drills (400 adaptive, 150 passage, 150 code, 100 ranked)
- **Profile**: 800 drills, synthetic score, 90-day streak
- **Tests**: Endgame, all complete, full dashboard, comprehensive ranked data
## Implementation
### File: `src/bin/generate_test_profiles.rs`
A standalone binary that imports keydr crate types and generates all profiles.
#### Helpers
```rust
/// Generate KeyStat with deterministic values derived from target confidence.
/// filtered_time_ms = target_time_ms / confidence
/// best_time_ms = filtered_time_ms * 0.85
/// sample_count and recent_times scaled to confidence level
fn make_key_stat(confidence: f64, sample_count: usize, target_cpm: f64) -> KeyStat
/// Generate a DrillResult with deterministic per_key_times.
/// Keys are chosen from the provided unlocked set.
fn make_drill_result(
wpm: f64, accuracy: f64, char_count: usize,
keys: &[char], timestamp: DateTime<Utc>,
mode: &str, ranked: bool,
) -> DrillResult
/// Wrap all components into ExportData.
fn make_export(
config: Config,
profile: ProfileData,
key_stats: KeyStatsData,
ranked_key_stats: KeyStatsData,
drill_history: DrillHistoryData,
) -> ExportData
/// Generate monotonic timestamps: base_date + day_offset + drill_offset * 2min
fn drill_timestamp(base: DateTime<Utc>, day: u32, drill_in_day: u32) -> DateTime<Utc>
```
#### Profile builders
One function per profile (`build_profile_01()` through `build_profile_07()`) that:
1. Constructs `SkillTreeProgress` with exact branch statuses and levels
2. Builds `KeyStatsStore` with stats only for unlocked/practiced keys
3. Generates drill history with proper timestamps and key references
4. Sets `total_score` to a synthetic plausible value for the progression stage
5. Derives `last_practice_date` and streak from drill timestamps
6. Returns `ExportData`
#### Main
```rust
fn main() {
fs::create_dir_all("test-profiles").unwrap();
for (name, data) in [
("01-brand-new", build_profile_01()),
("02-early-lowercase", build_profile_02()),
// ...
] {
let json = serde_json::to_string_pretty(&data).unwrap();
fs::write(format!("test-profiles/{name}.json"), json).unwrap();
}
}
```
### Key source files referenced
- `src/store/schema.rs` — ExportData, ProfileData, KeyStatsData, DrillHistoryData
- `src/engine/skill_tree.rs` — SkillTreeProgress, BranchProgress, BranchStatus, level definitions, LOWERCASE_MIN_KEYS=6
- `src/engine/key_stats.rs` — KeyStatsStore, KeyStat, DEFAULT_TARGET_CPM=175.0
- `src/session/result.rs` — DrillResult, KeyTime
- `src/config.rs` — Config defaults
- `src/engine/scoring.rs` — compute_score(), level_from_score()
## Verification
### Automated: `tests/test_profile_fixtures.rs`
Integration tests (separate from the generator binary) that for each generated JSON file:
- Deserializes into `ExportData` successfully
- Asserts `total_drills == drills.len()`
- Asserts no locked-branch keys appear in `KeyStatsStore`
- Asserts all keys in completed levels have `confidence >= 1.0`
- Asserts all keys in stats have `sample_count > 0`
- Asserts timestamps are monotonically increasing
- Asserts `ranked_key_stats` is empty for profiles 1-4
- Imports into a temp `JsonStore` via `import_all()` without error
### Manual smoke test per profile
| Profile | Check |
|---------|-------|
| 01 | Dashboard shows level 1, 0 drills, empty skill tree except lowercase InProgress |
| 02 | Skill tree shows 10/26 lowercase keys, focused key is from the weak-key pool (s,h,r,d) |
| 03 | Skill tree shows 18/26 lowercase keys, dashboard stats populated |
| 04 | All 6 branches visible, 5 show "Available", lowercase shows "Complete" |
| 05 | 3 branches InProgress with level indicators, branch drill selector works |
| 06 | 5 branches Complete, Code Symbols shows L3 in progress |
| 07 | All branches Complete, all stats filled, ranked data visible |
### Generation
`cargo run --bin generate_test_profiles` produces 7 files in `test-profiles/`
Generated JSON files are committed to the repo. CI runs fixture validation tests against the committed files (no regeneration step). If the schema changes, the developer reruns the generator manually and commits the updated fixtures.

View File

@@ -0,0 +1,572 @@
# keydr Multilingual Dictionary + Keyboard Layout Internationalization Plan
## Context
We currently use an English-only dictionary and an ASCII-centric adaptive model:
- Dictionary is hardcoded to `assets/words-en.json` in `src/generator/dictionary.rs`.
- Dictionary ingestion filters to ASCII lowercase only (`is_ascii_lowercase`).
- Transition table building (`src/generator/transition_table.rs`) skips non-ASCII words.
- Adaptive drill generation in `src/app.rs` builds lowercase filter from `is_ascii_lowercase`.
- Skill tree lowercase branch is fixed to English `a-z` frequency in `src/engine/skill_tree.rs`.
- Keyboard rendering/hit-testing logic has hardcoded row offsets and row count assumptions in `src/ui/components/keyboard_diagram.rs` and `src/ui/components/stats_dashboard.rs`.
## Explicit product decision: clean break
This app is currently work-in-progress and has no real user base. We explicitly do
not need to preserve old config/state/export compatibility for this change. If data
must be recreated from scratch, that is acceptable.
## Goals
1. Add user-selectable dictionary language (default `en`) using keybr-provided dictionary files.
2. Add user-selectable keyboard layout profiles for multiple languages.
3. Ensure keyboard visualizations, explorer, and stats heatmaps render correctly for variable row shapes and non-English keycaps.
4. Use a clean-break implementation with no backward-compatibility requirements.
5. Maintain license compliance for newly imported dictionaries.
## Non-goals (first delivery)
1. Full IME/dead-key composition support.
2. Full rewrite of adaptive model for every script from day one.
3. Perfect locale-specific pedagogy for all languages in phase 1.
4. Backward compatibility for old config/profile/export data.
## Execution constraints (must be explicit before implementation)
1. **Unicode normalization policy:** Use NFC as canonical storage/matching form for dictionary ingestion, generated text, keystroke comparison, and persisted stats keys. Do not use NFKC in phase 1 to avoid compatibility-fold surprises.
2. **Character equivalence policy:** Equality is by normalized scalar sequence (NFC), not by glyph appearance. Composed/decomposed equivalents must compare equal after normalization.
3. **Clean-break schema cutover policy:** This rollout uses hard reset semantics for old unscoped stats/profile files. On first run of the new schema version, old files are ignored (optionally archived with `.legacy` suffix); no partial migration path.
4. **Capability gating policy:** Only language/layout pairs marked supported in the registry capability matrix are selectable in UI during phased rollout.
5. **Performance envelope policy:** Keyboard geometry recomputation must be bounded and cached by profile key + render mode + viewport size.
## Upstream data availability
`keybr-content-words` includes dictionaries for:
`ar, be, cs, da, de, el, en, es, et, fa, fi, fr, he, hr, hu, it, ja, lt, lv, nb, nl, pl, pt, ro, ru, sl, sv, th, tr, uk`
Recommended rollout strategy:
- Initial support for Latin-script languages first (`en, de, es, fr, it, pt, nl, sv, da, nb, fi, pl, cs, ro, hr, hu, lt, lv, sl, et, tr`).
- Later support for non-Latin scripts (`el, ru, uk, be, ar, fa, he, ja, th`) after script-specific input/model behavior is in place.
---
## Key Architectural Decisions
### 1) Language Pack registry
Add a registry module (e.g. `src/l10n/language_pack.rs`) containing:
- `language_key`
- `display_name`
- `script`
- `dictionary_asset_id`
- `supported_keyboard_layout_keys`
- `primary_letter_sequence` (for ranked progression)
- `starter_weights` and optional `vowel_set` for generator fallback behavior
- `support_level` (`full`, `experimental`, `blocked`)
- `normalization_form` (phase 1 fixed to `NFC`)
- `input_capabilities` (for example `direct_letters_only`, `needs_ime`)
This becomes the single source of truth for language behavior.
### 2) Runtime dictionary/generator rebuild is required
Changing `dictionary_language` must immediately take effect without restart.
Implement `App::rebuild_language_assets(&mut self)` that rebuilds:
- `Dictionary`
- `TransitionTable`
- any cached generator state derived from language assets
- focused-character transforms derived from language rules
- drill-generation allowlists that depend on language pack data
Call it whenever language or language-dependent layout changes in settings.
`rebuild_language_assets` must also refresh capitalization/case behavior inputs used by adaptive generation.
`rebuild_language_assets` invalidation contract (required):
- always invalidate and rebuild `Dictionary` and `TransitionTable`
- clear adaptive cross-drill dictionary history cache
- clear/refresh any cached language-specific focus mapping
- do **not** mutate in-progress drill text
- all newly generated drills after rebuild must use new language assets
### 3) Asset loading strategy: compile-time embedded assets
For Phase 1 scope, dictionaries will be embedded at compile-time (generated asset map + `include_str!`/equivalent), not runtime file discovery.
Rationale:
- deterministic packaging
- no runtime path resolution complexity
- simpler cross-platform behavior
Tradeoff: larger binary size, acceptable for this phase.
### 4) Transition table fallback strategy
`TransitionTable::build_english()` will be gated to `language_key == "en"` only.
For non-English languages:
- use dictionary-derived transition table only
- if sparse, degrade gracefully to simple dictionary sampling behavior rather than English fallback model
### 5) Keyboard geometry refactor strategy
`src/ui/components/keyboard_diagram.rs` is a substantial refactor (all render and hit-test paths).
Implement shared `KeyboardGeometry` computed once per render context and consumed by:
- compact/full/fallback renderers
- all key hit-testing paths
- shift hit-testing paths
No duplicate hardcoded offsets should remain.
Performance constraints for geometry:
- geometry cache key: `(layout_key, render_mode, viewport_width, viewport_height)`
- recompute only when cache key changes
- hit-testing must be O(number_of_keys) or better per event with no per-key allocation
- include a benchmark/smoke check to detect regressions in repeated render/hit-test loops
### 6) Finger assignment source of truth
Finger assignment must be profile metadata, not inferred by QWERTY column heuristics.
Each keyboard profile defines finger mapping for each physical key position.
### 7) Stats isolation strategy
Stats are language-scoped and layout-scoped.
Adopt per-scope storage files (for example):
- `key_stats_<language>_<layout>.json`
- `key_stats_ranked_<language>_<layout>.json`
- optional scoped drill history files
No mixed-language key stats in a single store.
Profile/scoring scoping policy:
- `skill_tree` progress is language-scoped (at minimum by `language_key`).
- `total_score`, `total_drills`, `streak_days`, and `best_streak` remain global.
- `ProfileData` will separate global fields from language-scoped progression state.
Scoped-file discovery mechanism:
- registry-driven + current-config driven only
- app loads current scope directly and only enumerates scopes from supported language/layout registry pairs
- no unconstrained glob-based discovery of arbitrary stale files
Import/export strategy for scoped stats:
- export bundles all supported scoped stats files present in the data dir
- each bundle entry includes explicit `language_key` and `layout_key` metadata
- import applies two-phase commit per scoped target file
- export/import also includes language-scoped `skill_tree` progress entries with `language_key` metadata
Atomicity requirements for scoped import:
- stage writes to `<target>.tmp`
- flush file contents (`sync_all`) before rename
- rename temp file onto target atomically where supported
- on any failure, remove temp file and keep existing target untouched
- no commit of partially imported scope bundles
### 8) Settings architecture
Current index-based settings handling is fragile.
Phase 1 includes refactor from positional integer indices to enum/struct-based settings entries before adding multilingual controls.
Profile key validation must be registry-backed. Do not rely on `KeyboardModel::from_name()` fallback behavior.
Validation error taxonomy (typed, stable):
- `UnknownLanguage`
- `UnknownLayout`
- `UnsupportedLanguageLayoutPair`
- `LanguageBlockedBySupportLevel`
UI must show deterministic user-facing error text for each class (used by tests).
In-progress drill behavior on language/layout change:
- language/layout changes rebuild assets immediately for future generation
- current in-progress drill text is not mutated mid-drill
- new language/layout applies on the next drill generation
### 9) Unicode handling architecture
Define one shared Unicode utility module used by dictionary ingestion, generators, and input matching:
- normalize all dictionary entries to NFC at load time
- normalize typed characters before comparison against expected text
- normalize persisted per-key identifiers before write/read
- provide helper tests for composed/decomposed equivalence (for example `é` vs `e + ◌́`)
### 10) Rollout capability matrix architecture
Add a single registry-backed capability matrix keyed by `(language_key, layout_key)`:
- `enabled`: selectable and fully supported
- `preview`: selectable with warning banner
- `disabled`: visible but not selectable
Phase-gating must read this matrix in settings and selection screens; no ad-hoc checks.
---
## Phased Implementation
## Phase 0: Data + compliance groundwork
### Tasks
1. Import selected dictionaries to `assets/dictionaries/words-<lang>.json`.
2. Add sidecar license/provenance files for each imported dictionary.
3. Update `THIRD_PARTY_NOTICES.md` with imported assets.
4. Add validation script for dictionary manifest/checksums.
5. Define language pack registry seed data (including temporary `primary_letter_sequence` values).
6. Add `support_level` and capability-matrix seed entries for every language/layout pair.
7. Add a build-time utility that derives letter frequency sequence from each dictionary (seed data source of truth; manual overrides allowed but documented).
8. Write `docs/unicode-normalization-policy.md` (NFC/equivalence rules + examples).
### Verification
1. All imported dictionaries listed in third-party notices.
2. Sidecar license/provenance file exists for each imported dictionary.
3. Manifest validation script passes.
4. Build-time frequency derivation utility emits reproducible output for seeded languages.
5. Unicode policy doc exists and includes composed/decomposed test cases.
---
## Phase 1: Settings and configuration foundation
### Tasks
1. Add `dictionary_language` to `Config`.
2. Refactor settings implementation from raw indices to typed settings entries (enum/descriptor model).
3. Add settings controls for:
- dictionary language
- canonical keyboard layout profile key
4. Implement explicit invalid combination handling (reject with message), not silent fallback.
5. Wire language/layout change actions to `App::rebuild_language_assets(&mut self)`.
6. Introduce clean-break schema/version update for config/profile/store formats with hard-reset behavior for old files.
7. Replace `from_name` wildcard fallback paths with explicit lookup failure handling tied to registry validation.
8. Update import/export schema and transaction flow for scoped stats bundles.
9. Split profile persistence into global fields + language-scoped skill tree progress map.
10. Enforce capability-matrix gating in settings/selectors (`enabled/preview/disabled` states).
11. Add typed validation errors and stable user-facing status messages.
### Code areas
- `src/config.rs`
- `src/main.rs` (settings UI rendering and input handling)
- `src/app.rs` (settings action handlers, rebuild trigger)
- `src/store/schema.rs`
- `src/store/json_store.rs`
### Verification
1. Unit tests for config defaults/validation.
2. Unit tests for settings navigation/editing after index refactor.
3. Runtime test: changing dictionary language updates generated drills without restart.
4. Runtime test: invalid language/layout pair is rejected with visible error/status.
5. Export/import test: scoped stats for multiple language/layout pairs round-trip correctly.
6. Runtime test: changing language mid-drill preserves current drill text and applies new language on next drill.
7. Schema cutover test: old-format files are ignored/archived and never partially loaded.
8. UI test: disabled/preview capability-matrix entries render and behave correctly.
---
## Phase 2: Dictionary, transition table, and generator internationalization
### Tasks
1. Refactor `Dictionary::load(language_key)` with embedded asset map.
2. Remove ASCII-only filtering from dictionary ingestion and transition building.
3. Extend `phonetic.rs` to remove English hardcoding:
- replace hardcoded starter biases with language-pack starter data or derived frequencies
- replace fallback `"the"` with language-aware fallback (for example: top dictionary word)
- make vowel recovery optional/parameterized by language pack
- remove `is_ascii_lowercase` focus filtering and rely on allowed-character logic
4. Implement transition fallback policy:
- `build_english()` only for English
- non-English graceful degradation path without English fallback table
5. Address adaptive and non-adaptive mode filters:
- remove hardcoded `('a'..='z')` filters in code/passage modes
- use language-pack allowed sets where applicable
6. Refactor capitalization pipeline to Unicode-aware behavior:
- replace ASCII-only case checks/conversions in `capitalize.rs`
- use Unicode case mapping and language-pack constraints
- ensure non-ASCII letters (for example `ä/Ä`, `é/É`) are handled correctly
7. Implement shared normalization utility and apply it consistently in:
- dictionary load path
- generated text comparison/matching paths
- persisted key identity paths
8. Multilingual audit checklist (required pass/fail):
- `rg -n "is_ascii" src/app.rs src/generator/*.rs` has no unreviewed hits affecting multilingual behavior
- every remaining `is_ascii*` hit has a documented justification comment or issue reference
### Code areas
- `src/generator/dictionary.rs`
- `src/generator/transition_table.rs`
- `src/generator/phonetic.rs`
- `src/generator/capitalize.rs`
- `src/app.rs` (adaptive/code/passage filter construction)
### Verification
1. Unit tests for dictionary loading per supported language.
2. Unit tests for transition table generation with non-English characters.
3. Unit tests for phonetic fallback behavior per language pack.
4. Unit tests for capitalization correctness on non-ASCII letters.
5. Regression tests for English output quality.
6. Unit tests for NFC normalization and composed/decomposed equivalence.
---
## Phase 3: Keyboard layout profile system
### Tasks
1. Replace ad-hoc constructors with canonical keyboard profile registry.
2. Add language-relevant profiles (`de_qwertz`, `fr_azerty`, etc.).
3. Add profile metadata:
- key rows and shifted/base pairs
- geometry hints
- modifier placement metadata
- per-key finger assignments
4. Remove legacy alias layer and enforce canonical profile keys.
5. Evaluate `src/keyboard/layout.rs` usage:
- if unused, delete it
- otherwise fold it into the new profile registry without duplicate sources of truth
### Code areas
- `src/keyboard/model.rs`
- `src/keyboard/layout.rs`
- `src/keyboard/display.rs` (if locale labels/short labels need extension)
- `src/config.rs`
### Verification
1. Unit tests for all canonical profile keys.
2. Unit tests for profile completeness and unique key mapping.
3. Unit tests for finger assignment coverage/consistency.
---
## Phase 4: Keyboard visualization and hit-testing refactor
### Tasks
1. Implement shared `KeyboardGeometry` used by all keyboard rendering modes.
2. Rewrite keyboard diagram rendering paths to use shared geometry.
3. Rewrite all keyboard hit-testing paths to use shared geometry.
4. Refactor stats dashboard keyboard heatmap/timing rendering to use profile geometry metadata.
5. Ensure explorer and selection logic works for variable row counts and locale keycaps.
6. Update sentinel boundary tests if new files must reference sentinel constants.
7. Remove ASCII shift-display guards in keyboard rendering:
- replace `is_ascii_alphabetic()`-based shifted display checks
- use profile-defined shiftability (`base != shifted` or explicit shiftable set)
8. Audit and replace ASCII-specific input-handling logic in `main.rs`:
- caps-lock inference
- depressed-key normalization
- shift guidance and shifted-key detection in keyboard UI paths
9. Add geometry cache and recompute guards keyed by `(layout_key, render_mode, viewport)` with benchmark coverage.
### Code areas
- `src/ui/components/keyboard_diagram.rs`
- `src/ui/components/stats_dashboard.rs`
- `src/main.rs` keyboard explorer handlers
- `src/main.rs` input handling (`handle_key`, caps/shift logic, keyboard guidance/render helpers)
- `src/app.rs` explorer state/focus use
- `src/keyboard/display.rs` tests
### Verification
1. Snapshot/golden tests for compact/full/fallback rendering per profile.
2. Hit-test roundtrip tests per profile.
3. Manual keyboard explorer smoke tests for US + non-US profiles.
4. Sentinel boundary tests pass with updated policy.
5. Manual test: shifted rendering works for non-ASCII letter keys where profile defines shifted forms.
6. Manual test: caps/shift guidance and depressed-key behavior are correct for non-ASCII key input.
7. Benchmark/smoke test: repeated render + hit-test loops meet baseline without per-frame geometry rebuild when cache key is unchanged.
---
## Phase 5: Skill tree and ranked progression internationalization
### Tasks
1. Replace fixed English lowercase progression with language-pack `primary_letter_sequence`.
2. Replace hardcoded "lowercase as background" branch logic with language-pack primary-letter background behavior.
3. Remove UI copy assumptions of "26 lowercase" and `a-z`.
4. Ensure ranked gating uses language-pack readiness (sequence + profile support).
5. Define letter-frequency derivation approach:
- derive initial sequence from dictionary frequency data (build-time utility), not hand-curated long-term
6. Milestone-copy audit checklist (required pass/fail):
- grep for hardcoded milestone language in `main.rs` (`26`, `a-z`, `A-Z`, `lowercase`)
- replace with language-pack-aware dynamic copy
- add tests asserting copy adjusts with different sequence lengths
### Code areas
- `src/engine/skill_tree.rs`
- `src/app.rs` (focus/background/filter logic)
- `src/main.rs` (milestone/help copy)
### Verification
1. Tests for progression with multiple language sequences.
2. Tests for background-branch selection correctness.
3. Snapshot tests for milestone text across languages.
---
## Phase 6: UX polish, test parameterization, and rollout
### Tasks
1. Add dedicated language/layout selector screens where needed.
- Implemented in `src/main.rs` + `src/app.rs` with `DictionaryLanguageSelect` and `KeyboardLayoutSelect`.
2. Add explicit support-matrix messaging for partially supported scripts.
- Implemented in selector + settings UI copy in `src/main.rs` (`preview`/`disabled` state messaging).
3. Add parameterized test helpers:
- language-aware allowed key sets
- expected progression counts
- profile fixtures
- Implemented via cross-language/layout fixtures and property tests in `src/l10n/language_pack.rs`, `src/engine/skill_tree.rs`, and `src/ui/components/keyboard_diagram.rs`.
4. Document that Phase 2 may temporarily allow language/dictionary mismatch with keyboard visuals until Phase 3/4 is complete.
5. Add explicit note in docs that Phase 2 mismatch window is expected and resolved by Phase 4.
- Implemented in `docs/multilingual-rollout-notes.md`.
6. Add cross-language property tests:
- key uniqueness per profile
- hit-test round-trip invariants
- progression monotonicity per language sequence
- Implemented in `src/keyboard/model.rs`, `src/ui/components/keyboard_diagram.rs`, and `src/engine/skill_tree.rs`.
### Code areas
- `src/main.rs`
- `src/app.rs`
- test modules across `src/*`
- `docs/`
### Verification
1. End-to-end manual flows for language switch + layout switch + drill generation + keyboard explorer + stats.
2. Performance checks for embedded dictionary footprint and startup latency.
3. Test suite passes with parameterized language/profile cases.
4. Property/invariant tests pass for key uniqueness, hit-test round-trip, and progression monotonicity.
---
## File-by-file Impact Matrix
### Core config and app wiring
- `src/config.rs`
- add `dictionary_language` and canonical `keyboard_layout` profile key validation
- `src/app.rs`
- add `rebuild_language_assets`
- remove ASCII-only filters and audit residual ASCII assumptions (`rg is_ascii` pass)
- wire settings actions to runtime rebuild
- `src/main.rs`
- refactor settings UI to typed entries
- add/update selectors and error/status handling
- audit/replace ASCII-specific input/caps/shift handling
### Generators and adaptive engine
- `src/generator/dictionary.rs`
- dynamic, language-aware load via embedded registry
- `src/generator/transition_table.rs`
- non-ASCII support and explicit English-only fallback gating
- `src/generator/phonetic.rs`
- remove hardcoded English starter/vowel/fallback assumptions
- `src/generator/capitalize.rs`
- replace ASCII-only casing logic with Unicode-aware capitalization rules
### Skill progression
- `src/engine/skill_tree.rs`
- language-pack primary sequence
- language-pack background branch behavior
### Keyboard modeling and visualization
- `src/keyboard/model.rs`
- canonical profile registry with per-key finger mapping
- `src/keyboard/layout.rs`
- delete or fold into model registry
- `src/ui/components/keyboard_diagram.rs`
- shared geometry + full hit-test rewrite
- `src/ui/components/stats_dashboard.rs`
- geometry-driven keyboard heatmap/timing rendering
- `src/keyboard/display.rs`
- sentinel boundary test updates as needed
### Persistence/schema
- `src/store/schema.rs`
- clean-break schema/version bump as needed
- split profile data into global fields + language-scoped skill tree progress
- `src/store/json_store.rs`
- scoped stats storage by language/layout
- scoped file discovery based on supported registry pairs
- export/import scoped bundle handling with language/layout metadata
- export/import language-scoped skill tree progress entries
### Assets/compliance/docs
- `assets/dictionaries/*`
- `assets/dictionaries/*.license`
- `THIRD_PARTY_NOTICES.md`
- `docs/license-compliance.md`
- `docs/unicode-normalization-policy.md`
---
## Risks and mitigations
1. **Risk:** Non-Latin scripts break assumptions in multiple modules.
- **Mitigation:** staged rollout by script; support matrix gating.
2. **Risk:** Keyboard visualization regressions during geometry rewrite.
- **Mitigation:** shared geometry abstraction + dedicated hit-test/render tests.
3. **Risk:** Clean-break schema reset discards local data.
- **Mitigation:** explicitly documented and accepted by product decision.
4. **Risk:** Settings refactor increases short-term scope.
- **Mitigation:** do it early to avoid repeated index-cascade bugs.
5. **Risk:** Embedded dictionary set increases binary size/startup memory.
- **Mitigation:** track size/startup metrics per release and switch to hybrid packaging if thresholds are exceeded.
---
## Definition of Done
1. Language switch updates dictionary-driven generation without restart.
2. Keyboard profiles are canonical and language-aware; no legacy alias dependency.
3. Keyboard diagram, explorer, and stats views are geometry-driven and correct for supported profiles.
4. Ranked progression uses language-pack primary sequences and background logic.
5. Code/passage/adaptive modes no longer depend on hardcoded `a-z` filters.
6. Stats are isolated by language/layout scope.
7. Skill tree progression is language-scoped while streak/score totals remain global.
8. Third-party attributions and license sidecars cover all imported dictionary assets.
9. Automated tests cover runtime rebuild, generator behavior, keyboard geometry/hit-testing, progression invariants, and parameterized language/profile cases.
10. Unicode normalization policy is implemented and tested across ingestion, generation, input matching, and persisted stats keys.
11. Clean-break schema cutover behavior is deterministic (hard-reset semantics) and covered by automated tests.
12. Capability matrix gating is enforced consistently across settings/selectors and covered by UI/runtime tests.

View File

@@ -0,0 +1,154 @@
# Skill Tree Milestone Popups
## Context
When users reach major skill tree milestones, they should see celebratory popups explaining what they've achieved and what's next. Four milestone types:
1. **Lowercase complete** — all 26 lowercase keys mastered, other branches become available
2. **Branch complete** — a non-lowercase branch fully mastered
3. **All keys unlocked** — every key on the keyboard is available for practice
4. **All keys mastered** — every key at full confidence, ultimate achievement
These popups appear after key unlock/mastery popups and before the drill summary screen, using the existing `milestone_queue` system. The existing post-drill input lock (800ms) applies to these popups when they're the first popup shown after a drill.
## Implementation
### 1. Extend `SkillTreeUpdate` (`src/engine/skill_tree.rs`)
Add fields to `SkillTreeUpdate`:
```rust
pub branches_newly_available: Vec<BranchId>, // Locked → Available transitions
pub branches_newly_completed: Vec<BranchId>, // → Complete transitions
pub all_keys_unlocked: bool, // every key now in practice pool
pub all_keys_mastered: bool, // every key at confidence >= 1.0
```
**In `update()`:**
- Snapshot non-lowercase branch statuses before the auto-unlock loop. After it, collect `Locked``Available` transitions into `branches_newly_available`.
- Snapshot all branch statuses before updates. After `update_lowercase()` and all `update_branch_level()` calls, collect branches that became `Complete` into `branches_newly_completed`.
- `all_keys_unlocked`: compare `total_unlocked_count()` against `compute_total_unique_keys()`. Set to `true` only if they're equal now AND they weren't equal before (using a before-snapshot of unlocked count).
- `all_keys_mastered`: `true` if every branch in `ALL_BRANCHES` has `BranchStatus::Complete` after updates AND at least one wasn't `Complete` before.
`BranchId` is already used across all layers. Display names come from `get_branch_definition(id).name`.
### 2. Add milestone variants to `MilestoneKind` (`src/app.rs`)
```rust
pub enum MilestoneKind {
Unlock,
Mastery,
BranchesAvailable, // lowercase complete → other branches available
BranchComplete, // a non-lowercase branch fully completed
AllKeysUnlocked, // every key on the keyboard is unlocked
AllKeysMastered, // every key at full confidence
}
```
**In `finish_drill()`, after mastery popup queueing**, check each flag and push popups in order:
1. `branches_newly_available` non-empty → push `BranchesAvailable`
2. `branches_newly_completed` non-empty (excluding `BranchId::Lowercase` since `BranchesAvailable` covers it) → push `BranchComplete`
3. `all_keys_unlocked` → push `AllKeysUnlocked`
4. `all_keys_mastered` → push `AllKeysMastered`
For all four: `keys` and `finger_info` are empty, `message` is unused. The renderer owns all copy.
**Input lock**: These popups are pushed to `milestone_queue`, so the existing check `!self.milestone_queue.is_empty()` at `finish_drill()` already triggers `arm_post_drill_input_lock()`. No changes needed — the lock applies to whatever the first popup is.
### 3. Render popup variants in `render_milestone_overlay()` (`src/main.rs`)
Each variant gets its own rendering branch. No keyboard diagram for any of these. All use the standard footer (input lock remaining / "Press any key to continue").
**`BranchesAvailable`:**
- Title: `"New Skill Branches Available!"`
- Body:
```
Congratulations! You've mastered all 26 lowercase
keys!
New skill branches are now available:
• Capitals A-Z
• Numbers 0-9
• Prose Punctuation
• Whitespace
• Code Symbols
Visit the Skill Tree to unlock a new branch and
start training!
Press [t] from the menu to open the Skill Tree
```
(Branch names rendered dynamically from `get_branch_definition(id).name` for each ID in `branches_newly_available`.)
**`BranchComplete`:**
- Title: `"Branch Complete!"`
- Body:
```
You've fully mastered the {branch_name} branch!
Other branches are waiting to be unlocked in the
Skill Tree. Keep going!
Press [t] from the menu to open the Skill Tree
```
(If multiple branches completed simultaneously, list them all: "You've fully mastered the {name1} and {name2} branches!")
**`AllKeysUnlocked`:**
- Title: `"Every Key Unlocked!"`
- Body:
```
You've unlocked every key on the keyboard!
All keys are now part of your practice drills.
Keep training to build full confidence with each
key!
```
**`AllKeysMastered`:**
- Title: `"Full Keyboard Mastery!"`
- Body:
```
Incredible! You've reached full confidence with
every single key on the keyboard!
You've completed everything keydr has to teach.
Keep practicing to maintain your skills!
```
### 4. Sequencing
Queue order in `finish_drill()`:
1. Key unlock popups (existing)
2. Key mastery popups (existing)
3. `BranchesAvailable` (if applicable)
4. `BranchComplete` (if applicable, excluding lowercase)
5. `AllKeysUnlocked` (if applicable)
6. `AllKeysMastered` (if applicable)
The input lock is armed once when `milestone_queue` is non-empty (existing logic). User dismisses each popup with any keypress.
### 5. Tests
**In `src/engine/skill_tree.rs` tests:**
- `branches_newly_available` non-empty on first `update()` after lowercase completion, empty on second call
- `branches_newly_completed` contains the branch ID when a non-lowercase branch completes
- `all_keys_unlocked` fires when the last key becomes available, not on subsequent calls
- `all_keys_mastered` fires when all branches reach Complete, not on subsequent calls
- `branches_newly_available` only contains the five non-lowercase branch IDs
**In `src/app.rs` tests:**
- Queue order test: last lowercase key mastered → queue contains unlock → mastery → BranchesAvailable (no BranchComplete for lowercase)
- Branch complete test: non-lowercase branch completes → BranchComplete queued
- Helper: `seed_near_complete_lowercase(app)` — 25 keys at confidence 1.0, last key at 0.95
## Files to Modify
1. `src/engine/skill_tree.rs` — Extend `SkillTreeUpdate`, detect transitions in `update()`
2. `src/app.rs` — Add variants to `MilestoneKind`, queue popups in `finish_drill()`
3. `src/main.rs` — Render the four new popup variants in `render_milestone_overlay()`
## Verification
1. `cargo build` — compiles cleanly
2. `cargo test` — all existing + new tests pass
3. Manual testing with test profiles for each milestone scenario

View File

@@ -0,0 +1,217 @@
# Plan: Internationalize UI Text
## Context
keydr supports 21 languages for dictionaries and keyboard layouts, but all UI text is hardcoded English (~200 strings across inline literals, `format!()` templates, `const` arrays, `Display` impls, and prebuilt state like milestone messages). This plan translates all app-owned UI copy via a separate "UI Language" config setting. Source texts from code/passage drills remain untranslated. Nested error details from system/library errors (e.g. IO errors, serde errors) embedded in status messages remain in their original form — only the app-owned wrapper text around them is translated.
This initial change ships **English + German only**. Remaining languages will follow in a separate commit.
## Design Decisions
**Library: `rust-i18n` v3**
- `t!("key")` / `t!("key", var = val)` macro API
- Translations in YAML, compiled into binary — no runtime file loading
**Separate UI language setting:** `ui_language` config field independent of `dictionary_language`. Defaults to `"en"`.
**Separate supported locale list:** UI locale validation uses `SUPPORTED_UI_LOCALES` (initially `["en", "de"]`), decoupled from the dictionary language pack system.
**Language names as autonyms everywhere:** All places that display a language name (selectors, settings summaries, status messages) use the language's autonym ("Deutsch", "Français") via a new `autonym` field on `LanguagePack`. No exonyms or locale-translated language names. Tradeoff: users may not recognize unfamiliar languages by autonym alone (e.g. "Suomi" for Finnish), but this is consistent and avoids translating language names per-locale. The existing English `display_name` field remains available as context.
**Stale text on locale switch:** Already-rendered `StatusMessage.text` and open `KeyMilestonePopup` messages stay in the old language until dismissed. Only newly produced text uses the new locale.
**Domain errors stay UI-agnostic:** `LanguageLayoutValidationError::Display` keeps its current English implementation. Translation happens at the UI boundary via a helper function in the i18n module.
**Canonical import:** All files use `use crate::i18n::t;` as the single import style for the translation macro.
## Text Source Categories
| Category | Example Location | Strategy |
|----------|-----------------|----------|
| **Inline literals** | Render functions in `main.rs`, UI components | Replace with `t!()` |
| **`const` arrays** | `UNLOCK_MESSAGES`, `MASTERY_MESSAGES`, `TAB_LABELS`, `FOOTER_HINTS_*` | Convert to functions returning `Vec<String>` or build inline |
| **`format!()` templates** | `StatusMessage` construction in `app.rs`/`main.rs` | Replace template with `t!("key", var = val)` |
| **`Display` impls** | `LanguageLayoutValidationError` | Keep `Display` stable; translate at UI boundary in i18n module |
| **Domain display names** | `LanguagePack.display_name` | Add `autonym` field; code language names stay English ("Rust", "Python") |
| **Cached `'static` fields** | `KeyMilestonePopup.message: &'static str` | Change to `String` |
## Implementation Steps
### Step 1: Centralized i18n module and dependency setup
Add `rust-i18n = "3"` to `[dependencies]` and `serde_yaml = "0.9"` to `[dev-dependencies]` in `Cargo.toml`.
Create `src/i18n.rs`:
```rust
pub use rust_i18n::t;
rust_i18n::i18n!("locales", fallback = "en");
/// Available UI locale codes. Separate from dictionary language support.
pub const SUPPORTED_UI_LOCALES: &[&str] = &["en", "de"];
pub fn set_ui_locale(locale: &str) {
let effective = if SUPPORTED_UI_LOCALES.contains(&locale) { locale } else { "en" };
rust_i18n::set_locale(effective);
}
/// Translate a LanguageLayoutValidationError for display in the UI.
pub fn localized_language_layout_error(err: &crate::l10n::language_pack::LanguageLayoutValidationError) -> String {
use crate::l10n::language_pack::LanguageLayoutValidationError::*;
match err {
UnknownLanguage(key) => t!("errors.unknown_language", key = key),
UnknownLayout(key) => t!("errors.unknown_layout", key = key),
UnsupportedLanguageLayoutPair { language_key, layout_key } =>
t!("errors.unsupported_pair", language = language_key, layout = layout_key),
LanguageBlockedBySupportLevel(key) =>
t!("errors.language_blocked", key = key),
}
}
```
**Crate root ownership — which targets compile translated modules:**
| Target | Declares `mod i18n` | Compiles modules that call `t!()` | Must call `set_ui_locale()` |
|--------|---------------------|-----------------------------------|----------------------------|
| `src/main.rs` (binary) | Yes | Yes (`app.rs`, UI components, `main.rs` itself) | Yes, at startup |
| `src/lib.rs` (library) | Yes | Yes (`app.rs` is in the lib module tree) | No — lib is for benchmarks/test profiles; locale defaults to English via `fallback = "en"` |
| `src/bin/generate_test_profiles.rs` | No | No — imports from `keydr::` lib but only uses data types, not UI/translated code | No |
**Invariant:** Any module that calls `t!()` must be in a crate whose root declares `mod i18n;` with the `i18n!()` macro. If a future change adds `t!()` calls to a module reachable from `generate_test_profiles`, that binary must also add `mod i18n;`. The `fallback = "en"` default ensures English output when `set_ui_locale()` is never called.
Create `locales/en.yml` with initial structure, verify `cargo build`.
### Step 2: Add `ui_language` config field
**`src/config.rs`:**
- Add `ui_language: String` with `#[serde(default = "default_ui_language")]`, default `"en"`
- Add `normalize_ui_language()` — validates against `i18n::SUPPORTED_UI_LOCALES`, resets to `"en"` if unsupported
- Add to `Default` impl and `validate()`
**`src/app.rs`:**
- Add `UiLanguageSelect` variant to `AppScreen`
- Change `KeyMilestonePopup.message` from `&'static str` to `String`
**`src/main.rs`:**
- Call `i18n::set_ui_locale(&app.config.ui_language)` after `App::new()`
- Add "UI Language" setting item in settings menu (before "Dictionary Language")
- Add `UiLanguageSelect` screen reusing language selection list pattern (filtered to `SUPPORTED_UI_LOCALES`)
- On selection: update `config.ui_language`, call `i18n::set_ui_locale()`
- After data import: call `i18n::set_ui_locale()` again
### Step 3: Add autonym field to LanguagePack
**`src/l10n/language_pack.rs`:**
- Add `autonym: &'static str` field to `LanguagePack`
- Populate for all 21 languages: "English", "Deutsch", "Español", "Français", "Italiano", "Português", "Nederlands", "Svenska", "Dansk", "Norsk bokmål", "Suomi", "Polski", "Čeština", "Română", "Hrvatski", "Magyar", "Lietuvių", "Latviešu", "Slovenščina", "Eesti", "Türkçe"
**Update all language name display sites in `main.rs`:**
- Dictionary language selector: show `pack.autonym` instead of `pack.display_name`
- UI language selector: show `pack.autonym`
- Settings value display for dictionary language: show `pack.autonym`
- Status messages mentioning languages (e.g. "Switched to {}"): use `pack.autonym`
### Step 4: Create English base translation file (`locales/en.yml`)
Populate all ~200 keys organized by component:
```yaml
en:
menu: # menu items, descriptions, subtitle
drill: # mode headers, footers, focus labels
dashboard: # results screen labels, hints
sidebar: # stats sidebar labels
settings: # setting names, toggle values, buttons
status: # import/export/error messages (format templates)
skill_tree: # status labels, hints, notices
milestones: # unlock/mastery messages, congratulations
stats: # tab names, chart titles, hints, empty states
heatmap: # month/day abbreviations, title
keyboard: # explorer labels, detail fields
intro: # passage/code download setup dialogs
dialogs: # confirmation dialogs
errors: # validation error messages (for UI boundary translation)
common: # WPM, CPM, Back, etc.
```
### Step 5: Convert source files to use `t!()` — vertical slice first
**Phase A — Vertical slice (one file per text category to establish patterns):**
1. `src/ui/components/menu.rs` — inline literals (9 strings)
2. `src/ui/components/stats_dashboard.rs` — inline literals + `const` arrays → functions
3. `src/app.rs``StatusMessage` format templates (~20 strings), `UNLOCK_MESSAGES`/`MASTERY_MESSAGES` → functions
4. Update `StatusMessage` creation sites in `main.rs` that reference `LanguageLayoutValidationError` to use `i18n::localized_language_layout_error()` instead of `err.to_string()`
**Phase B — Remaining components:**
5. `src/ui/components/chart.rs` (3 strings)
6. `src/ui/components/activity_heatmap.rs` (14 strings)
7. `src/ui/components/stats_sidebar.rs` (10 strings)
8. `src/ui/components/dashboard.rs` (12 strings)
9. `src/ui/components/skill_tree.rs` (15 strings)
**Phase C — main.rs (largest):**
10. `src/main.rs` (~120+ strings) — settings menu, drill rendering, milestone overlay rendering, keyboard explorer, intro dialogs, footer hints, status messages
**Key patterns:**
- `use crate::i18n::t;` in every file that needs translation
- `t!()` returns `String`; for `&str` contexts: `let label = t!("key"); &label`
- Footer hints like `"[ESC] Back"` — full string in YAML, translators preserve bracket keys: `"[ESC] Zurück"`
- `const` arrays → functions: e.g. `fn unlock_messages() -> Vec<String>`
- `StatusMessage.text` built via `t!()` at creation time
### Step 6: Create German translation file (`locales/de.yml`)
AI-generated translation of all keys from `en.yml`:
- Keep `%{var}` placeholders unchanged
- Keep key names inside `[brackets]` unchanged (`[ESC]`, `[Enter]`, `[Tab]`, etc.)
- Keep technical terms WPM/CPM untranslated
- Be concise — German text tends to run ~20-30% longer; keep terminal width in mind
### Step 7: Tests and validation
- Add `rust_i18n::set_locale("en")` in test setup where tests assert against English output
- Add a test that sets locale to `"de"` and verifies a rendered component uses German text
- Add a test that switches locale mid-run and verifies new `StatusMessage` text uses the new locale
- **Add a catalog parity test** (using `serde_yaml` dev-dependency): parse both `locales/en.yml` and `locales/de.yml` as `serde_yaml::Value`, recursively walk the key trees, verify every key in `en.yml` exists in `de.yml` and vice versa, and that `%{var}` placeholders in each value string match between corresponding entries
- Run `cargo test` and `cargo build`
## Files Modified
| File | Scope |
|------|-------|
| `Cargo.toml` | Add `rust-i18n = "3"`, `serde_yaml = "0.9"` (dev) |
| `src/config.rs` | Add `ui_language` field, default, validation |
| `src/lib.rs` | Add `mod i18n;` |
| `src/main.rs` | Add `mod i18n;`, `set_ui_locale()` calls, UI Language setting/select screen, ~120 string replacements, use `localized_language_layout_error()` |
| `src/app.rs` | Add `UiLanguageSelect` to `AppScreen`, `KeyMilestonePopup.message``String`, ~20 StatusMessage string replacements, convert milestone constants to functions |
| `src/l10n/language_pack.rs` | Add `autonym` field to `LanguagePack` |
| `src/ui/components/menu.rs` | 9 string replacements |
| `src/ui/components/dashboard.rs` | 12 string replacements |
| `src/ui/components/stats_dashboard.rs` | 25 string replacements, refactor `const` arrays to functions |
| `src/ui/components/skill_tree.rs` | 15 string replacements |
| `src/ui/components/stats_sidebar.rs` | 10 string replacements |
| `src/ui/components/activity_heatmap.rs` | 14 string replacements |
| `src/ui/components/chart.rs` | 3 string replacements |
## Files Created
| File | Content |
|------|---------|
| `src/i18n.rs` | Centralized i18n bootstrap, `SUPPORTED_UI_LOCALES`, `set_ui_locale()`, `localized_language_layout_error()` |
| `locales/en.yml` | English base translations (~200 keys) |
| `locales/de.yml` | German translations |
## Verification
1. `cargo build` — rust-i18n checks referenced keys at compile time (not a complete catalog correctness guarantee; parity test and manual checks cover the rest)
2. `cargo test` — including catalog parity test + locale-specific tests
3. Manual testing with UI set to English: navigate all screens, verify identical behavior to pre-i18n
4. Manual testing with UI set to German: navigate all screens, verify German text
5. Verify drill source text (passage/code content) is NOT translated
6. Verify language selectors show autonyms ("Deutsch", not "German")
7. Test locale switch: change UI language in settings, verify new text appears in new language, existing status banner stays in old language
8. Check for layout/truncation issues with German text

View File

@@ -0,0 +1,117 @@
# Plan: Fix Remaining Untranslated UI Strings
## Context
The i18n system is implemented but several categories of strings were missed:
1. Menu item labels/descriptions are cached as `String` at construction and never refreshed when locale changes
2. Skill tree branch names and level names are hardcoded `&'static str` in `BranchDefinition`/`LevelDefinition`
3. Passage selector labels ("All (Built-in + all books)", "Built-in passages only", "Book: ...") are hardcoded
4. Branch progress list (`branch_progress_list.rs`) renders branch names and "Overall Key Progress" / "unlocked" / "mastered" in English
## Fix 1: Menu Items — Translate at Render Time
**Problem:** `Menu::new()` calls `t!()` once during `App::new()`. Even though `set_ui_locale()` runs after construction, the items are cached as `String` and never refreshed when the user changes UI language mid-session.
**Fix:** Define a shared static item list (keys + translation keys) and build rendered strings from it in both `Widget::render()` and navigation code.
**Files:** `src/ui/components/menu.rs`
- Define a `const MENU_ITEMS` array of `(&str, &str, &str)` tuples: `(shortcut_key, label_i18n_key, desc_i18n_key)`. This is the single authoritative definition.
- Remove `MenuItem` struct and the `items: Vec<MenuItem>` field.
- Keep `selected: usize` and `theme` fields. `next()`/`prev()` use `MENU_ITEMS.len()`.
- Add a `Menu::item_count() -> usize` helper returning `MENU_ITEMS.len()`.
- In `Widget::render()`, iterate `MENU_ITEMS` and call `t!()` for label/description each frame.
- Replace `app.menu.items.len()` in `src/main.rs` mouse handler (~line 660) with `Menu::item_count()`.
## Fix 2: Skill Tree Branch and Level Names — Replace `name` with `name_key`
**Problem:** `BranchDefinition.name` and `LevelDefinition.name` are `&'static str` with English text. They are used purely for UI display (confirmed: no serialization, logging, or export uses).
**Fix:** Replace `name` with `name_key` on both structs. The `name_key` holds a translation key (e.g. `"skill_tree.branch_primary_letters"`). All display sites use `t!(def.name_key)`.
Add `BranchDefinition::display_name()` and `LevelDefinition::display_name()` convenience methods that return `t!(self.name_key)` so call sites stay simple.
Change `find_key_branch()` to return `(&'static BranchDefinition, &'static LevelDefinition, usize)` instead of `(&'static BranchDefinition, &'static str, usize)`. This gives callers access to the `LevelDefinition` and its `name_key` so they can localize the level name themselves.
**Complete consumer inventory:**
| File | Lines | Usage |
|------|-------|-------|
| `src/ui/components/skill_tree.rs` | ~366 | Branch name in branch list header |
| `src/ui/components/skill_tree.rs` | ~445 | Branch name in detail header |
| `src/ui/components/skill_tree.rs` | ~483 | Level name in detail level list |
| `src/ui/components/branch_progress_list.rs` | ~95 | Branch name in single-branch drill sidebar |
| `src/ui/components/branch_progress_list.rs` | ~188 | Branch name in multi-branch progress cells |
| `src/main.rs` | ~3931 | Branch name in "branches available" milestone |
| `src/main.rs` | ~3961 | Branch names in "branch complete" milestone text |
| `src/main.rs` | ~6993 | Branch name in unlock confirmation dialog |
| `src/main.rs` | ~7327 | Branch name + level name in keyboard detail panel (via `find_key_branch()`) |
**Files:**
- `src/engine/skill_tree.rs` — Replace `name` with `name_key` on both structs; add `display_name()` methods; change `find_key_branch()` return type; populate `name_key` for all entries
- `src/ui/components/skill_tree.rs` — Use `def.display_name()` / `level.display_name()` at 3 sites
- `src/ui/components/branch_progress_list.rs` — Use `def.display_name()` at 2 sites; also translate "Overall Key Progress", "unlocked", "mastered"
- `src/main.rs` — Use `def.display_name()` at 4 sites; update `find_key_branch()` call site to use `level.display_name()`
- `locales/en.yml` — Add branch/level name keys under `skill_tree:`
- `locales/de.yml` — Add German translations
Note on truncation: `branch_progress_list.rs` uses fixed-width formatting (`{:<14}`, truncation widths 10/12/14). German branch names that exceed these widths will be truncated. This is acceptable for now — the widget already handles this via `truncate_and_pad()`. Proper dynamic-width layout is a separate concern.
Translation keys to add:
```yaml
skill_tree:
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'
```
Also add to `progress` section (translation values contain only text, no alignment whitespace — padding is applied in rendering code):
```yaml
progress:
overall_key_progress: 'Overall Key Progress'
unlocked_mastered: '%{unlocked}/%{total} unlocked (%{mastered} mastered)'
```
## Fix 3: Passage Book Selector Labels
**Problem:** `passage_options()` returns hardcoded `"All (Built-in + all books)"`, `"Built-in passages only"`, and `"Book: {title}"`.
**Fix:** Add `t!()` calls in `passage_options()`. Book titles (proper nouns like "Pride and Prejudice") stay untranslated per plan.
**Files:**
- `src/generator/passage.rs` — Add `use crate::i18n::t;`, convert the two label strings and the "Book:" prefix
- `locales/en.yml` — Add keys under `select:`:
```yaml
select:
passage_all: 'All (Built-in + all books)'
passage_builtin: 'Built-in passages only'
passage_book_prefix: 'Book: %{title}'
```
- `locales/de.yml` — German translations
## Verification
1. `cargo check` — must compile
2. `cargo test --lib i18n::tests` — catalog parity and placeholder parity tests catch missing keys
3. `cargo test --lib` — no new test failures
4. Add tests for the new translated surfaces. To avoid parallel-test races on global locale state, new tests use `t!("key", locale = "de")` directly on the translation keys rather than calling ambient-locale helpers like `display_name()` or `passage_options()`. This keeps tests deterministic without needing serial execution or locale-parameterized API variants.
- Test that `t!("skill_tree.branch_primary_letters", locale = "de")` returns the expected German text
- Test that `t!("select.passage_all", locale = "de")` returns the expected German text

454
locales/cs.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminalovy trenazer psani'
adaptive_drill: 'Adaptivni cviceni'
adaptive_drill_desc: 'Foneticka slova s adaptivnim odemykanim pismen'
code_drill: 'Cviceni kodu'
code_drill_desc: 'Procvicuj psani syntaxe kodu'
passage_drill: 'Cviceni textu'
passage_drill_desc: 'Opisuj pasaze z knih'
skill_tree: 'Strom dovednosti'
skill_tree_desc: 'Zobraz vetev postupu a spust cviceni'
keyboard: 'Klavesnice'
keyboard_desc: 'Prozkoumej rozlozeni klaves a statistiky'
statistics: 'Statistiky'
statistics_desc: 'Zobraz sve statistiky psani'
settings: 'Nastaveni'
settings_desc: 'Konfiguruj keydr'
day_streak: ' | %{days} dni v rade'
key_progress: ' Postup klaves %{unlocked}/%{total} (%{mastered} zvladnutych) | Cil %{target} WPM%{streak}'
hint_start: '[1-3] Spustit'
hint_skill_tree: '[t] Strom'
hint_keyboard: '[b] Klavesnice'
hint_stats: '[s] Statistiky'
hint_settings: '[c] Nastaveni'
hint_quit: '[q] Konec'
# Drill screen
drill:
title: ' Cviceni '
mode_adaptive: 'Adaptivni'
mode_code: 'Kod (bez hodnoceni)'
mode_passage: 'Text (bez hodnoceni)'
focus_char: 'Zamereni: ''%{ch}'''
focus_bigram: 'Zamereni: "%{bigram}"'
focus_both: 'Zamereni: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Presn'
header_err: 'Chyb'
code_source: ' Zdroj kodu '
passage_source: ' Zdroj textu '
footer: '[ESC] Ukoncit cviceni [Backspace] Smazat'
keys_reenabled: 'Klavesy obnoveny za %{ms}ms'
hint_end: '[ESC] Ukoncit cviceni'
hint_backspace: '[Backspace] Smazat'
# Dashboard / drill result
dashboard:
title: ' Cviceni dokonceno '
results: 'Vysledky'
unranked_note_prefix: ' (Bez hodnoceni'
unranked_note_suffix: ' nepocita se do stromu dovednosti)'
speed: ' Rychlost: '
accuracy_label: ' Presnost: '
time_label: ' Cas: '
errors_label: ' Chyby: '
correct_detail: ' (%{correct}/%{total} spravne)'
input_blocked: ' Vstup docasne zablokovany '
input_blocked_ms: '(%{ms}ms zbyva)'
hint_continue: '[c/Enter/Space] Pokracovat'
hint_retry: '[r] Znovu'
hint_menu: '[q] Menu'
hint_stats: '[s] Statistiky'
hint_delete: '[x] Smazat'
# Stats sidebar (during drill)
sidebar:
title: ' Statistiky '
wpm: 'WPM: '
target: 'Cil: '
target_wpm: '%{wpm} WPM'
accuracy: 'Presnost: '
progress: 'Postup: '
correct: 'Spravne: '
errors: 'Chyby: '
time: 'Cas: '
last_drill: ' Posledni cviceni '
vs_avg: ' vs prum: '
# Statistics dashboard
stats:
title: ' Statistiky '
empty: 'Zadna cviceni dosud. Zacni psat!'
tab_dashboard: '[1] Prehled'
tab_history: '[2] Historie'
tab_activity: '[3] Aktivita'
tab_accuracy: '[4] Presnost'
tab_timing: '[5] Casovani'
tab_ngrams: '[6] N-gramy'
hint_back: '[ESC] Zpet'
hint_next_tab: '[Tab] Dalsi karta'
hint_switch_tab: '[1-6] Prepnout kartu'
hint_navigate: '[j/k] Navigovat'
hint_page: '[PgUp/PgDn] Stranka'
hint_delete: '[x] Smazat'
summary_title: ' Souhrn '
drills: ' Cviceni: '
avg_wpm: ' Prum WPM: '
best_wpm: ' Nejlepsi WPM: '
accuracy_label: ' Presnost: '
total_time: ' Celkovy cas: '
wpm_chart_title: ' WPM na cviceni (poslednich 20, cil: %{target}) '
accuracy_chart_title: ' Presnost %% (poslednich 50 cviceni) '
chart_drill: 'Cvic #'
chart_accuracy_pct: 'Presnost %%'
sessions_title: ' Posledni relace '
session_header: ' # WPM Raw Presn%% Cas Datum/Cas Rezim Hodnocen Castecny'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Smazat relaci #%{idx}? (a/n)'
confirm_title: ' Potvrzeni '
yes: 'ano'
no: 'ne'
keyboard_accuracy_title: ' Presnost klavesnice %% '
keyboard_timing_title: ' Casovani klavesnice (ms) '
slowest_keys_title: ' Nejpomalejsi klavesy (ms) '
fastest_keys_title: ' Nejrychlejsi klavesy (ms) '
worst_accuracy_title: ' Nejhorsi presnost (%%) '
best_accuracy_title: ' Nejlepsi presnost (%%) '
not_enough_data: ' Nedostatek dat'
streaks_title: ' Serie '
current_streak: ' Aktualni: '
best_streak: ' Nejlepsi: '
active_days: ' Aktivni dny: '
top_days_none: ' Nejlepsi dny: zadne'
top_days: ' Nejlepsi dny: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Presn: %{pct}%%'
keys_label: ' Klavesy: %{unlocked}/%{total} (%{mastered} zvladnutych)'
ngram_empty: 'Dokonci adaptivni cviceni pro zobrazeni dat n-gramu'
ngram_header_speed_narrow: ' Bgrm Rychl Ocek Anom%'
ngram_header_error_narrow: ' Bgrm Chyb Vzrk Mira Ocek Anom%'
ngram_header_speed: ' Bigram Rychlost Ocekav Vzorky Anom%'
ngram_header_error: ' Bigram Chyby Vzorky Mira Ocekav Anom%'
focus_title: ' Aktivni zamereni '
focus_char_label: ' Zamereni: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'chyba'
anomaly_speed: 'rychlost'
focus_detail_both: ' Znak ''%{ch}'': nejslabsi klavesa | Bigram %{label}: anomalie %{type} %{pct}%%'
focus_detail_char_only: ' Znak ''%{ch}'': nejslabsi klavesa, zadne potvrzene anomalie bigramu'
focus_detail_bigram_only: ' (anomalie %{type}: %{pct}%%)'
focus_empty: ' Dokonci adaptivni cviceni pro zobrazeni dat zamereni'
error_anomalies_title: ' Anomalie chyb (%{count}) '
no_error_anomalies: ' Nebyly detekovany anomalie chyb'
speed_anomalies_title: ' Anomalie rychlosti (%{count}) '
no_speed_anomalies: ' Nebyly detekovany anomalie rychlosti'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Vah: >%{ms}ms'
focus_char_value: 'Znak ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Denni aktivita (relace za den) '
jan: 'Led'
feb: 'Uno'
mar: 'Bre'
apr: 'Dub'
may: 'Kve'
jun: 'Cer'
jul: 'Cvc'
aug: 'Srp'
sep: 'Zar'
oct: 'Rij'
nov: 'Lis'
dec: 'Pro'
# Chart
chart:
wpm_over_time: ' WPM v case '
drill_number: 'Cvic #'
# Settings
settings:
title: ' Nastaveni '
subtitle: 'Sipkami naviguj, Enter/vpravo zmeni, ESC ulozi a zavre'
target_wpm: 'Cilovy WPM'
theme: 'Motiv'
word_count: 'Pocet slov'
ui_language: 'Jazyk rozhrani'
dictionary_language: 'Jazyk slovniku'
keyboard_layout: 'Rozlozeni klaves'
code_language: 'Programovaci jazyk'
code_downloads: 'Stahovani kodu'
on: 'Zapnuto'
off: 'Vypnuto'
code_download_dir: 'Adresar stahovani kodu'
snippets_per_repo: 'Fragmenty na repo'
unlimited: 'Bez limitu'
download_code_now: 'Stahnout kod nyni'
run_downloader: 'Spustit stahovani'
passage_downloads: 'Stahovani textu'
passage_download_dir: 'Adresar stahovani textu'
paragraphs_per_book: 'Odstavce na knihu'
whole_book: 'Cela kniha'
download_passages_now: 'Stahnout texty nyni'
export_path: 'Cesta exportu'
export_data: 'Exportovat data'
export_now: 'Exportovat nyni'
import_path: 'Cesta importu'
import_data: 'Importovat data'
import_now: 'Importovat nyni'
hint_save_back: '[ESC] Ulozit a zpet'
hint_change_value: '[Enter/sipky] Zmenit hodnotu'
hint_edit_path: '[Enter na ceste] Upravit'
hint_move: '[←→] Posunout'
hint_tab_complete: '[Tab] Doplnit (na konci)'
hint_confirm: '[Enter] Potvrdit'
hint_cancel: '[Esc] Zrusit'
success_title: ' Uspech '
error_title: ' Chyba '
press_any_key: 'Stiskni libovolnou klavesu'
file_exists_title: ' Soubor existuje '
file_exists: 'Na teto ceste jiz soubor existuje.'
overwrite_rename: '[d] Prepsat [r] Prejmenovat [Esc] Zrusit'
erase_warning: 'Toto smaze vase aktualni data.'
export_first: 'Nejprve exportujte, pokud je chcete zachovat.'
proceed_yn: 'Pokracovat? (a/n)'
confirm_import_title: ' Potvrdit import '
# Selection screens
select:
dictionary_language_title: ' Vybrat jazyk slovniku '
keyboard_layout_title: ' Vybrat rozlozeni klaves '
code_language_title: ' Vybrat programovaci jazyk '
passage_source_title: ' Vybrat zdroj textu '
ui_language_title: ' Vybrat jazyk rozhrani '
more_above: '... %{count} vice vyse ...'
more_below: '... %{count} vice nize ...'
current: ' (aktualni)'
disabled: ' (vypnuto)'
enabled_default: ' (zapnuto, vychozi: %{layout})'
enabled: ' (zapnuto)'
disabled_blocked: ' (vypnuto: zablokovano)'
built_in: ' (vestaveny)'
cached: ' (v pameti)'
disabled_download: ' (vypnuto: nutne stahnout)'
download_required: ' (nutne stahnout)'
hint_navigate: '[Nahoru/Dolu/PgUp/PgDn] Navigovat'
hint_confirm: '[Enter] Potvrdit'
hint_back: '[ESC] Zpet'
language_resets_layout: 'Vyber jazyka obnovi rozlozeni klaves na vychozi pro dany jazyk.'
layout_no_language_change: 'Zmena rozlozeni nemeni jazyk slovniku.'
disabled_network_notice: 'Nektere jazyky jsou vypnute: povolte sitove stahovani v nastaveni.'
disabled_sources_notice: 'Nektere zdroje jsou vypnute: povolte sitove stahovani v nastaveni.'
passage_all: 'Vse (vestavene + vsechny knihy)'
passage_builtin: 'Pouze vestavene texty'
passage_book_prefix: 'Kniha: %{title}'
# Progress
progress:
overall_key_progress: 'Celkovy postup klaves'
unlocked_mastered: '%{unlocked}/%{total} odemcenych (%{mastered} zvladnutych)'
# Skill tree
skill_tree:
title: ' Strom dovednosti '
locked: 'Zamceny'
unlocked: 'odemceny'
mastered: 'zvladnuty'
in_progress: 'probihajici'
complete: 'dokonceny'
locked_status: 'zamceny'
locked_notice: 'Dokonci %{count} zakladnich pismen pro odemknuti vetvi'
branches_separator: 'Vetve (dostupne po %{count} zakladnich pismenech)'
unlocked_letters: 'Odemceno %{unlocked}/%{total} pismen'
level: 'Uroven %{current}/%{total}'
level_zero: 'Uroven 0/%{total}'
in_focus: ' v zamereni'
hint_navigate: '[↑↓/jk] Navigovat'
hint_scroll: '[PgUp/PgDn nebo Ctrl+U/Ctrl+D] Rolovat'
hint_back: '[q] Zpet'
hint_unlock: '[Enter] Odemknout'
hint_start_drill: '[Enter] Spustit cviceni'
unlock_msg_1: 'Po odemknuti bude vychozi adaptivni cviceni zahrovat klavesy z teto vetve.'
unlock_msg_2: 'Pokud se chces zamerit jen na tuto vetev, spust cviceni primo ze stromu.'
confirm_unlock: 'Odemknout %{branch}?'
confirm_yn: '[y] Odemknout [n/ESC] Zrusit'
lvl_prefix: 'Ur'
branch_primary_letters: 'Zakladni pismena'
branch_capital_letters: 'Velka pismena'
branch_numbers: 'Cislice 0-9'
branch_prose_punctuation: 'Interpunkce'
branch_whitespace: 'Bile znaky'
branch_code_symbols: 'Symboly kodu'
level_frequency_order: 'Poradi cetnosti'
level_common_sentence_capitals: 'Bezna velka pismena vet'
level_name_capitals: 'Velka pismena jmen'
level_remaining_capitals: 'Zbyvajici velka pismena'
level_common_digits: 'Bezne cislice'
level_all_digits: 'Vsechny cislice'
level_essential: 'Zakladni'
level_common: 'Bezne'
level_expressive: 'Expresivni'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/odsazeni'
level_arithmetic_assignment: 'Aritmetika a prirazeni'
level_grouping: 'Seskupovani'
level_logic_reference: 'Logika a reference'
level_special: 'Specialni'
# Milestones
milestones:
unlock_title: ' Klavesa odemcena! '
mastery_title: ' Klavesa zvladnuta! '
branches_title: ' Nove vetve dostupne! '
branch_complete_title: ' Vetev dokoncena! '
all_unlocked_title: ' Vsechny klavesy odemceny! '
all_mastered_title: ' Uplne zvladnuti klavesnice! '
unlocked: 'odemcena'
mastered: 'zvladnuta'
use_finger: 'Pouzij %{finger}'
hold_right_shift: 'Drz pravy Shift (pravy malicek)'
hold_left_shift: 'Drz levy Shift (levy malicek)'
congratulations_all_letters: 'Gratulujeme! Zvladl jsi vsech %{count} zakladnich pismen'
new_branches_available: 'Nove vetve dovednosti jsou nyni dostupne:'
visit_skill_tree: 'Navstiv strom dovednosti pro odemknuti nove vetve'
and_start_training: 'a zacni trenovat!'
open_skill_tree: 'Stiskni [t] pro otevreni stromu dovednosti'
branch_complete_msg: 'Dokoncil jsi vetev %{branch}!'
all_levels_mastered: 'Vsech %{count} urovni zvladnuto.'
all_keys_confident: 'Kazda klavesa v teto vetvi je na plne jistote.'
all_unlocked_msg: 'Odemkl jsi kazdou klavesu na klavesnici!'
all_unlocked_desc: 'Kazdy znak, symbol a modifikator je nyni dostupny ve tvych cvicenich.'
keep_practicing_mastery: 'Pokracuj v cviceni pro budovani zbehlosti — az kazda klavesa dosahne plne'
confidence_complete: 'jistoty, dosahnes uplneho zvladnuti klavesnice!'
all_mastered_msg: 'Gratulujeme — dosahl jsi uplneho zvladnuti klavesnice!'
all_mastered_desc: 'Kazda klavesa na klavesnici je na maximalni jistote.'
mastery_takes_practice: 'Zbehlost neni cil — vyzaduje prubezne cviceni.'
keep_drilling: 'Pokracuj v cviceni pro udrzeni sve urovne.'
hint_skill_tree_continue: '[t] Otevrit strom [Jina klavesa] Pokracovat'
hint_any_key: 'Stiskni libovolnou klavesu pro pokracovani'
input_blocked: 'Vstup docasne zablokovany (%{ms}ms zbyva)'
unlock_msg_1: 'Skvela prace! Pokracuj v rozvoji svych dovednosti.'
unlock_msg_2: 'Dalsi klavesa ve tvem arsenalu!'
unlock_msg_3: 'Tvoje klavesnice roste! Tak drzet.'
unlock_msg_4: 'O krok bliz k uplnemu zvladnuti klavesnice!'
mastery_msg_1: 'Tato klavesa je nyni na plne jistote!'
mastery_msg_2: 'Tuto klavesu mas v malicku!'
mastery_msg_3: 'Svalova pamet uzamcena!'
mastery_msg_4: 'Dalsi klavesa pokorena!'
# Keyboard explorer
keyboard:
title: ' Klavesnice '
subtitle: 'Stiskni libovolnou klavesu nebo klikni na klavesu'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigovat'
hint_back: '[q/ESC] Zpet'
key_label: 'Klavesa: '
finger_label: 'Prst: '
hand_left: 'Levy'
hand_right: 'Pravy'
finger_index: 'Ukazovacek'
finger_middle: 'Prostrednicek'
finger_ring: 'Prstenik'
finger_pinky: 'Malicek'
finger_thumb: 'Palec'
overall_accuracy: ' Celkova presnost: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Hodnocena presnost: %{correct}/%{total} (%{pct}%%)'
confidence: 'Jistota: '
no_data: 'Zatim zadna data'
no_data_short: 'Zadna data'
key_details: ' Detaily klavesy '
key_details_char: ' Detaily klavesy: ''%{ch}'' '
key_details_name: ' Detaily klavesy: %{name} '
press_key_hint: 'Stiskni klavesu pro zobrazeni detailu'
shift_label: 'Shift: '
shift_no: 'Ne'
overall_avg_time: 'Prumerny cas: '
overall_best_time: 'Nejlepsi cas: '
overall_samples: 'Vzorky: '
overall_accuracy_label: 'Celkova presnost: '
branch_label: 'Vetev: '
level_label: 'Uroven: '
built_in_key: 'Vestavena klavesa'
unlocked_label: 'Odemcena: '
yes: 'Ano'
no: 'Ne'
in_focus_label: 'V zamereni?: '
mastery_label: 'Zbehlost: '
mastery_locked: 'Zamcena'
ranked_avg_time: 'Hodnoceny prum cas: '
ranked_best_time: 'Hodnoceny nejl cas: '
ranked_samples: 'Hodnocene vzorky: '
ranked_accuracy_label: 'Hodnocena presnost: '
# Intro dialogs
intro:
passage_title: ' Nastaveni stahovani textu '
code_title: ' Nastaveni stahovani kodu '
enable_downloads: 'Povolit sitove stahovani'
download_dir: 'Adresar stahovani'
paragraphs_per_book: 'Odstavce na knihu (0 = cela)'
whole_book: 'cela kniha'
snippets_per_repo: 'Fragmenty na repo (0 = bez limitu)'
unlimited: 'bez limitu'
start_passage_drill: 'Spustit cviceni textu'
start_code_drill: 'Spustit cviceni kodu'
confirm: 'Potvrdit'
hint_navigate: '[Nahoru/Dolu] Navigovat'
hint_adjust: '[Vlevo/Vpravo] Upravit'
hint_edit: '[Pis/Backspace] Editovat'
hint_confirm: '[Enter] Potvrdit'
hint_cancel: '[ESC] Zrusit'
preparing_download: 'Pripravuji stahovani...'
download_passage_title: ' Stahuji zdroj textu '
download_code_title: ' Stahuji zdroj kodu '
book_label: ' Kniha: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bajtu'
downloaded_bytes: 'Stazeno: %{bytes} bajtu'
downloading_book_progress: 'Stahuji knihu: [%{bar}] %{downloaded}/%{total} bajtu'
downloading_book_bytes: 'Stahuji knihu: %{bytes} bajtu'
downloading_code_progress: 'Stahuji: [%{bar}] %{downloaded}/%{total} bajtu'
downloading_code_bytes: 'Stahuji: %{bytes} bajtu'
current_book: 'Aktualni: %{name} (kniha %{done}/%{total})'
current_repo: 'Aktualni: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr muze stahovat texty z Project Gutenberg pro cviceni psani.'
passage_instructions_2: 'Knihy se stahuji jednou a ukladaji lokalne.'
passage_instructions_3: 'Nastav moznosti stahovani nize a pak spust cviceni textu.'
code_instructions_1: 'keydr muze stahovat open-source kod z GitHubu pro cviceni psani.'
code_instructions_2: 'Kod se stahuje jednou a uklada lokalne.'
code_instructions_3: 'Nastav moznosti stahovani nize a pak spust cviceni kodu.'
# Status messages (from app.rs)
status:
recovery_files: 'Nalezeny obnovovaci soubory z preruseneho importu. Data mohou byt nekonzistentni — zvaz opetovny import.'
dir_not_exist: 'Adresar neexistuje: %{path}'
no_data_store: 'Datove uloziste neni k dispozici'
serialization_error: 'Chyba serializace: %{error}'
exported_to: 'Exportovano do %{path}'
export_failed: 'Export selhal: %{error}'
could_not_read: 'Nelze precist soubor: %{error}'
invalid_export: 'Neplatny exportni soubor: %{error}'
unsupported_version: 'Nepodporovana verze exportu: %{got} (ocekavana %{expected})'
import_failed: 'Import selhal: %{error}'
imported_theme_fallback: 'Importovano uspesne (motiv ''%{theme}'' nenalezen, pouzivam vychozi)'
imported_success: 'Importovano uspesne'
adaptive_unavailable: 'Adaptivni hodnoceny rezim nedostupny: %{error}'
switched_to: 'Prepnuto na %{name}'
layout_changed: 'Rozlozeni zmeneno na %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Neznamy jazyk: %{key}'
unknown_layout: 'Nezname rozlozeni klaves: %{key}'
unsupported_pair: 'Nepodporovany par jazyk/rozlozeni: %{language} + %{layout}'
language_blocked: 'Jazyk blokovan urovni podpory: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Zpet'

454
locales/da.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminal Skrivetraener'
adaptive_drill: 'Adaptiv oevelse'
adaptive_drill_desc: 'Fonetiske ord med adaptiv bogstavoplasning'
code_drill: 'Kodeoevelse'
code_drill_desc: 'Oev kodersyntaks'
passage_drill: 'Tekstoevelse'
passage_drill_desc: 'Skriv passager fra boeger'
skill_tree: 'Faerdighedstrae'
skill_tree_desc: 'Se fremskridtsgrene og start oevelser'
keyboard: 'Tastatur'
keyboard_desc: 'Udforsk tastaturlayout og tasterstatistik'
statistics: 'Statistik'
statistics_desc: 'Se din skrivestatistik'
settings: 'Indstillinger'
settings_desc: 'Konfigurer keydr'
day_streak: ' | %{days} dages raekke'
key_progress: ' Tastefremskridt %{unlocked}/%{total} (%{mastered} mestrede) | Maal %{target} WPM%{streak}'
hint_start: '[1-3] Start'
hint_skill_tree: '[t] Faerdighedstrae'
hint_keyboard: '[b] Tastatur'
hint_stats: '[s] Statistik'
hint_settings: '[c] Indstillinger'
hint_quit: '[q] Afslut'
# Drill screen
drill:
title: ' Oevelse '
mode_adaptive: 'Adaptiv'
mode_code: 'Kode (Urangeret)'
mode_passage: 'Tekst (Urangeret)'
focus_char: 'Fokus: ''%{ch}'''
focus_bigram: 'Fokus: "%{bigram}"'
focus_both: 'Fokus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Noej'
header_err: 'Fejl'
code_source: ' Kodekilde '
passage_source: ' Tekstkilde '
footer: '[ESC] Afslut oevelse [Backspace] Slet'
keys_reenabled: 'Taster genaktiveret efter %{ms}ms'
hint_end: '[ESC] Afslut oevelse'
hint_backspace: '[Backspace] Slet'
# Dashboard / drill result
dashboard:
title: ' Oevelse faerdig '
results: 'Resultater'
unranked_note_prefix: ' (Urangeret'
unranked_note_suffix: ' taeller ikke i faerdighedstraeet)'
speed: ' Hastighed: '
accuracy_label: ' Noejagtighed: '
time_label: ' Tid: '
errors_label: ' Fejl: '
correct_detail: ' (%{correct}/%{total} korrekte)'
input_blocked: ' Indtastning midlertidigt blokeret '
input_blocked_ms: '(%{ms}ms tilbage)'
hint_continue: '[c/Enter/Space] Fortsaet'
hint_retry: '[r] Proev igen'
hint_menu: '[q] Menu'
hint_stats: '[s] Statistik'
hint_delete: '[x] Slet'
# Stats sidebar (during drill)
sidebar:
title: ' Statistik '
wpm: 'WPM: '
target: 'Maal: '
target_wpm: '%{wpm} WPM'
accuracy: 'Noejagtighed: '
progress: 'Fremskridt: '
correct: 'Korrekte: '
errors: 'Fejl: '
time: 'Tid: '
last_drill: ' Seneste oevelse '
vs_avg: ' vs gns: '
# Statistics dashboard
stats:
title: ' Statistik '
empty: 'Ingen oevelser gennemfoert endnu. Begynd at skrive!'
tab_dashboard: '[1] Dashboard'
tab_history: '[2] Historik'
tab_activity: '[3] Aktivitet'
tab_accuracy: '[4] Noejagtighed'
tab_timing: '[5] Timing'
tab_ngrams: '[6] N-grammer'
hint_back: '[ESC] Tilbage'
hint_next_tab: '[Tab] Naeste fane'
hint_switch_tab: '[1-6] Skift fane'
hint_navigate: '[j/k] Naviger'
hint_page: '[PgUp/PgDn] Side'
hint_delete: '[x] Slet'
summary_title: ' Oversigt '
drills: ' Oevelser: '
avg_wpm: ' Gns. WPM: '
best_wpm: ' Bedste WPM: '
accuracy_label: ' Noejagtighed: '
total_time: ' Samlet tid: '
wpm_chart_title: ' WPM per oevelse (Seneste 20, Maal: %{target}) '
accuracy_chart_title: ' Noejagtighed %% (Seneste 50 oevelser) '
chart_drill: 'Oevelse #'
chart_accuracy_pct: 'Noejagtighed %%'
sessions_title: ' Seneste sessioner '
session_header: ' # WPM Raa Noej%% Tid Dato/Tid Tilstand Rangeret Delvis'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Slet session #%{idx}? (y/n)'
confirm_title: ' Bekraeft '
yes: 'ja'
no: 'nej'
keyboard_accuracy_title: ' Tastatur noejagtighed %% '
keyboard_timing_title: ' Tastatur timing (ms) '
slowest_keys_title: ' Langsomste taster (ms) '
fastest_keys_title: ' Hurtigste taster (ms) '
worst_accuracy_title: ' Vaerste noejagtighed (%%) '
best_accuracy_title: ' Bedste noejagtighed (%%) '
not_enough_data: ' Ikke nok data'
streaks_title: ' Raekker '
current_streak: ' Nuvaerende: '
best_streak: ' Bedste: '
active_days: ' Aktive dage: '
top_days_none: ' Topdage: ingen'
top_days: ' Topdage: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Noej: %{pct}%%'
keys_label: ' Taster: %{unlocked}/%{total} (%{mastered} mestrede)'
ngram_empty: 'Gennemfoer nogle adaptive oevelser for at se n-gram data'
ngram_header_speed_narrow: ' Bgrm Hast Forv Anom%'
ngram_header_error_narrow: ' Bgrm Fejl Stp Freq Forv Anom%'
ngram_header_speed: ' Bigram Hast Forvent Stikpr. Anom%'
ngram_header_error: ' Bigram Fejl Stikpr. Freq Forvent Anom%'
focus_title: ' Aktivt fokus '
focus_char_label: ' Fokus: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'fejl'
anomaly_speed: 'hastighed'
focus_detail_both: ' Tegn ''%{ch}'': svageste tast | Bigram %{label}: %{type}-anomali %{pct}%%'
focus_detail_char_only: ' Tegn ''%{ch}'': svageste tast, ingen bekraeftede bigram-anomalier'
focus_detail_bigram_only: ' (%{type}-anomali: %{pct}%%)'
focus_empty: ' Gennemfoer nogle adaptive oevelser for at se fokusdata'
error_anomalies_title: ' Fejl-anomalier (%{count}) '
no_error_anomalies: ' Ingen fejl-anomalier opdaget'
speed_anomalies_title: ' Hastigheds-anomalier (%{count}) '
no_speed_anomalies: ' Ingen hastigheds-anomalier opdaget'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Tegn ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Daglig aktivitet (Sessioner per dag) '
jan: 'Jan'
feb: 'Feb'
mar: 'Mar'
apr: 'Apr'
may: 'Maj'
jun: 'Jun'
jul: 'Jul'
aug: 'Aug'
sep: 'Sep'
oct: 'Okt'
nov: 'Nov'
dec: 'Dec'
# Chart
chart:
wpm_over_time: ' WPM over tid '
drill_number: 'Oevelse #'
# Settings
settings:
title: ' Indstillinger '
subtitle: 'Piletaster til navigation, Enter/Hoejre for at aendre, ESC for at gemme'
target_wpm: 'Maal-WPM'
theme: 'Tema'
word_count: 'Antal ord'
ui_language: 'Sprog (UI)'
dictionary_language: 'Ordbogssprog'
keyboard_layout: 'Tastaturlayout'
code_language: 'Kodesprog'
code_downloads: 'Kode-downloads'
on: 'Til'
off: 'Fra'
code_download_dir: 'Kode-downloadmappe'
snippets_per_repo: 'Uddrag per repo'
unlimited: 'Ubegreanset'
download_code_now: 'Download kode nu'
run_downloader: 'Start download'
passage_downloads: 'Tekst-downloads'
passage_download_dir: 'Tekst-downloadmappe'
paragraphs_per_book: 'Afsnit per bog'
whole_book: 'Hele bogen'
download_passages_now: 'Download tekster nu'
export_path: 'Eksportsti'
export_data: 'Eksporter data'
export_now: 'Eksporter nu'
import_path: 'Importsti'
import_data: 'Importer data'
import_now: 'Importer nu'
hint_save_back: '[ESC] Gem & tilbage'
hint_change_value: '[Enter/pile] AEndr vaerdi'
hint_edit_path: '[Enter paa sti] Rediger'
hint_move: '[←→] Flyt'
hint_tab_complete: '[Tab] Fuldfoer (i slutningen)'
hint_confirm: '[Enter] Bekraeft'
hint_cancel: '[Esc] Annuller'
success_title: ' Succes '
error_title: ' Fejl '
press_any_key: 'Tryk paa en tast'
file_exists_title: ' Filen findes '
file_exists: 'Der findes allerede en fil paa denne sti.'
overwrite_rename: '[d] Overskriv [r] Omdoeb [Esc] Annuller'
erase_warning: 'Dette vil slette dine nuvaerende data.'
export_first: 'Eksporter foerst, hvis du vil beholde dem.'
proceed_yn: 'Fortsaet? (y/n)'
confirm_import_title: ' Bekraeft import '
# Selection screens
select:
dictionary_language_title: ' Vaelg ordbogssprog '
keyboard_layout_title: ' Vaelg tastaturlayout '
code_language_title: ' Vaelg kodesprog '
passage_source_title: ' Vaelg tekstkilde '
ui_language_title: ' Vaelg sprog (UI) '
more_above: '... %{count} flere ovenfor ...'
more_below: '... %{count} flere nedenfor ...'
current: ' (nuvaerende)'
disabled: ' (deaktiveret)'
enabled_default: ' (aktiveret, standard: %{layout})'
enabled: ' (aktiveret)'
disabled_blocked: ' (deaktiveret: blokeret)'
built_in: ' (indbygget)'
cached: ' (gemt)'
disabled_download: ' (deaktiveret: download kraeves)'
download_required: ' (download kraeves)'
hint_navigate: '[Op/Ned/PgUp/PgDn] Naviger'
hint_confirm: '[Enter] Bekraeft'
hint_back: '[ESC] Tilbage'
language_resets_layout: 'Sprogvalg nulstiller tastaturlayoutet til sprogets standard.'
layout_no_language_change: 'Layoutaendringer aendrer ikke ordbogssproget.'
disabled_network_notice: 'Nogle sprog er deaktiverede: aktiver netvaerksdownloads i intro/indstillinger.'
disabled_sources_notice: 'Nogle kilder er deaktiverede: aktiver netvaerksdownloads i intro/indstillinger.'
passage_all: 'Alle (Indbyggede + alle boeger)'
passage_builtin: 'Kun indbyggede passager'
passage_book_prefix: 'Bog: %{title}'
# Progress
progress:
overall_key_progress: 'Samlet tastefremskridt'
unlocked_mastered: '%{unlocked}/%{total} laast op (%{mastered} mestrede)'
# Skill tree
skill_tree:
title: ' Faerdighedstrae '
locked: 'Laast'
unlocked: 'laast op'
mastered: 'mestret'
in_progress: 'igangvaerende'
complete: 'faerdig'
locked_status: 'laast'
locked_notice: 'Fuldfoor %{count} grundbogstaver for at laase grene op'
branches_separator: 'Grene (tilgaengelige efter %{count} grundbogstaver)'
unlocked_letters: '%{unlocked}/%{total} bogstaver laast op'
level: 'Niveau %{current}/%{total}'
level_zero: 'Niveau 0/%{total}'
in_focus: ' i fokus'
hint_navigate: '[↑↓/jk] Naviger'
hint_scroll: '[PgUp/PgDn eller Ctrl+U/Ctrl+D] Rul'
hint_back: '[q] Tilbage'
hint_unlock: '[Enter] Laas op'
hint_start_drill: '[Enter] Start oevelse'
unlock_msg_1: 'Efter oplaasning blandes oplaaste taster fra denne gren ind i den adaptive oevelse.'
unlock_msg_2: 'Vil du kun oeve denne gren, start en oevelse direkte fra denne gren i Faerdighedstraeet.'
confirm_unlock: 'Laas %{branch} op?'
confirm_yn: '[y] Laas op [n/ESC] Annuller'
lvl_prefix: 'Niv'
branch_primary_letters: 'Grundbogstaver'
branch_capital_letters: 'Store bogstaver'
branch_numbers: 'Tal 0-9'
branch_prose_punctuation: 'Tegnsaetning'
branch_whitespace: 'Mellemrum'
branch_code_symbols: 'Kodesymboler'
level_frequency_order: 'Frekvensraekkefoelge'
level_common_sentence_capitals: 'Almindelige saetningsstorbogst.'
level_name_capitals: 'Navnestorbogstaver'
level_remaining_capitals: 'Oevrige storbogstaver'
level_common_digits: 'Almindelige cifre'
level_all_digits: 'Alle cifre'
level_essential: 'Essentielle'
level_common: 'Almindelige'
level_expressive: 'Udtryksfulde'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Indrykning'
level_arithmetic_assignment: 'Aritmetik & Tildeling'
level_grouping: 'Gruppering'
level_logic_reference: 'Logik & Reference'
level_special: 'Special'
# Milestones
milestones:
unlock_title: ' Tast laast op! '
mastery_title: ' Tast mestret! '
branches_title: ' Nye faerdighetsgrene tilgaengelige! '
branch_complete_title: ' Gren faerdig! '
all_unlocked_title: ' Alle taster laast op! '
all_mastered_title: ' Fuld tastaturmestring! '
unlocked: 'laast op'
mastered: 'mestret'
use_finger: 'Brug din %{finger}'
hold_right_shift: 'Hold hoejre Shift (hoejre lillefinger)'
hold_left_shift: 'Hold venstre Shift (venstre lillefinger)'
congratulations_all_letters: 'Tillykke! Du har mestret alle %{count} grundbogstaver'
new_branches_available: 'Nye faerdighetsgrene er nu tilgaengelige:'
visit_skill_tree: 'Besoeg Faerdighedstraeet for at laase en ny gren op'
and_start_training: 'og begynd at traene!'
open_skill_tree: 'Tryk [t] for at aabne Faerdighedstraeet nu'
branch_complete_msg: 'Du har fuldfaort grenen %{branch}!'
all_levels_mastered: 'Alle %{count} niveauer mestrede.'
all_keys_confident: 'Hver tast i denne gren har fuld tillid.'
all_unlocked_msg: 'Du har laast hver tast paa tastaturet op!'
all_unlocked_desc: 'Hvert tegn, symbol og modifikator er nu tilgaengelig i dine oevelser.'
keep_practicing_mastery: 'Bliv ved med at oeve for at opbygge mestring — naar hver tast naar fuld'
confidence_complete: 'tillid, har du opnaaat fuldstaendig tastaturmestring!'
all_mastered_msg: 'Tillykke — du har opnaaat fuldstaendig tastaturmestring!'
all_mastered_desc: 'Hver tast paa tastaturet har maksimal tillid.'
mastery_takes_practice: 'Mestring er ikke en destination — det kraever vedvarende oevelse.'
keep_drilling: 'Bliv ved med at oeve for at bevare dit niveau.'
hint_skill_tree_continue: '[t] Faerdighedstrae [Anden tast] Fortsaet'
hint_any_key: 'Tryk paa en tast for at fortsaette'
input_blocked: 'Indtastning midlertidigt blokeret (%{ms}ms tilbage)'
unlock_msg_1: 'Godt klaret! Bliv ved med at opbygge dine skrivefaerdigheder.'
unlock_msg_2: 'Endnu en tast i dit arsenal!'
unlock_msg_3: 'Dit tastatur vokser! Bliv ved.'
unlock_msg_4: 'Et skridt naermere fuld tastaturmestring!'
mastery_msg_1: 'Denne tast har nu fuld tillid!'
mastery_msg_2: 'Du mestrer denne tast perfekt!'
mastery_msg_3: 'Muskelhukommelse forankret!'
mastery_msg_4: 'Endnu en tast erobret!'
# Keyboard explorer
keyboard:
title: ' Tastatur '
subtitle: 'Tryk paa en tast eller klik paa en tast'
hint_navigate: '[←→↑↓/hjkl/Tab] Naviger'
hint_back: '[q/ESC] Tilbage'
key_label: 'Tast: '
finger_label: 'Finger: '
hand_left: 'Venstre'
hand_right: 'Hoejre'
finger_index: 'Pegefinger'
finger_middle: 'Langfinger'
finger_ring: 'Ringfinger'
finger_pinky: 'Lillefinger'
finger_thumb: 'Tommelfinger'
overall_accuracy: ' Samlet noejagtighed: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Rangeret noejagtighed: %{correct}/%{total} (%{pct}%%)'
confidence: 'Tillid: '
no_data: 'Ingen data endnu'
no_data_short: 'Ingen data'
key_details: ' Tastdetaljer '
key_details_char: ' Tastdetaljer: ''%{ch}'' '
key_details_name: ' Tastdetaljer: %{name} '
press_key_hint: 'Tryk paa en tast for detaljer'
shift_label: 'Shift: '
shift_no: 'Nej'
overall_avg_time: 'Samlet gns. tid: '
overall_best_time: 'Samlet bedste tid: '
overall_samples: 'Samlet stikproever: '
overall_accuracy_label: 'Samlet noejagtighed: '
branch_label: 'Gren: '
level_label: 'Niveau: '
built_in_key: 'Indbygget tast'
unlocked_label: 'Laast op: '
yes: 'Ja'
no: 'Nej'
in_focus_label: 'I fokus?: '
mastery_label: 'Mestring: '
mastery_locked: 'Laast'
ranked_avg_time: 'Rangeret gns. tid: '
ranked_best_time: 'Rangeret bedste tid: '
ranked_samples: 'Rangerede stikproever: '
ranked_accuracy_label: 'Rangeret noejagtighed: '
# Intro dialogs
intro:
passage_title: ' Tekst-download opsaetning '
code_title: ' Kode-download opsaetning '
enable_downloads: 'Aktiver netvaerksdownloads'
download_dir: 'Downloadmappe'
paragraphs_per_book: 'Afsnit per bog (0 = hele)'
whole_book: 'hele bogen'
snippets_per_repo: 'Uddrag per repo (0 = ubegreanset)'
unlimited: 'ubegreanset'
start_passage_drill: 'Start tekstoevelse'
start_code_drill: 'Start kodeoevelse'
confirm: 'Bekraeft'
hint_navigate: '[Op/Ned] Naviger'
hint_adjust: '[Venstre/Hoejre] Juster'
hint_edit: '[Skriv/Backspace] Rediger'
hint_confirm: '[Enter] Bekraeft'
hint_cancel: '[ESC] Annuller'
preparing_download: 'Forbereder download...'
download_passage_title: ' Downloader tekstkilde '
download_code_title: ' Downloader kodekilde '
book_label: ' Bog: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bytes'
downloaded_bytes: 'Downloadet: %{bytes} bytes'
downloading_book_progress: 'Downloader aktuel bog: [%{bar}] %{downloaded}/%{total} bytes'
downloading_book_bytes: 'Downloader aktuel bog: %{bytes} bytes'
downloading_code_progress: 'Downloader: [%{bar}] %{downloaded}/%{total} bytes'
downloading_code_bytes: 'Downloader: %{bytes} bytes'
current_book: 'Aktuel: %{name} (bog %{done}/%{total})'
current_repo: 'Aktuel: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr kan downloade passager fra Project Gutenberg til skriveoeving.'
passage_instructions_2: 'Boeger downloades een gang og gemmes lokalt.'
passage_instructions_3: 'Konfigurer downloadindstillinger nedenfor og start en tekstoevelse.'
code_instructions_1: 'keydr kan downloade open source-kode fra GitHub til skriveoeving.'
code_instructions_2: 'Kode downloades een gang og gemmes lokalt.'
code_instructions_3: 'Konfigurer downloadindstillinger nedenfor og start en kodeoevelse.'
# Status messages (from app.rs)
status:
recovery_files: 'Gendannelsesfiler fundet fra afbrudt import. Data kan vaere inkonsistent — overvaej at importere igen.'
dir_not_exist: 'Mappe findes ikke: %{path}'
no_data_store: 'Intet datalager tilgaengeligt'
serialization_error: 'Serialiseringsfejl: %{error}'
exported_to: 'Eksporteret til %{path}'
export_failed: 'Eksport mislykkedes: %{error}'
could_not_read: 'Kunne ikke laese filen: %{error}'
invalid_export: 'Ugyldig eksportfil: %{error}'
unsupported_version: 'Eksportversion ikke understottet: %{got} (forventet %{expected})'
import_failed: 'Import mislykkedes: %{error}'
imported_theme_fallback: 'Importeret (tema ''%{theme}'' ikke fundet, standard bruges)'
imported_success: 'Importeret'
adaptive_unavailable: 'Adaptiv rangeret tilstand ikke tilgaengelig: %{error}'
switched_to: 'Skiftet til %{name}'
layout_changed: 'Layout aendret til %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Ukendt sprog: %{key}'
unknown_layout: 'Ukendt tastaturlayout: %{key}'
unsupported_pair: 'Ikke-understottet sprog-/layoutpar: %{language} + %{layout}'
language_blocked: 'Sprog blokeret af supportniveau: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Tilbage'

454
locales/de.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminal-Tipptrainer'
adaptive_drill: 'Adaptive Lektion'
adaptive_drill_desc: 'Phonetische Woerter mit adaptiver Buchstabenfreischaltung'
code_drill: 'Code-Lektion'
code_drill_desc: 'Code-Syntax tippen ueben'
passage_drill: 'Textpassagen-Lektion'
passage_drill_desc: 'Passagen aus Buechern abtippen'
skill_tree: 'Faehigkeitenbaum'
skill_tree_desc: 'Fortschrittszweige ansehen und Lektionen starten'
keyboard: 'Tastatur'
keyboard_desc: 'Tastaturlayout und Tastenstatistiken erkunden'
statistics: 'Statistik'
statistics_desc: 'Tippstatistiken ansehen'
settings: 'Einstellungen'
settings_desc: 'keydr konfigurieren'
day_streak: ' | %{days} Tage Serie'
key_progress: ' Tastenfortschritt %{unlocked}/%{total} (%{mastered} gemeistert) | Ziel %{target} WPM%{streak}'
hint_start: '[1-3] Start'
hint_skill_tree: '[t] Faehigkeitenbaum'
hint_keyboard: '[b] Tastatur'
hint_stats: '[s] Statistik'
hint_settings: '[c] Einstellungen'
hint_quit: '[q] Beenden'
# Drill screen
drill:
title: ' Lektion '
mode_adaptive: 'Adaptiv'
mode_code: 'Code (ohne Wertung)'
mode_passage: 'Textpassage (ohne Wertung)'
focus_char: 'Fokus: ''%{ch}'''
focus_bigram: 'Fokus: "%{bigram}"'
focus_both: 'Fokus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Gen'
header_err: 'Feh'
code_source: ' Code-Quelle '
passage_source: ' Textquelle '
footer: '[ESC] Lektion beenden [Backspace] Loeschen'
keys_reenabled: 'Tasten nach %{ms}ms wieder aktiv'
hint_end: '[ESC] Lektion beenden'
hint_backspace: '[Backspace] Loeschen'
# Dashboard / drill result
dashboard:
title: ' Lektion abgeschlossen '
results: 'Ergebnisse'
unranked_note_prefix: ' (Ohne Wertung'
unranked_note_suffix: ' zaehlt nicht fuer den Faehigkeitenbaum)'
speed: ' Tempo: '
accuracy_label: ' Genauigkeit: '
time_label: ' Zeit: '
errors_label: ' Fehler: '
correct_detail: ' (%{correct}/%{total} korrekt)'
input_blocked: ' Eingabe voruebergehend blockiert '
input_blocked_ms: '(%{ms}ms verbleibend)'
hint_continue: '[c/Enter/Space] Weiter'
hint_retry: '[r] Wiederholen'
hint_menu: '[q] Menue'
hint_stats: '[s] Statistik'
hint_delete: '[x] Loeschen'
# Stats sidebar (during drill)
sidebar:
title: ' Statistik '
wpm: 'WPM: '
target: 'Ziel: '
target_wpm: '%{wpm} WPM'
accuracy: 'Genauigkeit: '
progress: 'Fortschritt: '
correct: 'Korrekt: '
errors: 'Fehler: '
time: 'Zeit: '
last_drill: ' Letzte Lektion '
vs_avg: ' vs Schnitt: '
# Statistics dashboard
stats:
title: ' Statistik '
empty: 'Noch keine Lektionen abgeschlossen. Fang an zu tippen!'
tab_dashboard: '[1] Dashboard'
tab_history: '[2] Verlauf'
tab_activity: '[3] Aktivitaet'
tab_accuracy: '[4] Genauigkeit'
tab_timing: '[5] Timing'
tab_ngrams: '[6] N-Gramme'
hint_back: '[ESC] Zurueck'
hint_next_tab: '[Tab] Naechster Tab'
hint_switch_tab: '[1-6] Tab wechseln'
hint_navigate: '[j/k] Navigieren'
hint_page: '[PgUp/PgDn] Seite'
hint_delete: '[x] Loeschen'
summary_title: ' Zusammenfassung '
drills: ' Lektionen: '
avg_wpm: ' Schnitt WPM: '
best_wpm: ' Bestes WPM: '
accuracy_label: ' Genauigkeit: '
total_time: ' Gesamtzeit: '
wpm_chart_title: ' WPM pro Lektion (Letzte 20, Ziel: %{target}) '
accuracy_chart_title: ' Genauigkeit %% (Letzte 50 Lektionen) '
chart_drill: 'Lektion #'
chart_accuracy_pct: 'Genauigkeit %%'
sessions_title: ' Letzte Sitzungen '
session_header: ' # WPM Roh Gen%% Zeit Datum/Uhrzeit Modus Gewertet Teilw.'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Sitzung #%{idx} loeschen? (y/n)'
confirm_title: ' Bestaetigen '
yes: 'ja'
no: 'nein'
keyboard_accuracy_title: ' Tastatur-Genauigkeit %% '
keyboard_timing_title: ' Tastatur-Timing (ms) '
slowest_keys_title: ' Langsamste Tasten (ms) '
fastest_keys_title: ' Schnellste Tasten (ms) '
worst_accuracy_title: ' Schlechteste Genauigkeit (%%) '
best_accuracy_title: ' Beste Genauigkeit (%%) '
not_enough_data: ' Nicht genug Daten'
streaks_title: ' Serien '
current_streak: ' Aktuell: '
best_streak: ' Beste: '
active_days: ' Aktive Tage: '
top_days_none: ' Top-Tage: keine'
top_days: ' Top-Tage: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Gen: %{pct}%%'
keys_label: ' Tasten: %{unlocked}/%{total} (%{mastered} gemeistert)'
ngram_empty: 'Schliesse einige adaptive Lektionen ab, um N-Gramm-Daten zu sehen'
ngram_header_speed_narrow: ' Bgrm Tempo Erw. Anom%'
ngram_header_error_narrow: ' Bgrm Feh Stp Rate Erw Anom%'
ngram_header_speed: ' Bigramm Tempo Erwartet Stichpr. Anom%'
ngram_header_error: ' Bigramm Fehler Stichpr. Rate Erwartet Anom%'
focus_title: ' Aktiver Fokus '
focus_char_label: ' Fokus: '
focus_bigram_value: 'Bigramm %{label}'
focus_plus: ' + '
anomaly_error: 'Fehler'
anomaly_speed: 'Tempo'
focus_detail_both: ' Zeichen ''%{ch}'': schwaechste Taste | Bigramm %{label}: %{type}-Anomalie %{pct}%%'
focus_detail_char_only: ' Zeichen ''%{ch}'': schwaechste Taste, keine bestaetigten Bigramm-Anomalien'
focus_detail_bigram_only: ' (%{type}-Anomalie: %{pct}%%)'
focus_empty: ' Schliesse einige adaptive Lektionen ab, um Fokusdaten zu sehen'
error_anomalies_title: ' Fehler-Anomalien (%{count}) '
no_error_anomalies: ' Keine Fehler-Anomalien erkannt'
speed_anomalies_title: ' Tempo-Anomalien (%{count}) '
no_speed_anomalies: ' Keine Tempo-Anomalien erkannt'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Zeichen ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Taegliche Aktivitaet (Sitzungen pro Tag) '
jan: 'Jan'
feb: 'Feb'
mar: 'Mär'
apr: 'Apr'
may: 'Mai'
jun: 'Jun'
jul: 'Jul'
aug: 'Aug'
sep: 'Sep'
oct: 'Okt'
nov: 'Nov'
dec: 'Dez'
# Chart
chart:
wpm_over_time: ' WPM im Zeitverlauf '
drill_number: 'Lektion #'
# Settings
settings:
title: ' Einstellungen '
subtitle: 'Pfeiltasten zum Navigieren, Enter/Rechts zum Aendern, ESC zum Speichern'
target_wpm: 'Ziel-WPM'
theme: 'Farbschema'
word_count: 'Wortanzahl'
ui_language: 'UI-Sprache'
dictionary_language: 'Woerterbuchsprache'
keyboard_layout: 'Tastaturlayout'
code_language: 'Codesprache'
code_downloads: 'Code-Downloads'
on: 'An'
off: 'Aus'
code_download_dir: 'Code-Downloadverz.'
snippets_per_repo: 'Schnipsel pro Repo'
unlimited: 'Unbegrenzt'
download_code_now: 'Code jetzt laden'
run_downloader: 'Download starten'
passage_downloads: 'Text-Downloads'
passage_download_dir: 'Text-Downloadverz.'
paragraphs_per_book: 'Absaetze pro Buch'
whole_book: 'Ganzes Buch'
download_passages_now: 'Texte jetzt laden'
export_path: 'Exportpfad'
export_data: 'Daten exportieren'
export_now: 'Jetzt exportieren'
import_path: 'Importpfad'
import_data: 'Daten importieren'
import_now: 'Jetzt importieren'
hint_save_back: '[ESC] Speichern & zurueck'
hint_change_value: '[Enter/Pfeile] Wert aendern'
hint_edit_path: '[Enter auf Pfad] Bearbeiten'
hint_move: '[←→] Bewegen'
hint_tab_complete: '[Tab] Vervollstaendigen (am Ende)'
hint_confirm: '[Enter] Bestaetigen'
hint_cancel: '[Esc] Abbrechen'
success_title: ' Erfolg '
error_title: ' Fehler '
press_any_key: 'Beliebige Taste druecken'
file_exists_title: ' Datei existiert '
file_exists: 'An diesem Pfad existiert bereits eine Datei.'
overwrite_rename: '[d] Ueberschreiben [r] Umbenennen [Esc] Abbrechen'
erase_warning: 'Dies wird Ihre aktuellen Daten loeschen.'
export_first: 'Exportieren Sie zuerst, wenn Sie sie behalten moechten.'
proceed_yn: 'Fortfahren? (y/n)'
confirm_import_title: ' Import bestaetigen '
# Selection screens
select:
dictionary_language_title: ' Woerterbuchsprache waehlen '
keyboard_layout_title: ' Tastaturlayout waehlen '
code_language_title: ' Codesprache waehlen '
passage_source_title: ' Textquelle waehlen '
ui_language_title: ' UI-Sprache waehlen '
more_above: '... %{count} weitere oben ...'
more_below: '... %{count} weitere unten ...'
current: ' (aktuell)'
disabled: ' (deaktiviert)'
enabled_default: ' (aktiviert, Standard: %{layout})'
enabled: ' (aktiviert)'
disabled_blocked: ' (deaktiviert: gesperrt)'
built_in: ' (eingebaut)'
cached: ' (gespeichert)'
disabled_download: ' (deaktiviert: Download erforderlich)'
download_required: ' (Download erforderlich)'
hint_navigate: '[Auf/Ab/BildAuf/BildAb] Navigieren'
hint_confirm: '[Enter] Bestaetigen'
hint_back: '[ESC] Zurueck'
language_resets_layout: 'Die Sprachauswahl setzt das Tastaturlayout auf den Standard der Sprache zurueck.'
layout_no_language_change: 'Layoutaenderungen aendern nicht die Woerterbuchsprache.'
disabled_network_notice: 'Einige Sprachen sind deaktiviert: Netzwerk-Downloads in Intro/Einstellungen aktivieren.'
disabled_sources_notice: 'Einige Quellen sind deaktiviert: Netzwerk-Downloads in Intro/Einstellungen aktivieren.'
passage_all: 'Alle (Eingebaut + alle Buecher)'
passage_builtin: 'Nur eingebaute Passagen'
passage_book_prefix: 'Buch: %{title}'
# Progress
progress:
overall_key_progress: 'Gesamter Tastenfortschritt'
unlocked_mastered: '%{unlocked}/%{total} freigeschaltet (%{mastered} gemeistert)'
# Skill tree
skill_tree:
title: ' Faehigkeitenbaum '
locked: 'Gesperrt'
unlocked: 'freigeschaltet'
mastered: 'gemeistert'
in_progress: 'in Bearbeitung'
complete: 'abgeschlossen'
locked_status: 'gesperrt'
locked_notice: '%{count} Grundbuchstaben abschliessen, um Zweige freizuschalten'
branches_separator: 'Zweige (verfuegbar nach %{count} Grundbuchstaben)'
unlocked_letters: '%{unlocked}/%{total} Buchstaben freigeschaltet'
level: 'Stufe %{current}/%{total}'
level_zero: 'Stufe 0/%{total}'
in_focus: ' im Fokus'
hint_navigate: '[↑↓/jk] Navigieren'
hint_scroll: '[BildAuf/BildAb oder Strg+U/Strg+D] Scrollen'
hint_back: '[q] Zurueck'
hint_unlock: '[Enter] Freischalten'
hint_start_drill: '[Enter] Lektion starten'
unlock_msg_1: 'Nach dem Freischalten werden freigeschaltete Tasten dieses Zweigs in die adaptive Lektion eingemischt.'
unlock_msg_2: 'Um nur diesen Zweig zu ueben, starte eine Lektion direkt aus diesem Zweig im Faehigkeitenbaum.'
confirm_unlock: '%{branch} freischalten?'
confirm_yn: '[y] Freischalten [n/ESC] Abbrechen'
lvl_prefix: 'Lvl'
branch_primary_letters: 'Grundbuchstaben'
branch_capital_letters: 'Grossbuchstaben'
branch_numbers: 'Zahlen 0-9'
branch_prose_punctuation: 'Interpunktion'
branch_whitespace: 'Leerzeichen'
branch_code_symbols: 'Code-Symbole'
level_frequency_order: 'Haeufigkeitsfolge'
level_common_sentence_capitals: 'Haeufige Satzanfaenge'
level_name_capitals: 'Namensgrossbuchst.'
level_remaining_capitals: 'Restl. Grossbuchst.'
level_common_digits: 'Haeufige Ziffern'
level_all_digits: 'Alle Ziffern'
level_essential: 'Grundlegend'
level_common: 'Haeufig'
level_expressive: 'Ausdruck'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Einrueckung'
level_arithmetic_assignment: 'Arithmetik & Zuweisung'
level_grouping: 'Gruppierung'
level_logic_reference: 'Logik & Referenz'
level_special: 'Spezial'
# Milestones
milestones:
unlock_title: ' Taste freigeschaltet! '
mastery_title: ' Taste gemeistert! '
branches_title: ' Neue Faehigkeitenzweige verfuegbar! '
branch_complete_title: ' Zweig abgeschlossen! '
all_unlocked_title: ' Alle Tasten freigeschaltet! '
all_mastered_title: ' Volle Tastaturbeherrschung! '
unlocked: 'freigeschaltet'
mastered: 'gemeistert'
use_finger: 'Benutze deinen %{finger}'
hold_right_shift: 'Rechte Umschalttaste halten (rechter kleiner Finger)'
hold_left_shift: 'Linke Umschalttaste halten (linker kleiner Finger)'
congratulations_all_letters: 'Glueckwunsch! Du hast alle %{count} Grundbuchstaben gemeistert'
new_branches_available: 'Neue Faehigkeitenzweige sind jetzt verfuegbar:'
visit_skill_tree: 'Besuche den Faehigkeitenbaum, um einen neuen Zweig'
and_start_training: 'freizuschalten und zu trainieren!'
open_skill_tree: 'Druecke [t], um den Faehigkeitenbaum zu oeffnen'
branch_complete_msg: 'Du hast den Zweig %{branch} abgeschlossen!'
all_levels_mastered: 'Alle %{count} Stufen gemeistert.'
all_keys_confident: 'Jede Taste in diesem Zweig hat volle Sicherheit.'
all_unlocked_msg: 'Du hast jede Taste auf der Tastatur freigeschaltet!'
all_unlocked_desc: 'Jedes Zeichen, Symbol und jeder Modifikator ist jetzt in deinen Lektionen verfuegbar.'
keep_practicing_mastery: 'Uebe weiter, um Meisterschaft aufzubauen — wenn jede Taste volle'
confidence_complete: 'Sicherheit erreicht hat, hast du die volle Tastaturbeherrschung!'
all_mastered_msg: 'Glueckwunsch — du hast volle Tastaturbeherrschung erreicht!'
all_mastered_desc: 'Jede Taste auf der Tastatur hat maximale Sicherheit.'
mastery_takes_practice: 'Meisterschaft ist kein Ziel — sie erfordert staendiges Ueben.'
keep_drilling: 'Uebe weiter, um dein Koennen zu erhalten.'
hint_skill_tree_continue: '[t] Faehigkeitenbaum [Andere Taste] Weiter'
hint_any_key: 'Beliebige Taste zum Fortfahren'
input_blocked: 'Eingabe voruebergehend blockiert (%{ms}ms verbleibend)'
unlock_msg_1: 'Gut gemacht! Baue deine Tippfaehigkeiten weiter aus.'
unlock_msg_2: 'Eine weitere Taste in deinem Arsenal!'
unlock_msg_3: 'Deine Tastatur waechst! Weiter so.'
unlock_msg_4: 'Einen Schritt naeher an voller Tastaturbeherrschung!'
mastery_msg_1: 'Diese Taste hat jetzt volle Sicherheit!'
mastery_msg_2: 'Diese Taste sitzt perfekt!'
mastery_msg_3: 'Muskelgedaechtnis verankert!'
mastery_msg_4: 'Eine weitere Taste bezwungen!'
# Keyboard explorer
keyboard:
title: ' Tastatur '
subtitle: 'Druecke eine Taste oder klicke darauf'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigieren'
hint_back: '[q/ESC] Zurueck'
key_label: 'Taste: '
finger_label: 'Finger: '
hand_left: 'Links'
hand_right: 'Rechts'
finger_index: 'Zeigefinger'
finger_middle: 'Mittelfinger'
finger_ring: 'Ringfinger'
finger_pinky: 'Kleiner Finger'
finger_thumb: 'Daumen'
overall_accuracy: ' Gesamtgenauigkeit: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Gewertete Genauigkeit: %{correct}/%{total} (%{pct}%%)'
confidence: 'Sicherheit: '
no_data: 'Noch keine Daten'
no_data_short: 'Keine Daten'
key_details: ' Tastendetails '
key_details_char: ' Tastendetails: ''%{ch}'' '
key_details_name: ' Tastendetails: %{name} '
press_key_hint: 'Druecke eine Taste fuer Details'
shift_label: 'Umschalt: '
shift_no: 'Nein'
overall_avg_time: 'Gesamt Schnittzeit: '
overall_best_time: 'Gesamt Bestzeit: '
overall_samples: 'Gesamt Stichproben: '
overall_accuracy_label: 'Gesamt Genauigkeit: '
branch_label: 'Zweig: '
level_label: 'Stufe: '
built_in_key: 'Eingebaute Taste'
unlocked_label: 'Freigeschaltet: '
yes: 'Ja'
no: 'Nein'
in_focus_label: 'Im Fokus?: '
mastery_label: 'Meisterschaft: '
mastery_locked: 'Gesperrt'
ranked_avg_time: 'Gewertete Schnittzeit: '
ranked_best_time: 'Gewertete Bestzeit: '
ranked_samples: 'Gewertete Stichproben: '
ranked_accuracy_label: 'Gewertete Genauigkeit: '
# Intro dialogs
intro:
passage_title: ' Textpassagen-Download Einrichtung '
code_title: ' Code-Download Einrichtung '
enable_downloads: 'Netzwerk-Downloads aktivieren'
download_dir: 'Download-Verzeichnis'
paragraphs_per_book: 'Absaetze pro Buch (0 = ganz)'
whole_book: 'ganzes Buch'
snippets_per_repo: 'Schnipsel pro Repo (0 = unbegrenzt)'
unlimited: 'unbegrenzt'
start_passage_drill: 'Textpassagen-Lektion starten'
start_code_drill: 'Code-Lektion starten'
confirm: 'Bestaetigen'
hint_navigate: '[Auf/Ab] Navigieren'
hint_adjust: '[Links/Rechts] Anpassen'
hint_edit: '[Tippen/Backspace] Bearbeiten'
hint_confirm: '[Enter] Bestaetigen'
hint_cancel: '[ESC] Abbrechen'
preparing_download: 'Download wird vorbereitet...'
download_passage_title: ' Textquelle wird heruntergeladen '
download_code_title: ' Code-Quelle wird heruntergeladen '
book_label: ' Buch: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} Bytes'
downloaded_bytes: 'Heruntergeladen: %{bytes} Bytes'
downloading_book_progress: 'Aktuelles Buch wird geladen: [%{bar}] %{downloaded}/%{total} Bytes'
downloading_book_bytes: 'Aktuelles Buch wird geladen: %{bytes} Bytes'
downloading_code_progress: 'Wird heruntergeladen: [%{bar}] %{downloaded}/%{total} Bytes'
downloading_code_bytes: 'Wird heruntergeladen: %{bytes} Bytes'
current_book: 'Aktuell: %{name} (Buch %{done}/%{total})'
current_repo: 'Aktuell: %{name} (Repo %{done}/%{total})'
passage_instructions_1: 'keydr kann Textpassagen von Project Gutenberg zum Tippueben herunterladen.'
passage_instructions_2: 'Buecher werden einmal heruntergeladen und lokal gespeichert.'
passage_instructions_3: 'Konfiguriere die Download-Einstellungen unten und starte eine Textpassagen-Lektion.'
code_instructions_1: 'keydr kann Open-Source-Code von GitHub zum Tippueben herunterladen.'
code_instructions_2: 'Code wird einmal heruntergeladen und lokal gespeichert.'
code_instructions_3: 'Konfiguriere die Download-Einstellungen unten und starte eine Code-Lektion.'
# Status messages (from app.rs)
status:
recovery_files: 'Wiederherstellungsdateien von unterbrochenem Import gefunden. Daten koennten inkonsistent sein — erneuter Import empfohlen.'
dir_not_exist: 'Verzeichnis existiert nicht: %{path}'
no_data_store: 'Kein Datenspeicher verfuegbar'
serialization_error: 'Serialisierungsfehler: %{error}'
exported_to: 'Exportiert nach %{path}'
export_failed: 'Export fehlgeschlagen: %{error}'
could_not_read: 'Datei konnte nicht gelesen werden: %{error}'
invalid_export: 'Ungueltige Exportdatei: %{error}'
unsupported_version: 'Nicht unterstuetzte Exportversion: %{got} (erwartet %{expected})'
import_failed: 'Import fehlgeschlagen: %{error}'
imported_theme_fallback: 'Erfolgreich importiert (Farbschema ''%{theme}'' nicht gefunden, Standard wird verwendet)'
imported_success: 'Erfolgreich importiert'
adaptive_unavailable: 'Adaptiver gewerteter Modus nicht verfuegbar: %{error}'
switched_to: 'Gewechselt zu %{name}'
layout_changed: 'Layout geaendert zu %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Unbekannte Sprache: %{key}'
unknown_layout: 'Unbekanntes Tastaturlayout: %{key}'
unsupported_pair: 'Nicht unterstuetztes Sprach-/Layout-Paar: %{language} + %{layout}'
language_blocked: 'Sprache durch Unterstuetzungsstufe gesperrt: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'ZPM'
back: 'Zurueck'

454
locales/en.yml Normal file
View File

@@ -0,0 +1,454 @@
# 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}'
hes_label: ' | Hes: >%{ms}ms'
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} 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'

454
locales/es.yml Normal file
View File

@@ -0,0 +1,454 @@
# Menú principal
menu:
subtitle: 'Tutor de Mecanografía en Terminal'
adaptive_drill: 'Ejercicio Adaptativo'
adaptive_drill_desc: 'Palabras fonéticas con desbloqueo adaptativo de teclas'
code_drill: 'Ejercicio de Código'
code_drill_desc: 'Practica escribiendo sintaxis de código'
passage_drill: 'Ejercicio de Pasaje'
passage_drill_desc: 'Escribe pasajes de libros'
skill_tree: 'Árbol de Habilidades'
skill_tree_desc: 'Ver ramas de progresión e iniciar ejercicios'
keyboard: 'Teclado'
keyboard_desc: 'Explora la distribución del teclado y estadísticas'
statistics: 'Estadísticas'
statistics_desc: 'Ver tus estadísticas de escritura'
settings: 'Configuración'
settings_desc: 'Configurar keydr'
day_streak: ' | %{days} días seguidos'
key_progress: ' Progreso de Teclas %{unlocked}/%{total} (%{mastered} dominadas) | Objetivo %{target} WPM%{streak}'
hint_start: '[1-3] Iniciar'
hint_skill_tree: '[t] Árbol de Habilidades'
hint_keyboard: '[b] Teclado'
hint_stats: '[s] Estadísticas'
hint_settings: '[c] Configuración'
hint_quit: '[q] Salir'
# Pantalla de ejercicio
drill:
title: ' Ejercicio '
mode_adaptive: 'Adaptativo'
mode_code: 'Código (Sin rango)'
mode_passage: 'Pasaje (Sin rango)'
focus_char: 'Foco: ''%{ch}'''
focus_bigram: 'Foco: "%{bigram}"'
focus_both: 'Foco: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Pre'
header_err: 'Err'
code_source: ' Fuente de código '
passage_source: ' Fuente del pasaje '
footer: '[ESC] Fin [Backspace] Borrar'
keys_reenabled: 'Teclas reactivadas en %{ms}ms'
hint_end: '[ESC] Fin del ejercicio'
hint_backspace: '[Backspace] Borrar'
# Panel / resultado del ejercicio
dashboard:
title: ' Ejercicio Completo '
results: 'Resultados'
unranked_note_prefix: ' (Sin rango'
unranked_note_suffix: ' no cuenta para el árbol de habilidades)'
speed: ' Velocidad: '
accuracy_label: ' Precisión: '
time_label: ' Tiempo: '
errors_label: ' Errores: '
correct_detail: ' (%{correct}/%{total} correctos)'
input_blocked: ' Entrada bloqueada temporalmente '
input_blocked_ms: '(%{ms}ms restantes)'
hint_continue: '[c/Enter/Space] Continuar'
hint_retry: '[r] Reintentar'
hint_menu: '[q] Menú'
hint_stats: '[s] Estadísticas'
hint_delete: '[x] Eliminar'
# Barra lateral de estadísticas (durante el ejercicio)
sidebar:
title: ' Estadísticas '
wpm: 'WPM: '
target: 'Objetivo: '
target_wpm: '%{wpm} WPM'
accuracy: 'Precisión: '
progress: 'Progreso: '
correct: 'Correcto: '
errors: 'Errores: '
time: 'Tiempo: '
last_drill: ' Último Ejercicio '
vs_avg: ' vs prom: '
# Panel de estadísticas
stats:
title: ' Estadísticas '
empty: 'Aún no hay ejercicios completados. ¡Empieza a escribir!'
tab_dashboard: '[1] Panel'
tab_history: '[2] Historial'
tab_activity: '[3] Actividad'
tab_accuracy: '[4] Precisión'
tab_timing: '[5] Tiempos'
tab_ngrams: '[6] N-gramas'
hint_back: '[ESC] Volver'
hint_next_tab: '[Tab] Siguiente pestaña'
hint_switch_tab: '[1-6] Cambiar pestaña'
hint_navigate: '[j/k] Navegar'
hint_page: '[PgUp/PgDn] Página'
hint_delete: '[x] Eliminar'
summary_title: ' Resumen '
drills: ' Ejercicios: '
avg_wpm: ' WPM Prom: '
best_wpm: ' Mejor WPM: '
accuracy_label: ' Precisión: '
total_time: ' Tiempo total: '
wpm_chart_title: ' WPM por Ejercicio (Últimos 20, Objetivo: %{target}) '
accuracy_chart_title: ' Precisión %% (Últimos 50 Ejercicios) '
chart_drill: 'Ejercicio #'
chart_accuracy_pct: 'Precisión %%'
sessions_title: ' Sesiones Recientes '
session_header: ' # WPM Raw Pre%% Tiempo Fecha/Hora Modo Rango Parcial'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: '¿Eliminar sesión #%{idx}? (y/n)'
confirm_title: ' Confirmar '
yes: 'sí'
no: 'no'
keyboard_accuracy_title: ' Precisión del Teclado %% '
keyboard_timing_title: ' Tiempos del Teclado (ms) '
slowest_keys_title: ' Teclas más Lentas (ms) '
fastest_keys_title: ' Teclas más Rápidas (ms) '
worst_accuracy_title: ' Peor Precisión (%%) '
best_accuracy_title: ' Mejor Precisión (%%) '
not_enough_data: ' Datos insuficientes'
streaks_title: ' Rachas '
current_streak: ' Actual: '
best_streak: ' Mejor: '
active_days: ' Días activos: '
top_days_none: ' Mejores días: ninguno'
top_days: ' Mejores días: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Pre: %{pct}%%'
keys_label: ' Teclas: %{unlocked}/%{total} (%{mastered} dominadas)'
ngram_empty: 'Completa ejercicios adaptativos para ver datos de n-gramas'
ngram_header_speed_narrow: ' Bgrm Vel Esper Anom%'
ngram_header_error_narrow: ' Bgrm Err Mst Tasa Esp Anom%'
ngram_header_speed: ' Bigrama Vel Esper Muestras Anom%'
ngram_header_error: ' Bigrama Errores Muestras Tasa Esper Anom%'
focus_title: ' Foco Activo '
focus_char_label: ' Foco: '
focus_bigram_value: 'Bigrama %{label}'
focus_plus: ' + '
anomaly_error: 'errores'
anomaly_speed: 'velocidad'
focus_detail_both: ' Carácter ''%{ch}'': tecla más débil | Bigrama %{label}: anomalía de %{type} %{pct}%%'
focus_detail_char_only: ' Carácter ''%{ch}'': tecla más débil, sin anomalías de bigrama confirmadas'
focus_detail_bigram_only: ' (anomalía de %{type}: %{pct}%%)'
focus_empty: ' Completa ejercicios adaptativos para ver datos de foco'
error_anomalies_title: ' Anomalías de Error (%{count}) '
no_error_anomalies: ' No se detectaron anomalías de error'
speed_anomalies_title: ' Anomalías de Velocidad (%{count}) '
no_speed_anomalies: ' No se detectaron anomalías de velocidad'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Carácter ''%{ch}'''
# Mapa de actividad
heatmap:
title: ' Actividad Diaria (Sesiones por Día) '
jan: 'Ene'
feb: 'Feb'
mar: 'Mar'
apr: 'Abr'
may: 'May'
jun: 'Jun'
jul: 'Jul'
aug: 'Ago'
sep: 'Sep'
oct: 'Oct'
nov: 'Nov'
dec: 'Dic'
# Gráfico
chart:
wpm_over_time: ' WPM a lo largo del tiempo '
drill_number: 'Ejercicio #'
# Configuración
settings:
title: ' Configuración '
subtitle: 'Usa las flechas para navegar, Enter/Derecha para cambiar, ESC para guardar y salir'
target_wpm: 'WPM Objetivo'
theme: 'Tema'
word_count: 'Cantidad de Palabras'
ui_language: 'Idioma de Interfaz'
dictionary_language: 'Idioma del Diccionario'
keyboard_layout: 'Distribución de Teclado'
code_language: 'Lenguaje de Código'
code_downloads: 'Descargas de Código'
on: 'Sí'
off: 'No'
code_download_dir: 'Dir. Descarga de Código'
snippets_per_repo: 'Fragmentos por Repo'
unlimited: 'Ilimitado'
download_code_now: 'Descargar Código Ahora'
run_downloader: 'Ejecutar descargador'
passage_downloads: 'Descargas de Pasajes'
passage_download_dir: 'Dir. Descarga de Pasajes'
paragraphs_per_book: 'Párrafos por Libro'
whole_book: 'Libro completo'
download_passages_now: 'Descargar Pasajes Ahora'
export_path: 'Ruta de Exportación'
export_data: 'Exportar Datos'
export_now: 'Exportar ahora'
import_path: 'Ruta de Importación'
import_data: 'Importar Datos'
import_now: 'Importar ahora'
hint_save_back: '[ESC] Guardar y volver'
hint_change_value: '[Enter/flechas] Cambiar valor'
hint_edit_path: '[Enter en ruta] Editar'
hint_move: '[←→] Mover'
hint_tab_complete: '[Tab] Completar (al final)'
hint_confirm: '[Enter] Confirmar'
hint_cancel: '[Esc] Cancelar'
success_title: ' Éxito '
error_title: ' Fallo '
press_any_key: 'Presiona cualquier tecla'
file_exists_title: ' Archivo Existente '
file_exists: 'Ya existe un archivo en esta ruta.'
overwrite_rename: '[d] Sobrescribir [r] Renombrar [Esc] Cancelar'
erase_warning: 'Esto borrará tus datos actuales.'
export_first: 'Exporta primero si deseas conservarlos.'
proceed_yn: '¿Continuar? (y/n)'
confirm_import_title: ' Confirmar Importación '
# Pantallas de selección
select:
dictionary_language_title: ' Seleccionar Idioma del Diccionario '
keyboard_layout_title: ' Seleccionar Distribución de Teclado '
code_language_title: ' Seleccionar Lenguaje de Código '
passage_source_title: ' Seleccionar Fuente de Pasajes '
ui_language_title: ' Seleccionar Idioma de Interfaz '
more_above: '... %{count} más arriba ...'
more_below: '... %{count} más abajo ...'
current: ' (actual)'
disabled: ' (desactivado)'
enabled_default: ' (activado, predeterminado: %{layout})'
enabled: ' (activado)'
disabled_blocked: ' (desactivado: bloqueado)'
built_in: ' (incluido)'
cached: ' (en caché)'
disabled_download: ' (desactivado: requiere descarga)'
download_required: ' (requiere descarga)'
hint_navigate: '[Up/Down/PgUp/PgDn] Navegar'
hint_confirm: '[Enter] Confirmar'
hint_back: '[ESC] Volver'
language_resets_layout: 'Seleccionar un idioma restablece la distribución a la predeterminada de ese idioma.'
layout_no_language_change: 'Cambiar distribución no cambia el idioma del diccionario.'
disabled_network_notice: 'Algunos idiomas están desactivados: activa las descargas en intro/configuración.'
disabled_sources_notice: 'Algunas fuentes están desactivadas: activa las descargas en intro/configuración.'
passage_all: 'Todos (Incluidos + todos los libros)'
passage_builtin: 'Solo pasajes incluidos'
passage_book_prefix: 'Libro: %{title}'
# Progreso
progress:
overall_key_progress: 'Progreso General de Teclas'
unlocked_mastered: '%{unlocked}/%{total} desbloqueadas (%{mastered} dominadas)'
# Árbol de habilidades
skill_tree:
title: ' Árbol de Habilidades '
locked: 'Bloqueado'
unlocked: 'desbloqueado'
mastered: 'dominado'
in_progress: 'en progreso'
complete: 'completo'
locked_status: 'bloqueado'
locked_notice: 'Completa %{count} letras primarias para desbloquear ramas'
branches_separator: 'Ramas (disponibles tras %{count} letras primarias)'
unlocked_letters: 'Desbloqueadas %{unlocked}/%{total} letras'
level: 'Nivel %{current}/%{total}'
level_zero: 'Nivel 0/%{total}'
in_focus: ' en foco'
hint_navigate: '[↑↓/jk] Navegar'
hint_scroll: '[PgUp/PgDn o Ctrl+U/Ctrl+D] Desplazar'
hint_back: '[q] Volver'
hint_unlock: '[Enter] Desbloquear'
hint_start_drill: '[Enter] Iniciar Ejercicio'
unlock_msg_1: 'Una vez desbloqueado, el ejercicio adaptativo incluirá teclas de esta rama que estén desbloqueadas.'
unlock_msg_2: 'Si quieres enfocarte solo en esta rama, inicia un ejercicio directamente desde esta rama en el Árbol de Habilidades.'
confirm_unlock: '¿Desbloquear %{branch}?'
confirm_yn: '[y] Desbloquear [n/ESC] Cancelar'
lvl_prefix: 'Niv'
branch_primary_letters: 'Letras Primarias'
branch_capital_letters: 'Letras Mayúsculas'
branch_numbers: 'Números 0-9'
branch_prose_punctuation: 'Puntuación de Prosa'
branch_whitespace: 'Espacios en Blanco'
branch_code_symbols: 'Símbolos de Código'
level_frequency_order: 'Orden por Frecuencia'
level_common_sentence_capitals: 'Mayúsculas Comunes de Oración'
level_name_capitals: 'Mayúsculas de Nombres'
level_remaining_capitals: 'Mayúsculas Restantes'
level_common_digits: 'Dígitos Comunes'
level_all_digits: 'Todos los Dígitos'
level_essential: 'Esencial'
level_common: 'Común'
level_expressive: 'Expresivo'
level_enter_return: 'Enter/Retorno'
level_tab_indent: 'Tab/Sangría'
level_arithmetic_assignment: 'Aritmética y Asignación'
level_grouping: 'Agrupación'
level_logic_reference: 'Lógica y Referencia'
level_special: 'Especial'
# Hitos
milestones:
unlock_title: ' ¡Tecla Desbloqueada! '
mastery_title: ' ¡Tecla Dominada! '
branches_title: ' ¡Nuevas Ramas Disponibles! '
branch_complete_title: ' ¡Rama Completada! '
all_unlocked_title: ' ¡Todas las Teclas Desbloqueadas! '
all_mastered_title: ' ¡Dominio Total del Teclado! '
unlocked: 'desbloqueada'
mastered: 'dominada'
use_finger: 'Usa tu dedo %{finger}'
hold_right_shift: 'Mantén Shift Derecho (meñique derecho)'
hold_left_shift: 'Mantén Shift Izquierdo (meñique izquierdo)'
congratulations_all_letters: '¡Felicidades! Has dominado las %{count} letras primarias'
new_branches_available: 'Nuevas ramas de habilidades están disponibles:'
visit_skill_tree: 'Visita el Árbol de Habilidades para desbloquear una nueva rama'
and_start_training: '¡y empieza a entrenar!'
open_skill_tree: 'Presiona [t] para abrir el Árbol de Habilidades'
branch_complete_msg: '¡Has completado la rama %{branch}!'
all_levels_mastered: 'Los %{count} niveles dominados.'
all_keys_confident: 'Cada tecla en esta rama está a máxima confianza.'
all_unlocked_msg: '¡Has desbloqueado todas las teclas del teclado!'
all_unlocked_desc: 'Cada carácter, símbolo y modificador está disponible en tus ejercicios.'
keep_practicing_mastery: 'Sigue practicando para alcanzar el dominio — cuando cada tecla llegue a'
confidence_complete: 'máxima confianza, ¡habrás logrado el dominio total del teclado!'
all_mastered_msg: '¡Felicidades — has alcanzado el dominio total del teclado!'
all_mastered_desc: 'Cada tecla del teclado está a máxima confianza.'
mastery_takes_practice: 'El dominio no es un destino — requiere práctica continua.'
keep_drilling: 'Sigue practicando para mantener tu nivel.'
hint_skill_tree_continue: '[t] Abrir Árbol de Habilidades [Otra tecla] Continuar'
hint_any_key: 'Presiona cualquier tecla para continuar'
input_blocked: 'Entrada bloqueada temporalmente (%{ms}ms restantes)'
unlock_msg_1: '¡Buen trabajo! Sigue mejorando tus habilidades.'
unlock_msg_2: '¡Otra tecla añadida a tu arsenal!'
unlock_msg_3: '¡Tu teclado crece! Sigue así.'
unlock_msg_4: '¡Un paso más cerca del dominio total!'
mastery_msg_1: '¡Esta tecla está a máxima confianza!'
mastery_msg_2: '¡Dominas esta tecla a la perfección!'
mastery_msg_3: '¡Memoria muscular asegurada!'
mastery_msg_4: '¡Una tecla más conquistada!'
# Explorador de teclado
keyboard:
title: ' Teclado '
subtitle: 'Presiona o haz clic en una tecla'
hint_navigate: '[←→↑↓/hjkl/Tab] Navegar'
hint_back: '[q/ESC] Volver'
key_label: 'Tecla: '
finger_label: 'Dedo: '
hand_left: 'Izquierda'
hand_right: 'Derecha'
finger_index: 'Índice'
finger_middle: 'Medio'
finger_ring: 'Anular'
finger_pinky: 'Meñique'
finger_thumb: 'Pulgar'
overall_accuracy: ' Precisión general: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Precisión clasificada: %{correct}/%{total} (%{pct}%%)'
confidence: 'Confianza: '
no_data: 'Sin datos aún'
no_data_short: 'Sin datos'
key_details: ' Detalles de Tecla '
key_details_char: ' Detalles de Tecla: ''%{ch}'' '
key_details_name: ' Detalles de Tecla: %{name} '
press_key_hint: 'Presiona una tecla para ver sus detalles'
shift_label: 'Shift: '
shift_no: 'No'
overall_avg_time: 'Tiempo Prom. General: '
overall_best_time: 'Mejor Tiempo General: '
overall_samples: 'Muestras Generales: '
overall_accuracy_label: 'Precisión General: '
branch_label: 'Rama: '
level_label: 'Nivel: '
built_in_key: 'Tecla Integrada'
unlocked_label: 'Desbloqueada: '
yes: 'Sí'
no: 'No'
in_focus_label: '¿En Foco?: '
mastery_label: 'Dominio: '
mastery_locked: 'Bloqueado'
ranked_avg_time: 'Tiempo Prom. Clasificado: '
ranked_best_time: 'Mejor Tiempo Clasificado: '
ranked_samples: 'Muestras Clasificadas: '
ranked_accuracy_label: 'Precisión Clasificada: '
# Diálogos de introducción
intro:
passage_title: ' Configurar Descarga de Pasajes '
code_title: ' Configurar Descarga de Código '
enable_downloads: 'Activar descargas de red'
download_dir: 'Directorio de descarga'
paragraphs_per_book: 'Párrafos por libro (0 = completo)'
whole_book: 'libro completo'
snippets_per_repo: 'Fragmentos por repo (0 = ilimitado)'
unlimited: 'ilimitado'
start_passage_drill: 'Iniciar ejercicio de pasaje'
start_code_drill: 'Iniciar ejercicio de código'
confirm: 'Confirmar'
hint_navigate: '[Up/Down] Navegar'
hint_adjust: '[Left/Right] Ajustar'
hint_edit: '[Type/Backspace] Editar'
hint_confirm: '[Enter] Confirmar'
hint_cancel: '[ESC] Cancelar'
preparing_download: 'Preparando descarga...'
download_passage_title: ' Descargando Fuente de Pasaje '
download_code_title: ' Descargando Fuente de Código '
book_label: ' Libro: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bytes'
downloaded_bytes: 'Descargado: %{bytes} bytes'
downloading_book_progress: 'Descargando libro actual: [%{bar}] %{downloaded}/%{total} bytes'
downloading_book_bytes: 'Descargando libro actual: %{bytes} bytes'
downloading_code_progress: 'Descargando: [%{bar}] %{downloaded}/%{total} bytes'
downloading_code_bytes: 'Descargando: %{bytes} bytes'
current_book: 'Actual: %{name} (libro %{done}/%{total})'
current_repo: 'Actual: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr puede descargar pasajes de Project Gutenberg para práctica de escritura.'
passage_instructions_2: 'Los libros se descargan una vez y se almacenan localmente.'
passage_instructions_3: 'Configura los ajustes de descarga abajo, luego inicia un ejercicio de pasaje.'
code_instructions_1: 'keydr puede descargar código abierto de GitHub para práctica de escritura.'
code_instructions_2: 'El código se descarga una vez y se almacena localmente.'
code_instructions_3: 'Configura los ajustes de descarga abajo, luego inicia un ejercicio de código.'
# Mensajes de estado (de app.rs)
status:
recovery_files: 'Se encontraron archivos de recuperación de una importación interrumpida. Los datos pueden ser inconsistentes — considera reimportar.'
dir_not_exist: 'El directorio no existe: %{path}'
no_data_store: 'No hay almacén de datos disponible'
serialization_error: 'Error de serialización: %{error}'
exported_to: 'Exportado a %{path}'
export_failed: 'Exportación fallida: %{error}'
could_not_read: 'No se pudo leer el archivo: %{error}'
invalid_export: 'Archivo de exportación inválido: %{error}'
unsupported_version: 'Versión de exportación no soportada: %{got} (se esperaba %{expected})'
import_failed: 'Importación fallida: %{error}'
imported_theme_fallback: 'Importado exitosamente (tema ''%{theme}'' no encontrado, usando predeterminado)'
imported_success: 'Importado exitosamente'
adaptive_unavailable: 'Modo adaptativo clasificado no disponible: %{error}'
switched_to: 'Cambiado a %{name}'
layout_changed: 'Distribución cambiada a %{name}'
# Errores (para traducción de límites de UI)
errors:
unknown_language: 'Idioma desconocido: %{key}'
unknown_layout: 'Distribución de teclado desconocida: %{key}'
unsupported_pair: 'Par idioma/distribución no soportado: %{language} + %{layout}'
language_blocked: 'Idioma bloqueado por nivel de soporte: %{key}'
# Común
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Volver'

454
locales/et.yml Normal file
View File

@@ -0,0 +1,454 @@
# Peamenüü
menu:
subtitle: 'Terminali trükkimise tuutor'
adaptive_drill: 'Kohanduv harjutus'
adaptive_drill_desc: 'Foneetilised sõnad kohanduva tähtede avamisega'
code_drill: 'Koodi harjutus'
code_drill_desc: 'Harjuta koodi süntaksi trükkimist'
passage_drill: 'Tekstiharjutus'
passage_drill_desc: 'Trüki lõike raamatutest'
skill_tree: 'Oskuste puu'
skill_tree_desc: 'Vaata edenemisharusid ja käivita harjutusi'
keyboard: 'Klaviatuur'
keyboard_desc: 'Uuri klahvipaigutust ja klahvistatistikat'
statistics: 'Statistika'
statistics_desc: 'Vaata oma trükkimisstatistikat'
settings: 'Seaded'
settings_desc: 'Seadista keydr'
day_streak: ' | %{days} päeva järjest'
key_progress: ' Klahvide edenemine %{unlocked}/%{total} (%{mastered} omandatud) | Siht %{target} WPM%{streak}'
hint_start: '[1-3] Alusta'
hint_skill_tree: '[t] Oskuste puu'
hint_keyboard: '[b] Klaviatuur'
hint_stats: '[s] Statistika'
hint_settings: '[c] Seaded'
hint_quit: '[q] Välju'
# Harjutuse kuva
drill:
title: ' Harjutus '
mode_adaptive: 'Kohanduv'
mode_code: 'Kood (hindamata)'
mode_passage: 'Tekst (hindamata)'
focus_char: 'Fookus: ''%{ch}'''
focus_bigram: 'Fookus: "%{bigram}"'
focus_both: 'Fookus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Täps'
header_err: 'Vead'
code_source: ' Koodi allikas '
passage_source: ' Teksti allikas '
footer: '[ESC] Lõpeta harjutus [Backspace] Kustuta'
keys_reenabled: 'Klahvid taas lubatud %{ms}ms pärast'
hint_end: '[ESC] Lõpeta harjutus'
hint_backspace: '[Backspace] Kustuta'
# Tulemuste paneel / harjutuse tulemus
dashboard:
title: ' Harjutus lõpetatud '
results: 'Tulemused'
unranked_note_prefix: ' (Hindamata'
unranked_note_suffix: ' ei lähe oskuste puu arvestusse)'
speed: ' Kiirus: '
accuracy_label: ' Täpsus: '
time_label: ' Aeg: '
errors_label: ' Vead: '
correct_detail: ' (%{correct}/%{total} õiget)'
input_blocked: ' Sisend ajutiselt blokeeritud '
input_blocked_ms: '(%{ms}ms jäänud)'
hint_continue: '[c/Enter/Space] Jätka'
hint_retry: '[r] Uuesti'
hint_menu: '[q] Menüü'
hint_stats: '[s] Statistika'
hint_delete: '[x] Kustuta'
# Statistika külgriba (harjutuse ajal)
sidebar:
title: ' Statistika '
wpm: 'WPM: '
target: 'Siht: '
target_wpm: '%{wpm} WPM'
accuracy: 'Täpsus: '
progress: 'Edenemine: '
correct: 'Õiged: '
errors: 'Vead: '
time: 'Aeg: '
last_drill: ' Viimane harjutus '
vs_avg: ' vs kesk: '
# Statistika paneel
stats:
title: ' Statistika '
empty: 'Ühtegi harjutust pole tehtud. Alusta trükkimist!'
tab_dashboard: '[1] Ülevaade'
tab_history: '[2] Ajalugu'
tab_activity: '[3] Aktiivsus'
tab_accuracy: '[4] Täpsus'
tab_timing: '[5] Ajastus'
tab_ngrams: '[6] N-grammid'
hint_back: '[ESC] Tagasi'
hint_next_tab: '[Tab] Järgmine vahekaart'
hint_switch_tab: '[1-6] Vaheta vahekaart'
hint_navigate: '[j/k] Navigeeri'
hint_page: '[PgUp/PgDn] Lehekülg'
hint_delete: '[x] Kustuta'
summary_title: ' Kokkuvõte '
drills: ' Harjutused: '
avg_wpm: ' Kesk WPM: '
best_wpm: ' Parim WPM: '
accuracy_label: ' Täpsus: '
total_time: ' Koguaeg: '
wpm_chart_title: ' WPM harjutuse kohta (viimased 20, siht: %{target}) '
accuracy_chart_title: ' Täpsus %% (viimased 50 harjutust) '
chart_drill: 'Harjutus #'
chart_accuracy_pct: 'Täpsus %%'
sessions_title: ' Hiljutised seansid '
session_header: ' # WPM Toores Täps%% Aeg Kuupäev/Aeg Režiim Hind Osaline'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Kustuta seanss #%{idx}? (y/n)'
confirm_title: ' Kinnita '
yes: 'jah'
no: 'ei'
keyboard_accuracy_title: ' Klaviatuuri täpsus %% '
keyboard_timing_title: ' Klaviatuuri ajastus (ms) '
slowest_keys_title: ' Aeglaseimad klahvid (ms) '
fastest_keys_title: ' Kiireimad klahvid (ms) '
worst_accuracy_title: ' Halvim täpsus (%%) '
best_accuracy_title: ' Parim täpsus (%%) '
not_enough_data: ' Pole piisavalt andmeid'
streaks_title: ' Seeriad '
current_streak: ' Praegune: '
best_streak: ' Parim: '
active_days: ' Aktiivsed päevad: '
top_days_none: ' Parimad päevad: puudub'
top_days: ' Parimad päevad: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Täps: %{pct}%%'
keys_label: ' Klahvid: %{unlocked}/%{total} (%{mastered} omandatud)'
ngram_empty: 'Tee mõned kohanduvad harjutused n-grammi andmete nägemiseks'
ngram_header_speed_narrow: ' Bgrm Kiir Ooda Anom%'
ngram_header_error_narrow: ' Bgrm Vead Prv Määr Ooda Anom%'
ngram_header_speed: ' Bigramm Kiirus Oodatav Proovid Anom%'
ngram_header_error: ' Bigramm Vead Proovid Määr Oodatav Anom%'
focus_title: ' Aktiivne fookus '
focus_char_label: ' Fookus: '
focus_bigram_value: 'Bigramm %{label}'
focus_plus: ' + '
anomaly_error: 'viga'
anomaly_speed: 'kiirus'
focus_detail_both: ' Märk ''%{ch}'': nõrgim klahv | Bigramm %{label}: %{type} anomaalia %{pct}%%'
focus_detail_char_only: ' Märk ''%{ch}'': nõrgim klahv, kinnitatud bigrammi anomaaliaid pole'
focus_detail_bigram_only: ' (%{type} anomaalia: %{pct}%%)'
focus_empty: ' Tee mõned kohanduvad harjutused fookuse andmete nägemiseks'
error_anomalies_title: ' Vigade anomaaliad (%{count}) '
no_error_anomalies: ' Vigade anomaaliaid ei tuvastatud'
speed_anomalies_title: ' Kiiruse anomaaliad (%{count}) '
no_speed_anomalies: ' Kiiruse anomaaliaid ei tuvastatud'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Kõhk: >%{ms}ms'
focus_char_value: 'Märk ''%{ch}'''
# Aktiivsuse soojuskaart
heatmap:
title: ' Igapäevane aktiivsus (seansid päevas) '
jan: 'Jaan'
feb: 'Veebr'
mar: 'Märts'
apr: 'Apr'
may: 'Mai'
jun: 'Juuni'
jul: 'Juuli'
aug: 'Aug'
sep: 'Sept'
oct: 'Okt'
nov: 'Nov'
dec: 'Dets'
# Diagramm
chart:
wpm_over_time: ' WPM aja jooksul '
drill_number: 'Harjutus #'
# Seaded
settings:
title: ' Seaded '
subtitle: 'Navigeerimiseks kasuta nooli, Enter/Paremale muutmiseks, ESC salvestamiseks'
target_wpm: 'Siht-WPM'
theme: 'Teema'
word_count: 'Sõnade arv'
ui_language: 'Liidese keel'
dictionary_language: 'Sõnastiku keel'
keyboard_layout: 'Klahvipaigutus'
code_language: 'Programmeerimiskeel'
code_downloads: 'Koodi allalaadimised'
on: 'Sees'
off: 'Väljas'
code_download_dir: 'Koodi allalaadimiskaust'
snippets_per_repo: 'Katkendeid repo kohta'
unlimited: 'Piiramatu'
download_code_now: 'Laadi kood alla kohe'
run_downloader: 'Käivita allalaadimine'
passage_downloads: 'Teksti allalaadimised'
passage_download_dir: 'Teksti allalaadimiskaust'
paragraphs_per_book: 'Lõike raamatu kohta'
whole_book: 'Terve raamat'
download_passages_now: 'Laadi tekstid alla kohe'
export_path: 'Ekspordi tee'
export_data: 'Ekspordi andmed'
export_now: 'Ekspordi kohe'
import_path: 'Impordi tee'
import_data: 'Impordi andmed'
import_now: 'Impordi kohe'
hint_save_back: '[ESC] Salvesta ja tagasi'
hint_change_value: '[Enter/nooled] Muuda väärtust'
hint_edit_path: '[Enter teel] Muuda'
hint_move: '[←→] Liigu'
hint_tab_complete: '[Tab] Täienda (lõpus)'
hint_confirm: '[Enter] Kinnita'
hint_cancel: '[Esc] Tühista'
success_title: ' Õnnestus '
error_title: ' Viga '
press_any_key: 'Vajuta suvalist klahvi'
file_exists_title: ' Fail on olemas '
file_exists: 'Sellel teel on juba fail olemas.'
overwrite_rename: '[d] Kirjuta üle [r] Nimeta ümber [Esc] Tühista'
erase_warning: 'See kustutab teie praegused andmed.'
export_first: 'Eksportige esmalt, kui soovite neid säilitada.'
proceed_yn: 'Jätkata? (y/n)'
confirm_import_title: ' Kinnita import '
# Valikukuvad
select:
dictionary_language_title: ' Vali sõnastiku keel '
keyboard_layout_title: ' Vali klahvipaigutus '
code_language_title: ' Vali programmeerimiskeel '
passage_source_title: ' Vali teksti allikas '
ui_language_title: ' Vali liidese keel '
more_above: '... veel %{count} üleval ...'
more_below: '... veel %{count} all ...'
current: ' (praegune)'
disabled: ' (keelatud)'
enabled_default: ' (lubatud, vaikimisi: %{layout})'
enabled: ' (lubatud)'
disabled_blocked: ' (keelatud: blokeeritud)'
built_in: ' (sisseehitatud)'
cached: ' (puhverdatud)'
disabled_download: ' (keelatud: allalaadimine vajalik)'
download_required: ' (allalaadimine vajalik)'
hint_navigate: '[Üles/Alla/PgUp/PgDn] Navigeeri'
hint_confirm: '[Enter] Kinnita'
hint_back: '[ESC] Tagasi'
language_resets_layout: 'Keele valimine lähtestab klahvipaigutuse selle keele vaikimisi paigutusele.'
layout_no_language_change: 'Paigutuse muutmine ei muuda sõnastiku keelt.'
disabled_network_notice: 'Mõned keeled on keelatud: lubage võrgu allalaadimised sissejuhatuses/seadetes.'
disabled_sources_notice: 'Mõned allikad on keelatud: lubage võrgu allalaadimised sissejuhatuses/seadetes.'
passage_all: 'Kõik (sisseehitatud + kõik raamatud)'
passage_builtin: 'Ainult sisseehitatud tekstid'
passage_book_prefix: 'Raamat: %{title}'
# Edenemine
progress:
overall_key_progress: 'Üldine klahvide edenemine'
unlocked_mastered: '%{unlocked}/%{total} avatud (%{mastered} omandatud)'
# Oskuste puu
skill_tree:
title: ' Oskuste puu '
locked: 'Lukus'
unlocked: 'avatud'
mastered: 'omandatud'
in_progress: 'pooleli'
complete: 'lõpetatud'
locked_status: 'lukus'
locked_notice: 'Lõpeta %{count} põhitähte harude avamiseks'
branches_separator: 'Harud (saadaval pärast %{count} põhitähte)'
unlocked_letters: 'Avatud %{unlocked}/%{total} tähte'
level: 'Tase %{current}/%{total}'
level_zero: 'Tase 0/%{total}'
in_focus: ' fookuses'
hint_navigate: '[↑↓/jk] Navigeeri'
hint_scroll: '[PgUp/PgDn või Ctrl+U/Ctrl+D] Keri'
hint_back: '[q] Tagasi'
hint_unlock: '[Enter] Ava'
hint_start_drill: '[Enter] Alusta harjutust'
unlock_msg_1: 'Pärast avamist segab vaikimisi kohanduv harjutus selle haru avatud klahve.'
unlock_msg_2: 'Kui soovite keskenduda ainult sellele harule, käivitage harjutus otse oskuste puust.'
confirm_unlock: 'Avada %{branch}?'
confirm_yn: '[y] Ava [n/ESC] Tühista'
lvl_prefix: 'Tase'
branch_primary_letters: 'Põhitähed'
branch_capital_letters: 'Suurtähed'
branch_numbers: 'Numbrid 0-9'
branch_prose_punctuation: 'Kirjavahemärgid'
branch_whitespace: 'Tühimärgid'
branch_code_symbols: 'Koodi sümbolid'
level_frequency_order: 'Sageduse järjekord'
level_common_sentence_capitals: 'Tavalised lause suurtähed'
level_name_capitals: 'Nimede suurtähed'
level_remaining_capitals: 'Ülejäänud suurtähed'
level_common_digits: 'Tavalised numbrid'
level_all_digits: 'Kõik numbrid'
level_essential: 'Hädavajalik'
level_common: 'Tavaline'
level_expressive: 'Väljenduslik'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Taane'
level_arithmetic_assignment: 'Aritmeetika ja omistamine'
level_grouping: 'Rühmitamine'
level_logic_reference: 'Loogika ja viitamine'
level_special: 'Eriline'
# Verstapostid
milestones:
unlock_title: ' Klahv avatud! '
mastery_title: ' Klahv omandatud! '
branches_title: ' Uued oskuste harud saadaval! '
branch_complete_title: ' Haru lõpetatud! '
all_unlocked_title: ' Kõik klahvid avatud! '
all_mastered_title: ' Täielik klaviatuuri valdamine! '
unlocked: 'avatud'
mastered: 'omandatud'
use_finger: 'Kasuta oma %{finger}'
hold_right_shift: 'Hoia paremat Shifti (parem väike sõrm)'
hold_left_shift: 'Hoia vasakut Shifti (vasak väike sõrm)'
congratulations_all_letters: 'Palju õnne! Olete omandanud kõik %{count} põhitähte'
new_branches_available: 'Uued oskuste harud on nüüd saadaval:'
visit_skill_tree: 'Külastage oskuste puud uue haru avamiseks'
and_start_training: 'ja alustage treenimist!'
open_skill_tree: 'Vajutage [t] oskuste puu avamiseks'
branch_complete_msg: 'Olete lõpetanud haru %{branch}!'
all_levels_mastered: 'Kõik %{count} taset omandatud.'
all_keys_confident: 'Iga klahv selles harus on täielikul tasemel.'
all_unlocked_msg: 'Olete avanud kõik klahvid klaviatuuril!'
all_unlocked_desc: 'Iga märk, sümbol ja muuteklahv on nüüd harjutustes saadaval.'
keep_practicing_mastery: 'Jätkake harjutamist valdamise saavutamiseks — kui iga klahv jõuab täieliku'
confidence_complete: 'kindluseni, olete saavutanud täieliku klaviatuuri valdamise!'
all_mastered_msg: 'Palju õnne — olete saavutanud täieliku klaviatuuri valdamise!'
all_mastered_desc: 'Iga klahv klaviatuuril on maksimaalsel tasemel.'
mastery_takes_practice: 'Valdamine pole sihtkoht — see nõuab pidevat harjutamist.'
keep_drilling: 'Jätkake harjutamist oma taseme hoidmiseks.'
hint_skill_tree_continue: '[t] Ava oskuste puu [Suvaline klahv] Jätka'
hint_any_key: 'Vajuta suvalist klahvi jätkamiseks'
input_blocked: 'Sisend ajutiselt blokeeritud (%{ms}ms jäänud)'
unlock_msg_1: 'Tubli! Jätkake oma trükkimisoskuste arendamist.'
unlock_msg_2: 'Veel üks klahv teie arsenali!'
unlock_msg_3: 'Teie klaviatuur kasvab! Jätkake!'
unlock_msg_4: 'Samm lähemale täielikule klaviatuuri valdamisele!'
mastery_msg_1: 'See klahv on nüüd täielikul tasemel!'
mastery_msg_2: 'See klahv on teil selge!'
mastery_msg_3: 'Lihasmälu lukustatud!'
mastery_msg_4: 'Veel üks klahv vallutatud!'
# Klaviatuuri uurija
keyboard:
title: ' Klaviatuur '
subtitle: 'Vajuta suvalist klahvi või klõpsa klahvi'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigeeri'
hint_back: '[q/ESC] Tagasi'
key_label: 'Klahv: '
finger_label: 'Sõrm: '
hand_left: 'Vasak'
hand_right: 'Parem'
finger_index: 'Nimetissõrm'
finger_middle: 'Keskmine sõrm'
finger_ring: 'Nimesõrm'
finger_pinky: 'Väike sõrm'
finger_thumb: 'Pöial'
overall_accuracy: ' Üldine täpsus: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Hinnatud täpsus: %{correct}/%{total} (%{pct}%%)'
confidence: 'Kindlus: '
no_data: 'Andmed puuduvad'
no_data_short: 'Andmeid pole'
key_details: ' Klahvi üksikasjad '
key_details_char: ' Klahvi üksikasjad: ''%{ch}'' '
key_details_name: ' Klahvi üksikasjad: %{name} '
press_key_hint: 'Vajuta klahvi üksikasjade nägemiseks'
shift_label: 'Shift: '
shift_no: 'Ei'
overall_avg_time: 'Üldine kesk. aeg: '
overall_best_time: 'Üldine parim aeg: '
overall_samples: 'Üldised proovid: '
overall_accuracy_label: 'Üldine täpsus: '
branch_label: 'Haru: '
level_label: 'Tase: '
built_in_key: 'Sisseehitatud klahv'
unlocked_label: 'Avatud: '
yes: 'Jah'
no: 'Ei'
in_focus_label: 'Fookuses?: '
mastery_label: 'Valdamine: '
mastery_locked: 'Lukus'
ranked_avg_time: 'Hinnatud kesk. aeg: '
ranked_best_time: 'Hinnatud parim aeg: '
ranked_samples: 'Hinnatud proovid: '
ranked_accuracy_label: 'Hinnatud täpsus: '
# Sissejuhatuse dialoogid
intro:
passage_title: ' Teksti allalaadimise seadistus '
code_title: ' Koodi allalaadimise seadistus '
enable_downloads: 'Luba võrgu allalaadimised'
download_dir: 'Allalaadimiskaust'
paragraphs_per_book: 'Lõike raamatu kohta (0 = terve)'
whole_book: 'terve raamat'
snippets_per_repo: 'Katkendeid repo kohta (0 = piiramatu)'
unlimited: 'piiramatu'
start_passage_drill: 'Alusta tekstiharjutust'
start_code_drill: 'Alusta koodi harjutust'
confirm: 'Kinnita'
hint_navigate: '[Üles/Alla] Navigeeri'
hint_adjust: '[Vasakule/Paremale] Kohanda'
hint_edit: '[Trüki/Backspace] Muuda'
hint_confirm: '[Enter] Kinnita'
hint_cancel: '[ESC] Tühista'
preparing_download: 'Valmistan allalaadimist ette...'
download_passage_title: ' Teksti allika allalaadimine '
download_code_title: ' Koodi allika allalaadimine '
book_label: ' Raamat: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} baiti'
downloaded_bytes: 'Alla laaditud: %{bytes} baiti'
downloading_book_progress: 'Laadin raamatut: [%{bar}] %{downloaded}/%{total} baiti'
downloading_book_bytes: 'Laadin raamatut: %{bytes} baiti'
downloading_code_progress: 'Laadin alla: [%{bar}] %{downloaded}/%{total} baiti'
downloading_code_bytes: 'Laadin alla: %{bytes} baiti'
current_book: 'Praegune: %{name} (raamat %{done}/%{total})'
current_repo: 'Praegune: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr saab alla laadida lõike Project Gutenbergist trükkimisharjutuseks.'
passage_instructions_2: 'Raamatud laaditakse alla üks kord ja salvestatakse kohalikult.'
passage_instructions_3: 'Seadistage allalaadimised allpool, seejärel alustage tekstiharjutust.'
code_instructions_1: 'keydr saab alla laadida avatud lähtekoodiga koodi GitHubist trükkimisharjutuseks.'
code_instructions_2: 'Kood laaditakse alla üks kord ja salvestatakse kohalikult.'
code_instructions_3: 'Seadistage allalaadimised allpool, seejärel alustage koodi harjutust.'
# Olekuteated (failist app.rs)
status:
recovery_files: 'Leitud taastefailid katkestatud impordist. Andmed võivad olla ebajärjekindlad — kaaluge uuesti importimist.'
dir_not_exist: 'Kausta ei eksisteeri: %{path}'
no_data_store: 'Andmehoidla pole saadaval'
serialization_error: 'Serialiseerimisviga: %{error}'
exported_to: 'Eksporditud asukohta %{path}'
export_failed: 'Eksport ebaõnnestus: %{error}'
could_not_read: 'Faili ei saanud lugeda: %{error}'
invalid_export: 'Vigane ekspordifail: %{error}'
unsupported_version: 'Toetamata ekspordi versioon: %{got} (oodatud %{expected})'
import_failed: 'Import ebaõnnestus: %{error}'
imported_theme_fallback: 'Imporditud edukalt (teemat ''%{theme}'' ei leitud, kasutan vaikimisi)'
imported_success: 'Imporditud edukalt'
adaptive_unavailable: 'Kohanduv hinnatud režiim pole saadaval: %{error}'
switched_to: 'Lülitatud režiimile %{name}'
layout_changed: 'Paigutus muudetud: %{name}'
# Vead (liidese piiri tõlke jaoks)
errors:
unknown_language: 'Tundmatu keel: %{key}'
unknown_layout: 'Tundmatu klahvipaigutus: %{key}'
unsupported_pair: 'Toetamata keele/paigutuse paar: %{language} + %{layout}'
language_blocked: 'Keel on blokeeritud toe taseme tõttu: %{key}'
# Üldine
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Tagasi'

454
locales/fi.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminaalin kirjoitusharjoittelija'
adaptive_drill: 'Mukautuva harjoitus'
adaptive_drill_desc: 'Foneettiset sanat mukautuvalla kirjainten avauksella'
code_drill: 'Koodiharjoitus'
code_drill_desc: 'Harjoittele koodisyntaksin kirjoittamista'
passage_drill: 'Tekstiharjoitus'
passage_drill_desc: 'Kirjoita katkelmia kirjoista'
skill_tree: 'Taitopuu'
skill_tree_desc: 'Tarkastele etenemispolkuja ja aloita harjoituksia'
keyboard: 'Näppäimistö'
keyboard_desc: 'Tutustu näppäinasetteluun ja tilastoihin'
statistics: 'Tilastot'
statistics_desc: 'Tarkastele kirjoitustilastojasi'
settings: 'Asetukset'
settings_desc: 'Määritä keydr-asetukset'
day_streak: ' | %{days} päivän putki'
key_progress: ' Näppäinedistyminen %{unlocked}/%{total} (%{mastered} hallittu) | Tavoite %{target} WPM%{streak}'
hint_start: '[1-3] Aloita'
hint_skill_tree: '[t] Taitopuu'
hint_keyboard: '[b] Näppäimistö'
hint_stats: '[s] Tilastot'
hint_settings: '[c] Asetukset'
hint_quit: '[q] Lopeta'
# Drill screen
drill:
title: ' Harjoitus '
mode_adaptive: 'Mukautuva'
mode_code: 'Koodi (ei sijoitettu)'
mode_passage: 'Teksti (ei sijoitettu)'
focus_char: 'Fokus: ''%{ch}'''
focus_bigram: 'Fokus: "%{bigram}"'
focus_both: 'Fokus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Tark'
header_err: 'Virh'
code_source: ' Koodilähde '
passage_source: ' Tekstilähde '
footer: '[ESC] Lopeta harjoitus [Backspace] Poista'
keys_reenabled: 'Näppäimet palautettu %{ms}ms:ssa'
hint_end: '[ESC] Lopeta harjoitus'
hint_backspace: '[Backspace] Poista'
# Dashboard / drill result
dashboard:
title: ' Harjoitus valmis '
results: 'Tulokset'
unranked_note_prefix: ' (Ei sijoitettu'
unranked_note_suffix: ' ei lasketa taitopuuhun)'
speed: ' Nopeus: '
accuracy_label: ' Tarkkuus: '
time_label: ' Aika: '
errors_label: ' Virheet: '
correct_detail: ' (%{correct}/%{total} oikein)'
input_blocked: ' Syöte estetty väliaikaisesti '
input_blocked_ms: '(%{ms}ms jäljellä)'
hint_continue: '[c/Enter/Space] Jatka'
hint_retry: '[r] Uudelleen'
hint_menu: '[q] Valikko'
hint_stats: '[s] Tilastot'
hint_delete: '[x] Poista'
# Stats sidebar (during drill)
sidebar:
title: ' Tilastot '
wpm: 'WPM: '
target: 'Tavoite: '
target_wpm: '%{wpm} WPM'
accuracy: 'Tarkkuus: '
progress: 'Edistyminen: '
correct: 'Oikein: '
errors: 'Virheet: '
time: 'Aika: '
last_drill: ' Edellinen harjoitus '
vs_avg: ' vs ka: '
# Statistics dashboard
stats:
title: ' Tilastot '
empty: 'Ei harjoituksia vielä. Aloita kirjoittaminen!'
tab_dashboard: '[1] Yhteenveto'
tab_history: '[2] Historia'
tab_activity: '[3] Aktiivisuus'
tab_accuracy: '[4] Tarkkuus'
tab_timing: '[5] Ajoitus'
tab_ngrams: '[6] N-grammit'
hint_back: '[ESC] Takaisin'
hint_next_tab: '[Tab] Seuraava välilehti'
hint_switch_tab: '[1-6] Vaihda välilehteä'
hint_navigate: '[j/k] Navigoi'
hint_page: '[PgUp/PgDn] Sivu'
hint_delete: '[x] Poista'
summary_title: ' Yhteenveto '
drills: ' Harjoitukset: '
avg_wpm: ' Ka WPM: '
best_wpm: ' Paras WPM: '
accuracy_label: ' Tarkkuus: '
total_time: ' Kokonaisaika: '
wpm_chart_title: ' WPM per harjoitus (viimeiset 20, tavoite: %{target}) '
accuracy_chart_title: ' Tarkkuus %% (viimeiset 50 harjoitusta) '
chart_drill: 'Harj #'
chart_accuracy_pct: 'Tarkkuus %%'
sessions_title: ' Viimeisimmät istunnot '
session_header: ' # WPM Raw Tark%% Aika Pvm/Aika Tila Sijoitettu Ositt.'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Poista istunto #%{idx}? (k/e)'
confirm_title: ' Vahvista '
yes: 'kyllä'
no: 'ei'
keyboard_accuracy_title: ' Näppäimistön tarkkuus %% '
keyboard_timing_title: ' Näppäimistön ajoitus (ms) '
slowest_keys_title: ' Hitaimmat näppäimet (ms) '
fastest_keys_title: ' Nopeimmat näppäimet (ms) '
worst_accuracy_title: ' Heikoin tarkkuus (%%) '
best_accuracy_title: ' Paras tarkkuus (%%) '
not_enough_data: ' Ei tarpeeksi dataa'
streaks_title: ' Putket '
current_streak: ' Nykyinen: '
best_streak: ' Paras: '
active_days: ' Aktiiviset päivät: '
top_days_none: ' Parhaat päivät: ei yhtään'
top_days: ' Parhaat päivät: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Tark: %{pct}%%'
keys_label: ' Näppäimet: %{unlocked}/%{total} (%{mastered} hallittu)'
ngram_empty: 'Suorita mukautuvia harjoituksia nähdäksesi n-grammidataa'
ngram_header_speed_narrow: ' Bgrm Nop Odot Poikk%'
ngram_header_error_narrow: ' Bgrm Virh Näyt Tih Odot Poikk%'
ngram_header_speed: ' Bigrammi Nopeus Odotettu Näytteet Poikk%'
ngram_header_error: ' Bigrammi Virheet Näytteet Tiheys Odotettu Poikk%'
focus_title: ' Aktiivinen fokus '
focus_char_label: ' Fokus: '
focus_bigram_value: 'Bigrammi %{label}'
focus_plus: ' + '
anomaly_error: 'virhe'
anomaly_speed: 'nopeus'
focus_detail_both: ' Merkki ''%{ch}'': heikoin näppäin | Bigrammi %{label}: %{type}-poikkeama %{pct}%%'
focus_detail_char_only: ' Merkki ''%{ch}'': heikoin näppäin, ei vahvistettuja bigrammipoikkeamia'
focus_detail_bigram_only: ' (%{type}-poikkeama: %{pct}%%)'
focus_empty: ' Suorita mukautuvia harjoituksia nähdäksesi fokusdataa'
error_anomalies_title: ' Virhepoikkeamat (%{count}) '
no_error_anomalies: ' Virhepoikkeamia ei havaittu'
speed_anomalies_title: ' Nopeuspoikkeamat (%{count}) '
no_speed_anomalies: ' Nopeuspoikkeamia ei havaittu'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Epär: >%{ms}ms'
focus_char_value: 'Merkki ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Päivittäinen aktiivisuus (istunnot per päivä) '
jan: 'Tam'
feb: 'Hel'
mar: 'Maa'
apr: 'Huh'
may: 'Tou'
jun: 'Kes'
jul: 'Hei'
aug: 'Elo'
sep: 'Syy'
oct: 'Lok'
nov: 'Mar'
dec: 'Jou'
# Chart
chart:
wpm_over_time: ' WPM ajan kuluessa '
drill_number: 'Harj #'
# Settings
settings:
title: ' Asetukset '
subtitle: 'Käytä nuolia navigointiin, Enter/oikea muuttaa, ESC tallentaa ja poistuu'
target_wpm: 'Tavoite WPM'
theme: 'Teema'
word_count: 'Sanamäärä'
ui_language: 'Käyttöliittymän kieli'
dictionary_language: 'Sanakirjan kieli'
keyboard_layout: 'Näppäinasettelu'
code_language: 'Ohjelmointikieli'
code_downloads: 'Koodilataukset'
on: 'Päällä'
off: 'Pois'
code_download_dir: 'Koodilatauskansio'
snippets_per_repo: 'Katkelmat per repo'
unlimited: 'Rajaton'
download_code_now: 'Lataa koodi nyt'
run_downloader: 'Käynnistä lataaja'
passage_downloads: 'Tekstilataukset'
passage_download_dir: 'Tekstilatauskansio'
paragraphs_per_book: 'Kappaleet per kirja'
whole_book: 'Koko kirja'
download_passages_now: 'Lataa tekstit nyt'
export_path: 'Vientipolku'
export_data: 'Vie data'
export_now: 'Vie nyt'
import_path: 'Tuontipolku'
import_data: 'Tuo data'
import_now: 'Tuo nyt'
hint_save_back: '[ESC] Tallenna ja takaisin'
hint_change_value: '[Enter/nuolet] Muuta arvoa'
hint_edit_path: '[Enter polulla] Muokkaa'
hint_move: '[←→] Siirrä'
hint_tab_complete: '[Tab] Täydennä (lopussa)'
hint_confirm: '[Enter] Vahvista'
hint_cancel: '[Esc] Peruuta'
success_title: ' Onnistui '
error_title: ' Virhe '
press_any_key: 'Paina mitä tahansa näppäintä'
file_exists_title: ' Tiedosto on olemassa '
file_exists: 'Tiedosto on jo olemassa tässä polussa.'
overwrite_rename: '[d] Korvaa [r] Nimeä uudelleen [Esc] Peruuta'
erase_warning: 'Tämä poistaa nykyisen datasi.'
export_first: 'Vie data ensin, jos haluat säilyttää sen.'
proceed_yn: 'Jatketaanko? (k/e)'
confirm_import_title: ' Vahvista tuonti '
# Selection screens
select:
dictionary_language_title: ' Valitse sanakirjan kieli '
keyboard_layout_title: ' Valitse näppäinasettelu '
code_language_title: ' Valitse ohjelmointikieli '
passage_source_title: ' Valitse tekstilähde '
ui_language_title: ' Valitse käyttöliittymän kieli '
more_above: '... %{count} lisää ylhäällä ...'
more_below: '... %{count} lisää alhaalla ...'
current: ' (nykyinen)'
disabled: ' (pois käytöstä)'
enabled_default: ' (käytössä, oletus: %{layout})'
enabled: ' (käytössä)'
disabled_blocked: ' (pois käytöstä: estetty)'
built_in: ' (sisäänrakennettu)'
cached: ' (välimuistissa)'
disabled_download: ' (pois käytöstä: lataus vaaditaan)'
download_required: ' (lataus vaaditaan)'
hint_navigate: '[Ylös/Alas/PgUp/PgDn] Navigoi'
hint_confirm: '[Enter] Vahvista'
hint_back: '[ESC] Takaisin'
language_resets_layout: 'Kielen valinta palauttaa näppäinasettelun kielen oletukseen.'
layout_no_language_change: 'Asettelun muutos ei vaihda sanakirjan kieltä.'
disabled_network_notice: 'Jotkin kielet ovat pois käytöstä: ota verkkolataukset käyttöön asetuksissa.'
disabled_sources_notice: 'Jotkin lähteet ovat pois käytöstä: ota verkkolataukset käyttöön asetuksissa.'
passage_all: 'Kaikki (sisäänrakennetut + kaikki kirjat)'
passage_builtin: 'Vain sisäänrakennetut tekstit'
passage_book_prefix: 'Kirja: %{title}'
# Progress
progress:
overall_key_progress: 'Yleinen näppäinedistyminen'
unlocked_mastered: '%{unlocked}/%{total} avattu (%{mastered} hallittu)'
# Skill tree
skill_tree:
title: ' Taitopuu '
locked: 'Lukittu'
unlocked: 'avattu'
mastered: 'hallittu'
in_progress: 'käynnissä'
complete: 'valmis'
locked_status: 'lukittu'
locked_notice: 'Suorita %{count} peruskirjainta avataksesi haarat'
branches_separator: 'Haarat (käytettävissä %{count} peruskirjaimen jälkeen)'
unlocked_letters: 'Avattu %{unlocked}/%{total} kirjainta'
level: 'Taso %{current}/%{total}'
level_zero: 'Taso 0/%{total}'
in_focus: ' fokuksessa'
hint_navigate: '[↑↓/jk] Navigoi'
hint_scroll: '[PgUp/PgDn tai Ctrl+U/Ctrl+D] Vieritä'
hint_back: '[q] Takaisin'
hint_unlock: '[Enter] Avaa'
hint_start_drill: '[Enter] Aloita harjoitus'
unlock_msg_1: 'Avaamisen jälkeen mukautuva oletusharjoitus sisällyttää tämän haaran avattuja näppäimiä.'
unlock_msg_2: 'Jos haluat keskittyä vain tähän haaraan, aloita harjoitus suoraan taitopuusta.'
confirm_unlock: 'Avaa %{branch}?'
confirm_yn: '[y] Avaa [n/ESC] Peruuta'
lvl_prefix: 'Taso'
branch_primary_letters: 'Peruskirjaimet'
branch_capital_letters: 'Isot kirjaimet'
branch_numbers: 'Numerot 0-9'
branch_prose_punctuation: 'Välimerkit'
branch_whitespace: 'Tyhjämerkit'
branch_code_symbols: 'Koodisymbolit'
level_frequency_order: 'Yleisyysjärjestys'
level_common_sentence_capitals: 'Yleiset lauseen isot kirjaimet'
level_name_capitals: 'Nimien isot kirjaimet'
level_remaining_capitals: 'Loput isot kirjaimet'
level_common_digits: 'Yleiset numerot'
level_all_digits: 'Kaikki numerot'
level_essential: 'Välttämättömät'
level_common: 'Yleiset'
level_expressive: 'Ilmaisevat'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/sisennys'
level_arithmetic_assignment: 'Laskutoimitukset ja sijoitus'
level_grouping: 'Ryhmittely'
level_logic_reference: 'Logiikka ja viittaus'
level_special: 'Erikoismerkit'
# Milestones
milestones:
unlock_title: ' Näppäin avattu! '
mastery_title: ' Näppäin hallittu! '
branches_title: ' Uusia taitohaarat käytettävissä! '
branch_complete_title: ' Haara valmis! '
all_unlocked_title: ' Kaikki näppäimet avattu! '
all_mastered_title: ' Täysi näppäimistöhallinta! '
unlocked: 'avattu'
mastered: 'hallittu'
use_finger: 'Käytä %{finger}asi'
hold_right_shift: 'Pidä oikeaa Shiftiä (oikea pikkusormi)'
hold_left_shift: 'Pidä vasenta Shiftiä (vasen pikkusormi)'
congratulations_all_letters: 'Onnittelut! Olet hallinnut kaikki %{count} peruskirjainta'
new_branches_available: 'Uusia taitohaaroja on nyt käytettävissä:'
visit_skill_tree: 'Käy taitopuussa avataksesi uuden haaran'
and_start_training: 'ja aloita harjoittelu!'
open_skill_tree: 'Paina [t] avataksesi taitopuun nyt'
branch_complete_msg: 'Olet suorittanut haaran %{branch}!'
all_levels_mastered: 'Kaikki %{count} tasoa hallittu.'
all_keys_confident: 'Jokainen näppäin tässä haarassa on täydellä varmuudella.'
all_unlocked_msg: 'Olet avannut jokaisen näppäimen näppäimistöllä!'
all_unlocked_desc: 'Jokainen merkki, symboli ja muokkain on nyt käytettävissä harjoituksissasi.'
keep_practicing_mastery: 'Jatka harjoittelua hallinnan rakentamiseksi — kun jokainen näppäin saavuttaa täyden'
confidence_complete: 'varmuuden, olet saavuttanut täydellisen näppäimistöhallinnan!'
all_mastered_msg: 'Onnittelut — olet saavuttanut täyden näppäimistöhallinnan!'
all_mastered_desc: 'Jokainen näppäin näppäimistöllä on maksimivarmuudella.'
mastery_takes_practice: 'Hallinta ei ole päämäärä — se vaatii jatkuvaa harjoittelua.'
keep_drilling: 'Jatka harjoittelua säilyttääksesi taitosi.'
hint_skill_tree_continue: '[t] Avaa taitopuu [Muu näppäin] Jatka'
hint_any_key: 'Paina mitä tahansa näppäintä jatkaaksesi'
input_blocked: 'Syöte estetty väliaikaisesti (%{ms}ms jäljellä)'
unlock_msg_1: 'Hienoa! Jatka kirjoitustaitojesi kehittämistä.'
unlock_msg_2: 'Taas yksi näppäin arsenaalissasi!'
unlock_msg_3: 'Näppäimistösi kasvaa! Jatka samaan malliin.'
unlock_msg_4: 'Askel lähempänä täyttä näppäimistöhallintaa!'
mastery_msg_1: 'Tämä näppäin on nyt täydellä varmuudella!'
mastery_msg_2: 'Tämä näppäin on hallussa!'
mastery_msg_3: 'Lihasmuisti lukittuna!'
mastery_msg_4: 'Taas yksi näppäin valloitettu!'
# Keyboard explorer
keyboard:
title: ' Näppäimistö '
subtitle: 'Paina mitä tahansa näppäintä tai klikkaa näppäintä'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigoi'
hint_back: '[q/ESC] Takaisin'
key_label: 'Näppäin: '
finger_label: 'Sormi: '
hand_left: 'Vasen'
hand_right: 'Oikea'
finger_index: 'Etusormi'
finger_middle: 'Keskisormi'
finger_ring: 'Nimetön'
finger_pinky: 'Pikkusormi'
finger_thumb: 'Peukalo'
overall_accuracy: ' Kokonaistarkkuus: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Sijoitettu tarkkuus: %{correct}/%{total} (%{pct}%%)'
confidence: 'Varmuus: '
no_data: 'Ei dataa vielä'
no_data_short: 'Ei dataa'
key_details: ' Näppäintiedot '
key_details_char: ' Näppäintiedot: ''%{ch}'' '
key_details_name: ' Näppäintiedot: %{name} '
press_key_hint: 'Paina näppäintä nähdäksesi sen tiedot'
shift_label: 'Shift: '
shift_no: 'Ei'
overall_avg_time: 'Keskimääräinen aika: '
overall_best_time: 'Paras aika: '
overall_samples: 'Näytteet: '
overall_accuracy_label: 'Kokonaistarkkuus: '
branch_label: 'Haara: '
level_label: 'Taso: '
built_in_key: 'Sisäänrakennettu näppäin'
unlocked_label: 'Avattu: '
yes: 'Kyllä'
no: 'Ei'
in_focus_label: 'Fokuksessa?: '
mastery_label: 'Hallinta: '
mastery_locked: 'Lukittu'
ranked_avg_time: 'Sijoitettu ka aika: '
ranked_best_time: 'Sijoitettu paras aika: '
ranked_samples: 'Sijoitetut näytteet: '
ranked_accuracy_label: 'Sijoitettu tarkkuus: '
# Intro dialogs
intro:
passage_title: ' Tekstilatausten asetukset '
code_title: ' Koodilatausten asetukset '
enable_downloads: 'Ota verkkolataukset käyttöön'
download_dir: 'Latauskansio'
paragraphs_per_book: 'Kappaleet per kirja (0 = koko)'
whole_book: 'koko kirja'
snippets_per_repo: 'Katkelmat per repo (0 = rajaton)'
unlimited: 'rajaton'
start_passage_drill: 'Aloita tekstiharjoitus'
start_code_drill: 'Aloita koodiharjoitus'
confirm: 'Vahvista'
hint_navigate: '[Ylös/Alas] Navigoi'
hint_adjust: '[Vasen/Oikea] Säädä'
hint_edit: '[Kirjoita/Backspace] Muokkaa'
hint_confirm: '[Enter] Vahvista'
hint_cancel: '[ESC] Peruuta'
preparing_download: 'Valmistellaan latausta...'
download_passage_title: ' Ladataan tekstilähdettä '
download_code_title: ' Ladataan koodilähdettä '
book_label: ' Kirja: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} tavua'
downloaded_bytes: 'Ladattu: %{bytes} tavua'
downloading_book_progress: 'Ladataan kirjaa: [%{bar}] %{downloaded}/%{total} tavua'
downloading_book_bytes: 'Ladataan kirjaa: %{bytes} tavua'
downloading_code_progress: 'Ladataan: [%{bar}] %{downloaded}/%{total} tavua'
downloading_code_bytes: 'Ladataan: %{bytes} tavua'
current_book: 'Nykyinen: %{name} (kirja %{done}/%{total})'
current_repo: 'Nykyinen: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr voi ladata tekstejä Project Gutenbergistä kirjoitusharjoitteluun.'
passage_instructions_2: 'Kirjat ladataan kerran ja tallennetaan paikallisesti.'
passage_instructions_3: 'Määritä latausasetukset alla ja aloita tekstiharjoitus.'
code_instructions_1: 'keydr voi ladata avoimen lähdekoodin koodia GitHubista kirjoitusharjoitteluun.'
code_instructions_2: 'Koodi ladataan kerran ja tallennetaan paikallisesti.'
code_instructions_3: 'Määritä latausasetukset alla ja aloita koodiharjoitus.'
# Status messages (from app.rs)
status:
recovery_files: 'Palautustiedostoja löydetty keskeytyneestä tuonnista. Data voi olla epäjohdonmukaista — harkitse uudelleentuontia.'
dir_not_exist: 'Kansiota ei ole olemassa: %{path}'
no_data_store: 'Datavarasto ei käytettävissä'
serialization_error: 'Sarjallistamisvirhe: %{error}'
exported_to: 'Viety kohteeseen %{path}'
export_failed: 'Vienti epäonnistui: %{error}'
could_not_read: 'Tiedostoa ei voitu lukea: %{error}'
invalid_export: 'Virheellinen vientitiedosto: %{error}'
unsupported_version: 'Ei-tuettu vientiversion: %{got} (odotettu %{expected})'
import_failed: 'Tuonti epäonnistui: %{error}'
imported_theme_fallback: 'Tuotu onnistuneesti (teemaa ''%{theme}'' ei löytynyt, käytetään oletusta)'
imported_success: 'Tuotu onnistuneesti'
adaptive_unavailable: 'Mukautuva sijoitettu tila ei käytettävissä: %{error}'
switched_to: 'Vaihdettu: %{name}'
layout_changed: 'Asettelu vaihdettu: %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Tuntematon kieli: %{key}'
unknown_layout: 'Tuntematon näppäinasettelu: %{key}'
unsupported_pair: 'Ei-tuettu kieli/asettelu-pari: %{language} + %{layout}'
language_blocked: 'Kieli estetty tukitason vuoksi: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Takaisin'

454
locales/fr.yml Normal file
View File

@@ -0,0 +1,454 @@
# Menu principal
menu:
subtitle: 'Tuteur de Frappe en Terminal'
adaptive_drill: 'Exercice Adaptatif'
adaptive_drill_desc: 'Mots phonétiques avec déverrouillage adaptatif des touches'
code_drill: 'Exercice de Code'
code_drill_desc: 'Entraînez-vous à taper la syntaxe du code'
passage_drill: 'Exercice de Passage'
passage_drill_desc: 'Tapez des passages de livres'
skill_tree: 'Arbre de Compétences'
skill_tree_desc: 'Voir les branches de progression et lancer des exercices'
keyboard: 'Clavier'
keyboard_desc: 'Explorer la disposition du clavier et les statistiques'
statistics: 'Statistiques'
statistics_desc: 'Voir vos statistiques de frappe'
settings: 'Paramètres'
settings_desc: 'Configurer keydr'
day_streak: ' | %{days} jours consécutifs'
key_progress: ' Progression %{unlocked}/%{total} (%{mastered} maîtrisées) | Objectif %{target} WPM%{streak}'
hint_start: '[1-3] Démarrer'
hint_skill_tree: '[t] Arbre de Compétences'
hint_keyboard: '[b] Clavier'
hint_stats: '[s] Statistiques'
hint_settings: '[c] Paramètres'
hint_quit: '[q] Quitter'
# Écran d'exercice
drill:
title: ' Exercice '
mode_adaptive: 'Adaptatif'
mode_code: 'Code (Non classé)'
mode_passage: 'Passage (Non classé)'
focus_char: 'Focus : ''%{ch}'''
focus_bigram: 'Focus : "%{bigram}"'
focus_both: 'Focus : ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Pré'
header_err: 'Err'
code_source: ' Source du code '
passage_source: ' Source du passage '
footer: '[ESC] Fin [Backspace] Effacer'
keys_reenabled: 'Touches réactivées en %{ms}ms'
hint_end: '[ESC] Fin de l''exercice'
hint_backspace: '[Backspace] Effacer'
# Tableau de bord / résultat de l'exercice
dashboard:
title: ' Exercice Terminé '
results: 'Résultats'
unranked_note_prefix: ' (Non classé'
unranked_note_suffix: ' ne compte pas pour l''arbre de compétences)'
speed: ' Vitesse : '
accuracy_label: ' Précision : '
time_label: ' Temps : '
errors_label: ' Erreurs : '
correct_detail: ' (%{correct}/%{total} corrects)'
input_blocked: ' Saisie temporairement bloquée '
input_blocked_ms: '(%{ms}ms restantes)'
hint_continue: '[c/Enter/Space] Continuer'
hint_retry: '[r] Réessayer'
hint_menu: '[q] Menu'
hint_stats: '[s] Statistiques'
hint_delete: '[x] Supprimer'
# Barre latérale de statistiques (pendant l'exercice)
sidebar:
title: ' Statistiques '
wpm: 'WPM : '
target: 'Objectif : '
target_wpm: '%{wpm} WPM'
accuracy: 'Précision : '
progress: 'Progression : '
correct: 'Corrects : '
errors: 'Erreurs : '
time: 'Temps : '
last_drill: ' Dernier Exercice '
vs_avg: ' vs moy : '
# Tableau de bord des statistiques
stats:
title: ' Statistiques '
empty: 'Aucun exercice terminé. Commencez à taper !'
tab_dashboard: '[1] Tableau de bord'
tab_history: '[2] Historique'
tab_activity: '[3] Activité'
tab_accuracy: '[4] Précision'
tab_timing: '[5] Chronométrage'
tab_ngrams: '[6] N-grammes'
hint_back: '[ESC] Retour'
hint_next_tab: '[Tab] Onglet suivant'
hint_switch_tab: '[1-6] Changer d''onglet'
hint_navigate: '[j/k] Naviguer'
hint_page: '[PgUp/PgDn] Défiler'
hint_delete: '[x] Supprimer'
summary_title: ' Résumé '
drills: ' Exercices : '
avg_wpm: ' WPM Moy : '
best_wpm: ' Meilleur WPM : '
accuracy_label: ' Précision : '
total_time: ' Temps total : '
wpm_chart_title: ' WPM par Exercice (20 derniers, Objectif : %{target}) '
accuracy_chart_title: ' Précision %% (50 derniers Exercices) '
chart_drill: 'Exercice #'
chart_accuracy_pct: 'Précision %%'
sessions_title: ' Sessions Récentes '
session_header: ' # WPM Raw Pré%% Temps Date/Heure Mode Classé Partiel'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Supprimer la session #%{idx} ? (y/n)'
confirm_title: ' Confirmer '
yes: 'oui'
no: 'non'
keyboard_accuracy_title: ' Précision du Clavier %% '
keyboard_timing_title: ' Chronométrage du Clavier (ms) '
slowest_keys_title: ' Touches les plus Lentes (ms) '
fastest_keys_title: ' Touches les plus Rapides (ms) '
worst_accuracy_title: ' Pire Précision (%%) '
best_accuracy_title: ' Meilleure Précision (%%) '
not_enough_data: ' Données insuffisantes'
streaks_title: ' Séries '
current_streak: ' Actuelle : '
best_streak: ' Meilleure : '
active_days: ' Jours actifs : '
top_days_none: ' Meilleurs jours : aucun'
top_days: ' Meilleurs jours : %{days}'
wpm_label: ' WPM : %{avg}/%{target} (%{pct}%%)'
acc_label: ' Pré : %{pct}%%'
keys_label: ' Touches : %{unlocked}/%{total} (%{mastered} maîtrisées)'
ngram_empty: 'Terminez des exercices adaptatifs pour voir les données de n-grammes'
ngram_header_speed_narrow: ' Bgrm Vit Att Anom%'
ngram_header_error_narrow: ' Bgrm Err Éch Taux Att Anom%'
ngram_header_speed: ' Bigramme Vit Att Échan. Anom%'
ngram_header_error: ' Bigramme Erreurs Échan. Taux Att Anom%'
focus_title: ' Focus Actif '
focus_char_label: ' Focus : '
focus_bigram_value: 'Bigramme %{label}'
focus_plus: ' + '
anomaly_error: 'erreur'
anomaly_speed: 'vitesse'
focus_detail_both: ' Caractère ''%{ch}'' : touche la plus faible | Bigramme %{label} : anomalie de %{type} %{pct}%%'
focus_detail_char_only: ' Caractère ''%{ch}'' : touche la plus faible, aucune anomalie de bigramme confirmée'
focus_detail_bigram_only: ' (anomalie de %{type} : %{pct}%%)'
focus_empty: ' Terminez des exercices adaptatifs pour voir les données de focus'
error_anomalies_title: ' Anomalies d''Erreur (%{count}) '
no_error_anomalies: ' Aucune anomalie d''erreur détectée'
speed_anomalies_title: ' Anomalies de Vitesse (%{count}) '
no_speed_anomalies: ' Aucune anomalie de vitesse détectée'
scope_label_prefix: ' '
bi_label: ' | Bi : %{count}'
hes_label: ' | Hés : >%{ms}ms'
focus_char_value: 'Caractère ''%{ch}'''
# Carte d'activité
heatmap:
title: ' Activité Quotidienne (Sessions par Jour) '
jan: 'Jan'
feb: 'Fév'
mar: 'Mar'
apr: 'Avr'
may: 'Mai'
jun: 'Jun'
jul: 'Jul'
aug: 'Aoû'
sep: 'Sep'
oct: 'Oct'
nov: 'Nov'
dec: 'Déc'
# Graphique
chart:
wpm_over_time: ' WPM au fil du temps '
drill_number: 'Exercice #'
# Paramètres
settings:
title: ' Paramètres '
subtitle: 'Utilisez les flèches pour naviguer, Entrée/Droite pour changer, ESC pour sauvegarder et quitter'
target_wpm: 'WPM Objectif'
theme: 'Thème'
word_count: 'Nombre de Mots'
ui_language: 'Langue de l''Interface'
dictionary_language: 'Langue du Dictionnaire'
keyboard_layout: 'Disposition du Clavier'
code_language: 'Langage de Code'
code_downloads: 'Téléchargements de Code'
on: 'Oui'
off: 'Non'
code_download_dir: 'Rép. Téléchargement Code'
snippets_per_repo: 'Extraits par Dépôt'
unlimited: 'Illimité'
download_code_now: 'Télécharger Code Maintenant'
run_downloader: 'Lancer le téléchargeur'
passage_downloads: 'Téléchargements de Passages'
passage_download_dir: 'Rép. Téléchargement Passages'
paragraphs_per_book: 'Paragraphes par Livre'
whole_book: 'Livre entier'
download_passages_now: 'Télécharger Passages Maintenant'
export_path: 'Chemin d''Export'
export_data: 'Exporter les Données'
export_now: 'Exporter maintenant'
import_path: 'Chemin d''Import'
import_data: 'Importer les Données'
import_now: 'Importer maintenant'
hint_save_back: '[ESC] Sauvegarder et retour'
hint_change_value: '[Enter/flèches] Changer la valeur'
hint_edit_path: '[Enter sur chemin] Éditer'
hint_move: '[←→] Déplacer'
hint_tab_complete: '[Tab] Compléter (à la fin)'
hint_confirm: '[Enter] Confirmer'
hint_cancel: '[Esc] Annuler'
success_title: ' Succès '
error_title: ' Erreur '
press_any_key: 'Appuyez sur une touche'
file_exists_title: ' Fichier Existant '
file_exists: 'Un fichier existe déjà à ce chemin.'
overwrite_rename: '[d] Écraser [r] Renommer [Esc] Annuler'
erase_warning: 'Ceci effacera vos données actuelles.'
export_first: 'Exportez d''abord si vous voulez les conserver.'
proceed_yn: 'Continuer ? (y/n)'
confirm_import_title: ' Confirmer l''Importation '
# Écrans de sélection
select:
dictionary_language_title: ' Sélectionner la Langue du Dictionnaire '
keyboard_layout_title: ' Sélectionner la Disposition du Clavier '
code_language_title: ' Sélectionner le Langage de Code '
passage_source_title: ' Sélectionner la Source de Passages '
ui_language_title: ' Sélectionner la Langue de l''Interface '
more_above: '... %{count} de plus au-dessus ...'
more_below: '... %{count} de plus en-dessous ...'
current: ' (actuel)'
disabled: ' (désactivé)'
enabled_default: ' (activé, par défaut : %{layout})'
enabled: ' (activé)'
disabled_blocked: ' (désactivé : bloqué)'
built_in: ' (intégré)'
cached: ' (en cache)'
disabled_download: ' (désactivé : téléchargement requis)'
download_required: ' (téléchargement requis)'
hint_navigate: '[Up/Down/PgUp/PgDn] Naviguer'
hint_confirm: '[Enter] Confirmer'
hint_back: '[ESC] Retour'
language_resets_layout: 'Sélectionner une langue réinitialise la disposition à celle par défaut de cette langue.'
layout_no_language_change: 'Changer la disposition ne change pas la langue du dictionnaire.'
disabled_network_notice: 'Certaines langues sont désactivées : activez les téléchargements dans intro/paramètres.'
disabled_sources_notice: 'Certaines sources sont désactivées : activez les téléchargements dans intro/paramètres.'
passage_all: 'Tous (Intégrés + tous les livres)'
passage_builtin: 'Passages intégrés uniquement'
passage_book_prefix: 'Livre : %{title}'
# Progression
progress:
overall_key_progress: 'Progression Globale des Touches'
unlocked_mastered: '%{unlocked}/%{total} déverrouillées (%{mastered} maîtrisées)'
# Arbre de compétences
skill_tree:
title: ' Arbre de Compétences '
locked: 'Verrouillé'
unlocked: 'déverrouillé'
mastered: 'maîtrisé'
in_progress: 'en cours'
complete: 'terminé'
locked_status: 'verrouillé'
locked_notice: 'Terminez %{count} lettres primaires pour débloquer les branches'
branches_separator: 'Branches (disponibles après %{count} lettres primaires)'
unlocked_letters: 'Déverrouillées %{unlocked}/%{total} lettres'
level: 'Niveau %{current}/%{total}'
level_zero: 'Niveau 0/%{total}'
in_focus: ' en focus'
hint_navigate: '[↑↓/jk] Naviguer'
hint_scroll: '[PgUp/PgDn ou Ctrl+U/Ctrl+D] Défiler'
hint_back: '[q] Retour'
hint_unlock: '[Enter] Déverrouiller'
hint_start_drill: '[Enter] Lancer l''Exercice'
unlock_msg_1: 'Une fois déverrouillé, l''exercice adaptatif inclura les touches de cette branche qui sont déverrouillées.'
unlock_msg_2: 'Si vous voulez vous concentrer sur cette branche, lancez un exercice directement depuis cette branche dans l''Arbre de Compétences.'
confirm_unlock: 'Déverrouiller %{branch} ?'
confirm_yn: '[y] Déverrouiller [n/ESC] Annuler'
lvl_prefix: 'Niv'
branch_primary_letters: 'Lettres Primaires'
branch_capital_letters: 'Lettres Majuscules'
branch_numbers: 'Chiffres 0-9'
branch_prose_punctuation: 'Ponctuation de Prose'
branch_whitespace: 'Espaces Blancs'
branch_code_symbols: 'Symboles de Code'
level_frequency_order: 'Ordre de Fréquence'
level_common_sentence_capitals: 'Majuscules de Phrases Courantes'
level_name_capitals: 'Majuscules de Noms'
level_remaining_capitals: 'Majuscules Restantes'
level_common_digits: 'Chiffres Courants'
level_all_digits: 'Tous les Chiffres'
level_essential: 'Essentiel'
level_common: 'Courant'
level_expressive: 'Expressif'
level_enter_return: 'Entrée/Retour'
level_tab_indent: 'Tab/Indentation'
level_arithmetic_assignment: 'Arithmétique et Affectation'
level_grouping: 'Groupement'
level_logic_reference: 'Logique et Référence'
level_special: 'Spécial'
# Jalons
milestones:
unlock_title: ' Touche Déverrouillée ! '
mastery_title: ' Touche Maîtrisée ! '
branches_title: ' Nouvelles Branches Disponibles ! '
branch_complete_title: ' Branche Terminée ! '
all_unlocked_title: ' Toutes les Touches Déverrouillées ! '
all_mastered_title: ' Maîtrise Totale du Clavier ! '
unlocked: 'déverrouillée'
mastered: 'maîtrisée'
use_finger: 'Utilisez votre %{finger}'
hold_right_shift: 'Maintenez Shift Droit (auriculaire droit)'
hold_left_shift: 'Maintenez Shift Gauche (auriculaire gauche)'
congratulations_all_letters: 'Félicitations ! Vous avez maîtrisé les %{count} lettres primaires'
new_branches_available: 'De nouvelles branches de compétences sont disponibles :'
visit_skill_tree: 'Visitez l''Arbre de Compétences pour déverrouiller une nouvelle branche'
and_start_training: 'et commencez l''entraînement !'
open_skill_tree: 'Appuyez sur [t] pour ouvrir l''Arbre de Compétences'
branch_complete_msg: 'Vous avez terminé la branche %{branch} !'
all_levels_mastered: 'Les %{count} niveaux sont maîtrisés.'
all_keys_confident: 'Chaque touche de cette branche est à confiance maximale.'
all_unlocked_msg: 'Vous avez déverrouillé toutes les touches du clavier !'
all_unlocked_desc: 'Chaque caractère, symbole et modificateur est disponible dans vos exercices.'
keep_practicing_mastery: 'Continuez à pratiquer pour atteindre la maîtrise — quand chaque touche atteindra'
confidence_complete: 'la confiance maximale, vous aurez atteint la maîtrise totale du clavier !'
all_mastered_msg: 'Félicitations — vous avez atteint la maîtrise totale du clavier !'
all_mastered_desc: 'Chaque touche du clavier est à confiance maximale.'
mastery_takes_practice: 'La maîtrise n''est pas une destination — elle nécessite une pratique continue.'
keep_drilling: 'Continuez à vous entraîner pour garder votre niveau.'
hint_skill_tree_continue: '[t] Ouvrir l''Arbre de Compétences [Autre touche] Continuer'
hint_any_key: 'Appuyez sur une touche pour continuer'
input_blocked: 'Saisie temporairement bloquée (%{ms}ms restantes)'
unlock_msg_1: 'Bon travail ! Continuez à améliorer vos compétences.'
unlock_msg_2: 'Une touche de plus dans votre arsenal !'
unlock_msg_3: 'Votre clavier s''agrandit ! Continuez !'
unlock_msg_4: 'Un pas de plus vers la maîtrise totale !'
mastery_msg_1: 'Cette touche est à confiance maximale !'
mastery_msg_2: 'Vous maîtrisez cette touche parfaitement !'
mastery_msg_3: 'Mémoire musculaire acquise !'
mastery_msg_4: 'Une touche de plus conquise !'
# Explorateur de clavier
keyboard:
title: ' Clavier '
subtitle: 'Appuyez ou cliquez sur une touche'
hint_navigate: '[←→↑↓/hjkl/Tab] Naviguer'
hint_back: '[q/ESC] Retour'
key_label: 'Touche : '
finger_label: 'Doigt : '
hand_left: 'Gauche'
hand_right: 'Droite'
finger_index: 'Indicateur'
finger_middle: 'Majeur'
finger_ring: 'Annulaire'
finger_pinky: 'Auriculaire'
finger_thumb: 'Pouce'
overall_accuracy: ' Précision globale : %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Précision classée : %{correct}/%{total} (%{pct}%%)'
confidence: 'Confiance : '
no_data: 'Pas encore de données'
no_data_short: 'Pas de données'
key_details: ' Détails de la Touche '
key_details_char: ' Détails de la Touche : ''%{ch}'' '
key_details_name: ' Détails de la Touche : %{name} '
press_key_hint: 'Appuyez sur une touche pour voir ses détails'
shift_label: 'Shift : '
shift_no: 'Non'
overall_avg_time: 'Temps Moy. Global : '
overall_best_time: 'Meilleur Temps Global : '
overall_samples: 'Échantillons Globaux : '
overall_accuracy_label: 'Précision Globale : '
branch_label: 'Branche : '
level_label: 'Niveau : '
built_in_key: 'Touche Intégrée'
unlocked_label: 'Déverrouillée : '
yes: 'Oui'
no: 'Non'
in_focus_label: 'En Focus ? : '
mastery_label: 'Maîtrise : '
mastery_locked: 'Verrouillé'
ranked_avg_time: 'Temps Moy. Classé : '
ranked_best_time: 'Meilleur Temps Classé : '
ranked_samples: 'Échantillons Classés : '
ranked_accuracy_label: 'Précision Classée : '
# Dialogues d'introduction
intro:
passage_title: ' Configuration Téléchargement de Passages '
code_title: ' Configuration Téléchargement de Code '
enable_downloads: 'Activer les téléchargements réseau'
download_dir: 'Répertoire de téléchargement'
paragraphs_per_book: 'Paragraphes par livre (0 = entier)'
whole_book: 'livre entier'
snippets_per_repo: 'Extraits par dépôt (0 = illimité)'
unlimited: 'illimité'
start_passage_drill: 'Lancer l''exercice de passage'
start_code_drill: 'Lancer l''exercice de code'
confirm: 'Confirmer'
hint_navigate: '[Up/Down] Naviguer'
hint_adjust: '[Left/Right] Ajuster'
hint_edit: '[Type/Backspace] Éditer'
hint_confirm: '[Enter] Confirmer'
hint_cancel: '[ESC] Annuler'
preparing_download: 'Préparation du téléchargement...'
download_passage_title: ' Téléchargement de la Source de Passage '
download_code_title: ' Téléchargement de la Source de Code '
book_label: ' Livre : %{name}'
repo_label: ' Dépôt : %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bytes'
downloaded_bytes: 'Téléchargé : %{bytes} bytes'
downloading_book_progress: 'Téléchargement du livre : [%{bar}] %{downloaded}/%{total} bytes'
downloading_book_bytes: 'Téléchargement du livre : %{bytes} bytes'
downloading_code_progress: 'Téléchargement : [%{bar}] %{downloaded}/%{total} bytes'
downloading_code_bytes: 'Téléchargement : %{bytes} bytes'
current_book: 'Actuel : %{name} (livre %{done}/%{total})'
current_repo: 'Actuel : %{name} (dépôt %{done}/%{total})'
passage_instructions_1: 'keydr peut télécharger des passages de Project Gutenberg pour la pratique de frappe.'
passage_instructions_2: 'Les livres sont téléchargés une fois et mis en cache localement.'
passage_instructions_3: 'Configurez les paramètres ci-dessous, puis lancez un exercice de passage.'
code_instructions_1: 'keydr peut télécharger du code open-source de GitHub pour la pratique de frappe.'
code_instructions_2: 'Le code est téléchargé une fois et mis en cache localement.'
code_instructions_3: 'Configurez les paramètres ci-dessous, puis lancez un exercice de code.'
# Messages de statut (de app.rs)
status:
recovery_files: 'Fichiers de récupération trouvés suite à une importation interrompue. Les données peuvent être incohérentes — envisagez de réimporter.'
dir_not_exist: 'Le répertoire n''existe pas : %{path}'
no_data_store: 'Aucun stockage de données disponible'
serialization_error: 'Erreur de sérialisation : %{error}'
exported_to: 'Exporté vers %{path}'
export_failed: 'Échec de l''exportation : %{error}'
could_not_read: 'Impossible de lire le fichier : %{error}'
invalid_export: 'Fichier d''export invalide : %{error}'
unsupported_version: 'Version d''export non supportée : %{got} (attendue %{expected})'
import_failed: 'Échec de l''importation : %{error}'
imported_theme_fallback: 'Importé avec succès (thème ''%{theme}'' introuvable, utilisation du défaut)'
imported_success: 'Importé avec succès'
adaptive_unavailable: 'Mode adaptatif classé non disponible : %{error}'
switched_to: 'Basculé vers %{name}'
layout_changed: 'Disposition changée en %{name}'
# Erreurs (pour traduction des limites d'UI)
errors:
unknown_language: 'Langue inconnue : %{key}'
unknown_layout: 'Disposition de clavier inconnue : %{key}'
unsupported_pair: 'Paire langue/disposition non supportée : %{language} + %{layout}'
language_blocked: 'Langue bloquée par le niveau de support : %{key}'
# Commun
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Retour'

454
locales/hr.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminalni trener tipkanja'
adaptive_drill: 'Prilagodljiva vježba'
adaptive_drill_desc: 'Fonetske riječi s prilagodljivim otključavanjem slova'
code_drill: 'Vježba koda'
code_drill_desc: 'Vježbajte tipkanje sintakse koda'
passage_drill: 'Vježba teksta'
passage_drill_desc: 'Tipkajte odlomke iz knjiga'
skill_tree: 'Stablo vještina'
skill_tree_desc: 'Pregledajte grane napretka i pokrenite vježbe'
keyboard: 'Tipkovnica'
keyboard_desc: 'Istražite raspored tipkovnice i statistiku tipki'
statistics: 'Statistika'
statistics_desc: 'Pregledajte svoju statistiku tipkanja'
settings: 'Postavke'
settings_desc: 'Konfigurirajte keydr'
day_streak: ' | %{days} dana zaredom'
key_progress: ' Napredak tipki %{unlocked}/%{total} (%{mastered} savladano) | Cilj %{target} WPM%{streak}'
hint_start: '[1-3] Pokreni'
hint_skill_tree: '[t] Stablo vještina'
hint_keyboard: '[b] Tipkovnica'
hint_stats: '[s] Statistika'
hint_settings: '[c] Postavke'
hint_quit: '[q] Izlaz'
# Drill screen
drill:
title: ' Vježba '
mode_adaptive: 'Prilagodljiva'
mode_code: 'Kod (bez ocjene)'
mode_passage: 'Tekst (bez ocjene)'
focus_char: 'Fokus: ''%{ch}'''
focus_bigram: 'Fokus: "%{bigram}"'
focus_both: 'Fokus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Toč'
header_err: 'Greš'
code_source: ' Izvor koda '
passage_source: ' Izvor teksta '
footer: '[ESC] Završi vježbu [Backspace] Obriši'
keys_reenabled: 'Tipke ponovo aktivne za %{ms}ms'
hint_end: '[ESC] Završi vježbu'
hint_backspace: '[Backspace] Obriši'
# Dashboard / drill result
dashboard:
title: ' Vježba završena '
results: 'Rezultati'
unranked_note_prefix: ' (Bez ocjene'
unranked_note_suffix: ' ne broji se za stablo vještina)'
speed: ' Brzina: '
accuracy_label: ' Točnost: '
time_label: ' Vrijeme: '
errors_label: ' Greške: '
correct_detail: ' (%{correct}/%{total} točno)'
input_blocked: ' Unos privremeno blokiran '
input_blocked_ms: '(%{ms}ms preostalo)'
hint_continue: '[c/Enter/Space] Nastavi'
hint_retry: '[r] Ponovi'
hint_menu: '[q] Izbornik'
hint_stats: '[s] Statistika'
hint_delete: '[x] Obriši'
# Stats sidebar (during drill)
sidebar:
title: ' Statistika '
wpm: 'WPM: '
target: 'Cilj: '
target_wpm: '%{wpm} WPM'
accuracy: 'Točnost: '
progress: 'Napredak: '
correct: 'Točno: '
errors: 'Greške: '
time: 'Vrijeme: '
last_drill: ' Zadnja vježba '
vs_avg: ' vs prosjek: '
# Statistics dashboard
stats:
title: ' Statistika '
empty: 'Nema završenih vježbi. Počnite tipkati!'
tab_dashboard: '[1] Pregled'
tab_history: '[2] Povijest'
tab_activity: '[3] Aktivnost'
tab_accuracy: '[4] Točnost'
tab_timing: '[5] Tajming'
tab_ngrams: '[6] N-grami'
hint_back: '[ESC] Natrag'
hint_next_tab: '[Tab] Sljedeća kartica'
hint_switch_tab: '[1-6] Kartica'
hint_navigate: '[j/k] Navigacija'
hint_page: '[PgUp/PgDn] Stranica'
hint_delete: '[x] Obriši'
summary_title: ' Sažetak '
drills: ' Vježbe: '
avg_wpm: ' Prosj. WPM: '
best_wpm: ' Najbolji WPM: '
accuracy_label: ' Točnost: '
total_time: ' Ukupno vrijeme: '
wpm_chart_title: ' WPM po vježbi (Zadnjih 20, Cilj: %{target}) '
accuracy_chart_title: ' Točnost %% (Zadnjih 50 vježbi) '
chart_drill: 'Vježba #'
chart_accuracy_pct: 'Točnost %%'
sessions_title: ' Nedavne sesije '
session_header: ' # WPM Raw Toč%% Vrijeme Datum/Vrijeme Način Ocjena Djelom.'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Obrisati sesiju #%{idx}? (y/n)'
confirm_title: ' Potvrda '
yes: 'da'
no: 'ne'
keyboard_accuracy_title: ' Točnost tipkovnice %% '
keyboard_timing_title: ' Tajming tipkovnice (ms) '
slowest_keys_title: ' Najsporije tipke (ms) '
fastest_keys_title: ' Najbrže tipke (ms) '
worst_accuracy_title: ' Najgora točnost (%%) '
best_accuracy_title: ' Najbolja točnost (%%) '
not_enough_data: ' Nedovoljno podataka'
streaks_title: ' Nizovi '
current_streak: ' Trenutni: '
best_streak: ' Najbolji: '
active_days: ' Aktivni dani: '
top_days_none: ' Najbolji dani: nema'
top_days: ' Najbolji dani: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Toč: %{pct}%%'
keys_label: ' Tipke: %{unlocked}/%{total} (%{mastered} savladano)'
ngram_empty: 'Završite nekoliko prilagodljivih vježbi za prikaz n-gram podataka'
ngram_header_speed_narrow: ' Bgrm Brzina Očekiv Anom%'
ngram_header_error_narrow: ' Bgrm Greš Uzrk Stopa Očk Anom%'
ngram_header_speed: ' Bigram Brzina Očekiv Uzorci Anom%'
ngram_header_error: ' Bigram Greške Uzorci Stopa Očekiv Anom%'
focus_title: ' Aktivni fokus '
focus_char_label: ' Fokus: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'greška'
anomaly_speed: 'brzina'
focus_detail_both: ' Znak ''%{ch}'': najslabija tipka | Bigram %{label}: %{type} anomalija %{pct}%%'
focus_detail_char_only: ' Znak ''%{ch}'': najslabija tipka, nema potvrđenih bigram anomalija'
focus_detail_bigram_only: ' (%{type} anomalija: %{pct}%%)'
focus_empty: ' Završite nekoliko prilagodljivih vježbi za prikaz fokus podataka'
error_anomalies_title: ' Anomalije grešaka (%{count}) '
no_error_anomalies: ' Nema otkrivenih anomalija grešaka'
speed_anomalies_title: ' Anomalije brzine (%{count}) '
no_speed_anomalies: ' Nema otkrivenih anomalija brzine'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Znak ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Dnevna aktivnost (Sesije po danu) '
jan: 'Sij'
feb: 'Velj'
mar: 'Ožu'
apr: 'Tra'
may: 'Svi'
jun: 'Lip'
jul: 'Srp'
aug: 'Kol'
sep: 'Ruj'
oct: 'Lis'
nov: 'Stu'
dec: 'Pro'
# Chart
chart:
wpm_over_time: ' WPM kroz vrijeme '
drill_number: 'Vježba #'
# Settings
settings:
title: ' Postavke '
subtitle: 'Strelicama navigirajte, Enter/Desno za promjenu, ESC za spremanje'
target_wpm: 'Ciljni WPM'
theme: 'Tema'
word_count: 'Broj riječi'
ui_language: 'Jezik sučelja'
dictionary_language: 'Jezik rječnika'
keyboard_layout: 'Raspored tipkovnice'
code_language: 'Programski jezik'
code_downloads: 'Preuzimanje koda'
on: 'Uklj.'
off: 'Isklj.'
code_download_dir: 'Mapa za preuzimanje koda'
snippets_per_repo: 'Isječaka po repozitoriju'
unlimited: 'Neograničeno'
download_code_now: 'Preuzmi kod sada'
run_downloader: 'Pokreni preuzimanje'
passage_downloads: 'Preuzimanje tekstova'
passage_download_dir: 'Mapa za preuzimanje tekstova'
paragraphs_per_book: 'Odlomaka po knjizi'
whole_book: 'Cijela knjiga'
download_passages_now: 'Preuzmi tekstove sada'
export_path: 'Putanja izvoza'
export_data: 'Izvezi podatke'
export_now: 'Izvezi sada'
import_path: 'Putanja uvoza'
import_data: 'Uvezi podatke'
import_now: 'Uvezi sada'
hint_save_back: '[ESC] Spremi i natrag'
hint_change_value: '[Enter/strelice] Promijeni'
hint_edit_path: '[Enter na putanju] Uredi'
hint_move: '[←→] Pomakni'
hint_tab_complete: '[Tab] Dovrši (na kraju)'
hint_confirm: '[Enter] Potvrdi'
hint_cancel: '[Esc] Odustani'
success_title: ' Uspjeh '
error_title: ' Greška '
press_any_key: 'Pritisnite bilo koju tipku'
file_exists_title: ' Datoteka postoji '
file_exists: 'Datoteka već postoji na ovoj putanji.'
overwrite_rename: '[d] Prepiši [r] Preimenuj [Esc] Odustani'
erase_warning: 'Ovo će izbrisati vaše trenutne podatke.'
export_first: 'Prvo izvezite ako želite sačuvati.'
proceed_yn: 'Nastaviti? (y/n)'
confirm_import_title: ' Potvrda uvoza '
# Selection screens
select:
dictionary_language_title: ' Odaberite jezik rječnika '
keyboard_layout_title: ' Odaberite raspored tipkovnice '
code_language_title: ' Odaberite programski jezik '
passage_source_title: ' Odaberite izvor teksta '
ui_language_title: ' Odaberite jezik sučelja '
more_above: '... još %{count} iznad ...'
more_below: '... još %{count} ispod ...'
current: ' (trenutni)'
disabled: ' (onemogućeno)'
enabled_default: ' (omogućeno, zadano: %{layout})'
enabled: ' (omogućeno)'
disabled_blocked: ' (onemogućeno: blokirano)'
built_in: ' (ugrađeno)'
cached: ' (u predmemoriji)'
disabled_download: ' (onemogućeno: potrebno preuzimanje)'
download_required: ' (potrebno preuzimanje)'
hint_navigate: '[Gore/Dolje/PgUp/PgDn] Navigacija'
hint_confirm: '[Enter] Potvrdi'
hint_back: '[ESC] Natrag'
language_resets_layout: 'Odabir jezika resetira raspored tipkovnice na zadani za taj jezik.'
layout_no_language_change: 'Promjena rasporeda ne mijenja jezik rječnika.'
disabled_network_notice: 'Neki jezici su onemogućeni: omogućite mrežna preuzimanja u uvodu/postavkama.'
disabled_sources_notice: 'Neki izvori su onemogućeni: omogućite mrežna preuzimanja u uvodu/postavkama.'
passage_all: 'Sve (Ugrađeno + sve knjige)'
passage_builtin: 'Samo ugrađeni tekstovi'
passage_book_prefix: 'Knjiga: %{title}'
# Progress
progress:
overall_key_progress: 'Ukupni napredak tipki'
unlocked_mastered: '%{unlocked}/%{total} otključano (%{mastered} savladano)'
# Skill tree
skill_tree:
title: ' Stablo vještina '
locked: 'Zaključano'
unlocked: 'otključano'
mastered: 'savladano'
in_progress: 'u tijeku'
complete: 'završeno'
locked_status: 'zaključano'
locked_notice: 'Završite %{count} primarnih slova za otključavanje grana'
branches_separator: 'Grane (dostupne nakon %{count} primarnih slova)'
unlocked_letters: 'Otključano %{unlocked}/%{total} slova'
level: 'Razina %{current}/%{total}'
level_zero: 'Razina 0/%{total}'
in_focus: ' u fokusu'
hint_navigate: '[↑↓/jk] Navigacija'
hint_scroll: '[PgUp/PgDn ili Ctrl+U/Ctrl+D] Pomicanje'
hint_back: '[q] Natrag'
hint_unlock: '[Enter] Otključaj'
hint_start_drill: '[Enter] Pokreni vježbu'
unlock_msg_1: 'Nakon otključavanja, zadana prilagodljiva vježba uključit će otključane tipke iz ove grane.'
unlock_msg_2: 'Ako se želite usredotočiti samo na ovu granu, pokrenite vježbu izravno iz stabla vještina.'
confirm_unlock: 'Otključati %{branch}?'
confirm_yn: '[y] Otključaj [n/ESC] Odustani'
lvl_prefix: 'Raz'
branch_primary_letters: 'Primarna slova'
branch_capital_letters: 'Velika slova'
branch_numbers: 'Brojevi 0-9'
branch_prose_punctuation: 'Interpunkcija'
branch_whitespace: 'Bjeline'
branch_code_symbols: 'Simboli koda'
level_frequency_order: 'Poredak po učestalosti'
level_common_sentence_capitals: 'Uobičajena velika slova'
level_name_capitals: 'Velika slova za imena'
level_remaining_capitals: 'Preostala velika slova'
level_common_digits: 'Uobičajene znamenke'
level_all_digits: 'Sve znamenke'
level_essential: 'Osnovno'
level_common: 'Uobičajeno'
level_expressive: 'Izražajno'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Uvlačenje'
level_arithmetic_assignment: 'Aritmetika i pridruživanje'
level_grouping: 'Grupiranje'
level_logic_reference: 'Logika i reference'
level_special: 'Specijalno'
# Milestones
milestones:
unlock_title: ' Tipka otključana! '
mastery_title: ' Tipka savladana! '
branches_title: ' Nove grane vještina dostupne! '
branch_complete_title: ' Grana završena! '
all_unlocked_title: ' Sve tipke otključane! '
all_mastered_title: ' Potpuno vladanje tipkovnicom! '
unlocked: 'otključano'
mastered: 'savladano'
use_finger: 'Koristite %{finger}'
hold_right_shift: 'Držite desni Shift (desni mali prst)'
hold_left_shift: 'Držite lijevi Shift (lijevi mali prst)'
congratulations_all_letters: 'Čestitamo! Savladali ste svih %{count} primarnih slova'
new_branches_available: 'Nove grane vještina su sada dostupne:'
visit_skill_tree: 'Posjetite stablo vještina za otključavanje nove grane'
and_start_training: 'i počnite vježbati!'
open_skill_tree: 'Pritisnite [t] za otvaranje stabla vještina'
branch_complete_msg: 'Završili ste granu %{branch}!'
all_levels_mastered: 'Svih %{count} razina savladano.'
all_keys_confident: 'Svaka tipka u ovoj grani je na punoj razini pouzdanosti.'
all_unlocked_msg: 'Otključali ste svaku tipku na tipkovnici!'
all_unlocked_desc: 'Svaki znak, simbol i modifikator je sada dostupan u vašim vježbama.'
keep_practicing_mastery: 'Nastavite vježbati za izgradnju majstorstva — kada svaka tipka dosegne punu'
confidence_complete: 'razinu pouzdanosti, postigli ste potpuno vladanje tipkovnicom!'
all_mastered_msg: 'Čestitamo — postigli ste potpuno vladanje tipkovnicom!'
all_mastered_desc: 'Svaka tipka na tipkovnici je na maksimalnoj razini pouzdanosti.'
mastery_takes_practice: 'Majstorstvo nije odredište — zahtijeva stalnu vježbu.'
keep_drilling: 'Nastavite vježbati kako biste održali formu.'
hint_skill_tree_continue: '[t] Otvori stablo vještina [Bilo koja tipka] Nastavi'
hint_any_key: 'Pritisnite bilo koju tipku za nastavak'
input_blocked: 'Unos privremeno blokiran (%{ms}ms preostalo)'
unlock_msg_1: 'Odličan posao! Nastavite graditi vještine tipkanja.'
unlock_msg_2: 'Još jedna tipka dodana u vaš arsenal!'
unlock_msg_3: 'Vaša tipkovnica raste! Samo naprijed.'
unlock_msg_4: 'Korak bliže potpunom vladanju tipkovnicom!'
mastery_msg_1: 'Ova tipka je sada na punoj razini pouzdanosti!'
mastery_msg_2: 'Ovu tipku imate u malom prstu!'
mastery_msg_3: 'Mišićna memorija zaključana!'
mastery_msg_4: 'Još jedna tipka osvojena!'
# Keyboard explorer
keyboard:
title: ' Tipkovnica '
subtitle: 'Pritisnite bilo koju tipku ili kliknite tipku'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigacija'
hint_back: '[q/ESC] Natrag'
key_label: 'Tipka: '
finger_label: 'Prst: '
hand_left: 'Lijeva'
hand_right: 'Desna'
finger_index: 'Kažiprst'
finger_middle: 'Srednji'
finger_ring: 'Prstenjak'
finger_pinky: 'Mali prst'
finger_thumb: 'Palac'
overall_accuracy: ' Ukupna točnost: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Ocjenjena točnost: %{correct}/%{total} (%{pct}%%)'
confidence: 'Pouzdanost: '
no_data: 'Još nema podataka'
no_data_short: 'Nema podat.'
key_details: ' Detalji tipke '
key_details_char: ' Detalji tipke: ''%{ch}'' '
key_details_name: ' Detalji tipke: %{name} '
press_key_hint: 'Pritisnite tipku za prikaz detalja'
shift_label: 'Shift: '
shift_no: 'Ne'
overall_avg_time: 'Ukupno prosj. vrijeme: '
overall_best_time: 'Ukupno najbolje vrijeme: '
overall_samples: 'Ukupno uzoraka: '
overall_accuracy_label: 'Ukupna točnost: '
branch_label: 'Grana: '
level_label: 'Razina: '
built_in_key: 'Ugrađena tipka'
unlocked_label: 'Otključano: '
yes: 'Da'
no: 'Ne'
in_focus_label: 'U fokusu?: '
mastery_label: 'Majstorstvo: '
mastery_locked: 'Zaključano'
ranked_avg_time: 'Ocj. prosj. vrijeme: '
ranked_best_time: 'Ocj. najbolje vrijeme: '
ranked_samples: 'Ocj. uzoraka: '
ranked_accuracy_label: 'Ocj. točnost: '
# Intro dialogs
intro:
passage_title: ' Postavke preuzimanja tekstova '
code_title: ' Postavke preuzimanja koda '
enable_downloads: 'Omogući mrežna preuzimanja'
download_dir: 'Mapa za preuzimanje'
paragraphs_per_book: 'Odlomaka po knjizi (0 = cijela)'
whole_book: 'cijela knjiga'
snippets_per_repo: 'Isječaka po repozitoriju (0 = neograničeno)'
unlimited: 'neograničeno'
start_passage_drill: 'Pokreni vježbu teksta'
start_code_drill: 'Pokreni vježbu koda'
confirm: 'Potvrdi'
hint_navigate: '[Gore/Dolje] Navigacija'
hint_adjust: '[Lijevo/Desno] Prilagodi'
hint_edit: '[Tipkanje/Backspace] Uredi'
hint_confirm: '[Enter] Potvrdi'
hint_cancel: '[ESC] Odustani'
preparing_download: 'Priprema preuzimanja...'
download_passage_title: ' Preuzimanje izvora teksta '
download_code_title: ' Preuzimanje izvora koda '
book_label: ' Knjiga: %{name}'
repo_label: ' Repozitorij: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bajtova'
downloaded_bytes: 'Preuzeto: %{bytes} bajtova'
downloading_book_progress: 'Preuzimanje knjige: [%{bar}] %{downloaded}/%{total} bajtova'
downloading_book_bytes: 'Preuzimanje knjige: %{bytes} bajtova'
downloading_code_progress: 'Preuzimanje: [%{bar}] %{downloaded}/%{total} bajtova'
downloading_code_bytes: 'Preuzimanje: %{bytes} bajtova'
current_book: 'Trenutno: %{name} (knjiga %{done}/%{total})'
current_repo: 'Trenutno: %{name} (repozitorij %{done}/%{total})'
passage_instructions_1: 'keydr može preuzeti tekstove iz Project Gutenberga za vježbu tipkanja.'
passage_instructions_2: 'Knjige se preuzimaju jednom i lokalno pohranjuju.'
passage_instructions_3: 'Konfigurirajte postavke preuzimanja, zatim pokrenite vježbu teksta.'
code_instructions_1: 'keydr može preuzeti otvoreni kod s GitHuba za vježbu tipkanja.'
code_instructions_2: 'Kod se preuzima jednom i lokalno pohranjuje.'
code_instructions_3: 'Konfigurirajte postavke preuzimanja, zatim pokrenite vježbu koda.'
# Status messages (from app.rs)
status:
recovery_files: 'Pronađene datoteke za oporavak od prekinutog uvoza. Podaci mogu biti nekonzistentni — razmislite o ponovnom uvozu.'
dir_not_exist: 'Mapa ne postoji: %{path}'
no_data_store: 'Nema dostupnog spremišta podataka'
serialization_error: 'Greška serijalizacije: %{error}'
exported_to: 'Izvezeno u %{path}'
export_failed: 'Izvoz neuspješan: %{error}'
could_not_read: 'Nije moguće pročitati datoteku: %{error}'
invalid_export: 'Nevaljana izvozna datoteka: %{error}'
unsupported_version: 'Nepodržana verzija izvoza: %{got} (očekivano %{expected})'
import_failed: 'Uvoz neuspješan: %{error}'
imported_theme_fallback: 'Uvoz uspješan (tema ''%{theme}'' nije pronađena, koristi se zadana)'
imported_success: 'Uvoz uspješan'
adaptive_unavailable: 'Prilagodljivi ocijenjeni način nedostupan: %{error}'
switched_to: 'Prebačeno na %{name}'
layout_changed: 'Raspored promijenjen na %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Nepoznat jezik: %{key}'
unknown_layout: 'Nepoznat raspored tipkovnice: %{key}'
unsupported_pair: 'Nepodržani par jezik/raspored: %{language} + %{layout}'
language_blocked: 'Jezik je blokiran razinom podrške: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Natrag'

454
locales/hu.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminál gépelésgyakorló'
adaptive_drill: 'Adaptív gyakorlat'
adaptive_drill_desc: 'Fonetikus szavak adaptív betűfeloldással'
code_drill: 'Kód gyakorlat'
code_drill_desc: 'Kódszintaxis gépelés gyakorlása'
passage_drill: 'Szöveg gyakorlat'
passage_drill_desc: 'Könyvekből származó szövegek gépelése'
skill_tree: 'Képességfa'
skill_tree_desc: 'Haladási ágak megtekintése és gyakorlatok indítása'
keyboard: 'Billentyűzet'
keyboard_desc: 'Billentyűzetkiosztás és billentyűstatisztikák felfedezése'
statistics: 'Statisztika'
statistics_desc: 'Gépelési statisztikák megtekintése'
settings: 'Beállítások'
settings_desc: 'keydr konfigurálása'
day_streak: ' | %{days} napos sorozat'
key_progress: ' Billentyűhaladás %{unlocked}/%{total} (%{mastered} elsajátítva) | Cél %{target} WPM%{streak}'
hint_start: '[1-3] Indítás'
hint_skill_tree: '[t] Képességfa'
hint_keyboard: '[b] Billentyűzet'
hint_stats: '[s] Statisztika'
hint_settings: '[c] Beállítások'
hint_quit: '[q] Kilépés'
# Drill screen
drill:
title: ' Gyakorlat '
mode_adaptive: 'Adaptív'
mode_code: 'Kód (nem értékelt)'
mode_passage: 'Szöveg (nem értékelt)'
focus_char: 'Fókusz: ''%{ch}'''
focus_bigram: 'Fókusz: "%{bigram}"'
focus_both: 'Fókusz: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Pont'
header_err: 'Hiba'
code_source: ' Kódforrás '
passage_source: ' Szövegforrás '
footer: '[ESC] Gyakorlat vége [Backspace] Törlés'
keys_reenabled: 'Billentyűk újra aktívak %{ms}ms múlva'
hint_end: '[ESC] Gyakorlat vége'
hint_backspace: '[Backspace] Törlés'
# Dashboard / drill result
dashboard:
title: ' Gyakorlat kész '
results: 'Eredmények'
unranked_note_prefix: ' (Nem értékelt'
unranked_note_suffix: ' nem számít a képességfába)'
speed: ' Sebesség: '
accuracy_label: ' Pontosság:'
time_label: ' Idő: '
errors_label: ' Hibák: '
correct_detail: ' (%{correct}/%{total} helyes)'
input_blocked: ' Bevitel ideiglenesen blokkolva '
input_blocked_ms: '(%{ms}ms hátra)'
hint_continue: '[c/Enter/Space] Tovább'
hint_retry: '[r] Újra'
hint_menu: '[q] Menü'
hint_stats: '[s] Statisztika'
hint_delete: '[x] Törlés'
# Stats sidebar (during drill)
sidebar:
title: ' Statisztika '
wpm: 'WPM: '
target: 'Cél: '
target_wpm: '%{wpm} WPM'
accuracy: 'Pontosság: '
progress: 'Haladás: '
correct: 'Helyes: '
errors: 'Hibák: '
time: 'Idő: '
last_drill: ' Utolsó gyakorlat '
vs_avg: ' vs átlag: '
# Statistics dashboard
stats:
title: ' Statisztika '
empty: 'Még nincs befejezett gyakorlat. Kezdjen gépelni!'
tab_dashboard: '[1] Áttekintés'
tab_history: '[2] Előzmények'
tab_activity: '[3] Aktivitás'
tab_accuracy: '[4] Pontosság'
tab_timing: '[5] Időzítés'
tab_ngrams: '[6] N-gramok'
hint_back: '[ESC] Vissza'
hint_next_tab: '[Tab] Következő fül'
hint_switch_tab: '[1-6] Fül váltás'
hint_navigate: '[j/k] Navigáció'
hint_page: '[PgUp/PgDn] Lap'
hint_delete: '[x] Törlés'
summary_title: ' Összefoglaló '
drills: ' Gyakorlatok: '
avg_wpm: ' Átl. WPM: '
best_wpm: ' Legjobb WPM: '
accuracy_label: ' Pontosság: '
total_time: ' Összes idő: '
wpm_chart_title: ' WPM gyakorlatonként (Utolsó 20, Cél: %{target}) '
accuracy_chart_title: ' Pontosság %% (Utolsó 50 gyakorlat) '
chart_drill: 'Gyakorlat #'
chart_accuracy_pct: 'Pontosság %%'
sessions_title: ' Legutóbbi munkamenetek '
session_header: ' # WPM Raw Pont%% Idő Dátum/Idő Mód Értékelt Részl.'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Munkamenet #%{idx} törlése? (y/n)'
confirm_title: ' Megerősítés '
yes: 'igen'
no: 'nem'
keyboard_accuracy_title: ' Billentyűzet pontosság %% '
keyboard_timing_title: ' Billentyűzet időzítés (ms) '
slowest_keys_title: ' Leglassabb billentyűk (ms) '
fastest_keys_title: ' Leggyorsabb billentyűk (ms) '
worst_accuracy_title: ' Legrosszabb pontosság (%%) '
best_accuracy_title: ' Legjobb pontosság (%%) '
not_enough_data: ' Nincs elég adat'
streaks_title: ' Sorozatok '
current_streak: ' Jelenlegi: '
best_streak: ' Legjobb: '
active_days: ' Aktív napok: '
top_days_none: ' Legjobb napok: nincs'
top_days: ' Legjobb napok: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Pont: %{pct}%%'
keys_label: ' Billentyűk: %{unlocked}/%{total} (%{mastered} elsajátítva)'
ngram_empty: 'Végezzen el néhány adaptív gyakorlatot az n-gram adatok megjelenítéséhez'
ngram_header_speed_narrow: ' Bgrm Seb Várh Anom%'
ngram_header_error_narrow: ' Bgrm Hib Mnt Arány Vrh Anom%'
ngram_header_speed: ' Bigram Seb Várható Minták Anom%'
ngram_header_error: ' Bigram Hibák Minták Arány Várható Anom%'
focus_title: ' Aktív fókusz '
focus_char_label: ' Fókusz: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'hiba'
anomaly_speed: 'sebesség'
focus_detail_both: ' Kar. ''%{ch}'': leggyengébb billentyű | Bigram %{label}: %{type} anomália %{pct}%%'
focus_detail_char_only: ' Kar. ''%{ch}'': leggyengébb billentyű, nincs megerősített bigram anomália'
focus_detail_bigram_only: ' (%{type} anomália: %{pct}%%)'
focus_empty: ' Végezzen el néhány adaptív gyakorlatot a fókusz adatok megjelenítéséhez'
error_anomalies_title: ' Hiba anomáliák (%{count}) '
no_error_anomalies: ' Nem észlelt hiba anomáliák'
speed_anomalies_title: ' Sebesség anomáliák (%{count}) '
no_speed_anomalies: ' Nem észlelt sebesség anomáliák'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Kar. ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Napi aktivitás (munkamenetek naponta) '
jan: 'Jan'
feb: 'Feb'
mar: 'Már'
apr: 'Ápr'
may: 'Máj'
jun: 'Jún'
jul: 'Júl'
aug: 'Aug'
sep: 'Sze'
oct: 'Okt'
nov: 'Nov'
dec: 'Dec'
# Chart
chart:
wpm_over_time: ' WPM időben '
drill_number: 'Gyakorlat #'
# Settings
settings:
title: ' Beállítások '
subtitle: 'Nyilakkal navigáljon, Enter/Jobbra a módosításhoz, ESC a mentéshez'
target_wpm: 'Cél WPM'
theme: 'Téma'
word_count: 'Szószám'
ui_language: 'Felület nyelve'
dictionary_language: 'Szótár nyelve'
keyboard_layout: 'Billentyűzetkiosztás'
code_language: 'Programnyelv'
code_downloads: 'Kód letöltések'
on: 'Be'
off: 'Ki'
code_download_dir: 'Kód letöltési mappa'
snippets_per_repo: 'Részletek repónként'
unlimited: 'Korlátlan'
download_code_now: 'Kód letöltése most'
run_downloader: 'Letöltő futtatása'
passage_downloads: 'Szöveg letöltések'
passage_download_dir: 'Szöveg letöltési mappa'
paragraphs_per_book: 'Bekezdések könyvenként'
whole_book: 'Teljes könyv'
download_passages_now: 'Szövegek letöltése most'
export_path: 'Exportálási útvonal'
export_data: 'Adatok exportálása'
export_now: 'Exportálás most'
import_path: 'Importálási útvonal'
import_data: 'Adatok importálása'
import_now: 'Importálás most'
hint_save_back: '[ESC] Mentés és vissza'
hint_change_value: '[Enter/nyilak] Érték módosítása'
hint_edit_path: '[Enter az útvonalon] Szerkesztés'
hint_move: '[←→] Mozgatás'
hint_tab_complete: '[Tab] Kiegészítés (végén)'
hint_confirm: '[Enter] Megerősítés'
hint_cancel: '[Esc] Mégse'
success_title: ' Sikeres '
error_title: ' Hiba '
press_any_key: 'Nyomjon egy billentyűt'
file_exists_title: ' Fájl létezik '
file_exists: 'Egy fájl már létezik ezen az útvonalon.'
overwrite_rename: '[d] Felülírás [r] Átnevezés [Esc] Mégse'
erase_warning: 'Ez törli a jelenlegi adatokat.'
export_first: 'Előbb exportáljon, ha meg szeretné tartani.'
proceed_yn: 'Folytatja? (y/n)'
confirm_import_title: ' Importálás megerősítése '
# Selection screens
select:
dictionary_language_title: ' Szótár nyelv kiválasztása '
keyboard_layout_title: ' Billentyűzetkiosztás kiválasztása '
code_language_title: ' Programnyelv kiválasztása '
passage_source_title: ' Szövegforrás kiválasztása '
ui_language_title: ' Felület nyelvének kiválasztása '
more_above: '... még %{count} feljebb ...'
more_below: '... még %{count} lejjebb ...'
current: ' (jelenlegi)'
disabled: ' (letiltva)'
enabled_default: ' (engedélyezve, alapért.: %{layout})'
enabled: ' (engedélyezve)'
disabled_blocked: ' (letiltva: blokkolva)'
built_in: ' (beépített)'
cached: ' (gyorsítótárazva)'
disabled_download: ' (letiltva: letöltés szükséges)'
download_required: ' (letöltés szükséges)'
hint_navigate: '[Fel/Le/PgUp/PgDn] Navigáció'
hint_confirm: '[Enter] Megerősítés'
hint_back: '[ESC] Vissza'
language_resets_layout: 'A nyelv kiválasztása visszaállítja a billentyűzetkiosztást az adott nyelv alapértelmezésére.'
layout_no_language_change: 'A kiosztás módosítása nem változtatja meg a szótár nyelvét.'
disabled_network_notice: 'Egyes nyelvek letiltva: engedélyezze a hálózati letöltéseket a bevezetőben/beállításokban.'
disabled_sources_notice: 'Egyes források letiltva: engedélyezze a hálózati letöltéseket a bevezetőben/beállításokban.'
passage_all: 'Összes (Beépített + minden könyv)'
passage_builtin: 'Csak beépített szövegek'
passage_book_prefix: 'Könyv: %{title}'
# Progress
progress:
overall_key_progress: 'Általános billentyűhaladás'
unlocked_mastered: '%{unlocked}/%{total} feloldva (%{mastered} elsajátítva)'
# Skill tree
skill_tree:
title: ' Képességfa '
locked: 'Zárolva'
unlocked: 'feloldva'
mastered: 'elsajátítva'
in_progress: 'folyamatban'
complete: 'kész'
locked_status: 'zárolva'
locked_notice: 'Végezzen el %{count} elsődleges betűt az ágak feloldásához'
branches_separator: 'Ágak (elérhető %{count} elsődleges betű után)'
unlocked_letters: 'Feloldva %{unlocked}/%{total} betű'
level: 'Szint %{current}/%{total}'
level_zero: 'Szint 0/%{total}'
in_focus: ' fókuszban'
hint_navigate: '[↑↓/jk] Navigáció'
hint_scroll: '[PgUp/PgDn vagy Ctrl+U/Ctrl+D] Görgetés'
hint_back: '[q] Vissza'
hint_unlock: '[Enter] Feloldás'
hint_start_drill: '[Enter] Gyakorlat indítása'
unlock_msg_1: 'Feloldás után az alapértelmezett adaptív gyakorlat bevonja az ág feloldott billentyűit.'
unlock_msg_2: 'Ha csak erre az ágra szeretne összpontosítani, indítson gyakorlatot közvetlenül a képességfából.'
confirm_unlock: '%{branch} feloldása?'
confirm_yn: '[y] Feloldás [n/ESC] Mégse'
lvl_prefix: 'Szint'
branch_primary_letters: 'Elsődleges betűk'
branch_capital_letters: 'Nagybetűk'
branch_numbers: 'Számok 0-9'
branch_prose_punctuation: 'Írásjelek'
branch_whitespace: 'Szóközök'
branch_code_symbols: 'Kódszimbólumok'
level_frequency_order: 'Gyakoriság szerinti sorrend'
level_common_sentence_capitals: 'Gyakori mondatkezdő nagybetűk'
level_name_capitals: 'Névkezdő nagybetűk'
level_remaining_capitals: 'Fennmaradó nagybetűk'
level_common_digits: 'Gyakori számjegyek'
level_all_digits: 'Összes számjegy'
level_essential: 'Alapvető'
level_common: 'Gyakori'
level_expressive: 'Kifejező'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Behúzás'
level_arithmetic_assignment: 'Aritmetika és hozzárendelés'
level_grouping: 'Csoportosítás'
level_logic_reference: 'Logika és hivatkozás'
level_special: 'Speciális'
# Milestones
milestones:
unlock_title: ' Billentyű feloldva! '
mastery_title: ' Billentyű elsajátítva! '
branches_title: ' Új képességágak elérhetők! '
branch_complete_title: ' Ág befejezve! '
all_unlocked_title: ' Minden billentyű feloldva! '
all_mastered_title: ' Teljes billentyűzet elsajátítva! '
unlocked: 'feloldva'
mastered: 'elsajátítva'
use_finger: 'Használja a %{finger} ujját'
hold_right_shift: 'Tartsa a jobb Shift-et (jobb kisujj)'
hold_left_shift: 'Tartsa a bal Shift-et (bal kisujj)'
congratulations_all_letters: 'Gratulálunk! Elsajátította mind a %{count} elsődleges betűt'
new_branches_available: 'Új képességágak elérhetők:'
visit_skill_tree: 'Látogasson el a képességfába egy új ág feloldásához'
and_start_training: 'és kezdje el a gyakorlást!'
open_skill_tree: 'Nyomja meg a [t] gombot a képességfa megnyitásához'
branch_complete_msg: 'Befejezte a %{branch} ágat!'
all_levels_mastered: 'Mind a %{count} szint elsajátítva.'
all_keys_confident: 'Minden billentyű ebben az ágban teljes megbízhatóságú.'
all_unlocked_msg: 'Feloldotta a billentyűzet összes billentyűjét!'
all_unlocked_desc: 'Minden karakter, szimbólum és módosító elérhető a gyakorlatokban.'
keep_practicing_mastery: 'Folytassa a gyakorlást a mesteri szint eléréséhez — ha minden billentyű eléri a teljes'
confidence_complete: 'megbízhatóságot, elérte a teljes billentyűzet elsajátítását!'
all_mastered_msg: 'Gratulálunk — elérte a teljes billentyűzet elsajátítását!'
all_mastered_desc: 'A billentyűzet minden billentyűje maximális megbízhatóságú.'
mastery_takes_practice: 'Az elsajátítás nem végállomás — folyamatos gyakorlást igényel.'
keep_drilling: 'Folytassa a gyakorlást, hogy megőrizze szintjét.'
hint_skill_tree_continue: '[t] Képességfa megnyitása [Bármely billentyű] Tovább'
hint_any_key: 'Nyomjon bármely billentyűt a folytatáshoz'
input_blocked: 'Bevitel ideiglenesen blokkolva (%{ms}ms hátra)'
unlock_msg_1: 'Szép munka! Fejlessze tovább gépelési készségeit.'
unlock_msg_2: 'Újabb billentyű az arzenáljában!'
unlock_msg_3: 'A billentyűzete bővül! Így tovább.'
unlock_msg_4: 'Egy lépéssel közelebb a teljes elsajátításhoz!'
mastery_msg_1: 'Ez a billentyű most teljes megbízhatóságú!'
mastery_msg_2: 'Ezt a billentyűt tökéletesen tudja!'
mastery_msg_3: 'Izommemória rögzítve!'
mastery_msg_4: 'Még egy billentyű meghódítva!'
# Keyboard explorer
keyboard:
title: ' Billentyűzet '
subtitle: 'Nyomjon meg bármely billentyűt vagy kattintson'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigáció'
hint_back: '[q/ESC] Vissza'
key_label: 'Billentyű: '
finger_label: 'Ujj: '
hand_left: 'Bal'
hand_right: 'Jobb'
finger_index: 'Mutatóujj'
finger_middle: 'Középső ujj'
finger_ring: 'Gyűrűsujj'
finger_pinky: 'Kisujj'
finger_thumb: 'Hüvelykujj'
overall_accuracy: ' Összesített pontosság: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Értékelt pontosság: %{correct}/%{total} (%{pct}%%)'
confidence: 'Megbízhatóság: '
no_data: 'Még nincs adat'
no_data_short: 'Nincs adat'
key_details: ' Billentyű részletei '
key_details_char: ' Billentyű részletei: ''%{ch}'' '
key_details_name: ' Billentyű részletei: %{name} '
press_key_hint: 'Nyomjon egy billentyűt a részletekhez'
shift_label: 'Shift: '
shift_no: 'Nem'
overall_avg_time: 'Össz. átl. idő: '
overall_best_time: 'Össz. legjobb idő: '
overall_samples: 'Össz. minták: '
overall_accuracy_label: 'Össz. pontosság: '
branch_label: 'Ág: '
level_label: 'Szint: '
built_in_key: 'Beépített billentyű'
unlocked_label: 'Feloldva: '
yes: 'Igen'
no: 'Nem'
in_focus_label: 'Fókuszban?: '
mastery_label: 'Elsajátítás: '
mastery_locked: 'Zárolva'
ranked_avg_time: 'Ért. átl. idő: '
ranked_best_time: 'Ért. legjobb idő: '
ranked_samples: 'Ért. minták: '
ranked_accuracy_label: 'Ért. pontosság: '
# Intro dialogs
intro:
passage_title: ' Szövegletöltés beállítása '
code_title: ' Kódletöltés beállítása '
enable_downloads: 'Hálózati letöltések engedélyezése'
download_dir: 'Letöltési mappa'
paragraphs_per_book: 'Bekezdések könyvenként (0 = teljes)'
whole_book: 'teljes könyv'
snippets_per_repo: 'Részletek repónként (0 = korlátlan)'
unlimited: 'korlátlan'
start_passage_drill: 'Szöveg gyakorlat indítása'
start_code_drill: 'Kód gyakorlat indítása'
confirm: 'Megerősítés'
hint_navigate: '[Fel/Le] Navigáció'
hint_adjust: '[Bal/Jobb] Állítás'
hint_edit: '[Gépelés/Backspace] Szerkesztés'
hint_confirm: '[Enter] Megerősítés'
hint_cancel: '[ESC] Mégse'
preparing_download: 'Letöltés előkészítése...'
download_passage_title: ' Szövegforrás letöltése '
download_code_title: ' Kódforrás letöltése '
book_label: ' Könyv: %{name}'
repo_label: ' Repó: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bájt'
downloaded_bytes: 'Letöltve: %{bytes} bájt'
downloading_book_progress: 'Könyv letöltése: [%{bar}] %{downloaded}/%{total} bájt'
downloading_book_bytes: 'Könyv letöltése: %{bytes} bájt'
downloading_code_progress: 'Letöltés: [%{bar}] %{downloaded}/%{total} bájt'
downloading_code_bytes: 'Letöltés: %{bytes} bájt'
current_book: 'Jelenlegi: %{name} (könyv %{done}/%{total})'
current_repo: 'Jelenlegi: %{name} (repó %{done}/%{total})'
passage_instructions_1: 'A keydr letölthet szövegeket a Project Gutenbergből gépelésgyakorláshoz.'
passage_instructions_2: 'A könyvek egyszer töltődnek le és helyben tárolódnak.'
passage_instructions_3: 'Állítsa be a letöltési beállításokat, majd indítson szöveg gyakorlatot.'
code_instructions_1: 'A keydr letölthet nyílt forráskódot a GitHubról gépelésgyakorláshoz.'
code_instructions_2: 'A kód egyszer töltődik le és helyben tárolódik.'
code_instructions_3: 'Állítsa be a letöltési beállításokat, majd indítson kód gyakorlatot.'
# Status messages (from app.rs)
status:
recovery_files: 'Helyreállítási fájlok találhatók megszakított importálásból. Az adatok inkonzisztensek lehetnek — fontolja meg az újraimportálást.'
dir_not_exist: 'A mappa nem létezik: %{path}'
no_data_store: 'Nincs elérhető adattár'
serialization_error: 'Sorosítási hiba: %{error}'
exported_to: 'Exportálva ide: %{path}'
export_failed: 'Exportálás sikertelen: %{error}'
could_not_read: 'Nem sikerült olvasni a fájlt: %{error}'
invalid_export: 'Érvénytelen exportfájl: %{error}'
unsupported_version: 'Nem támogatott exportverzió: %{got} (várt: %{expected})'
import_failed: 'Importálás sikertelen: %{error}'
imported_theme_fallback: 'Importálás sikeres (''%{theme}'' téma nem található, alapértelmezett használva)'
imported_success: 'Importálás sikeres'
adaptive_unavailable: 'Adaptív értékelt mód nem elérhető: %{error}'
switched_to: 'Átváltva erre: %{name}'
layout_changed: 'Kiosztás megváltoztatva: %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Ismeretlen nyelv: %{key}'
unknown_layout: 'Ismeretlen billentyűzetkiosztás: %{key}'
unsupported_pair: 'Nem támogatott nyelv/kiosztás pár: %{language} + %{layout}'
language_blocked: 'A nyelv blokkolva a támogatási szint által: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Vissza'

454
locales/it.yml Normal file
View File

@@ -0,0 +1,454 @@
# Menu principale
menu:
subtitle: 'Tutor di Digitazione nel Terminale'
adaptive_drill: 'Esercizio Adattivo'
adaptive_drill_desc: 'Parole fonetiche con sblocco adattivo dei tasti'
code_drill: 'Esercizio di Codice'
code_drill_desc: 'Esercitati a digitare sintassi di codice'
passage_drill: 'Esercizio di Brano'
passage_drill_desc: 'Digita brani da libri'
skill_tree: 'Albero delle Abilità'
skill_tree_desc: 'Visualizza rami di progressione e avvia esercizi'
keyboard: 'Tastiera'
keyboard_desc: 'Esplora il layout della tastiera e le statistiche'
statistics: 'Statistiche'
statistics_desc: 'Visualizza le tue statistiche di digitazione'
settings: 'Impostazioni'
settings_desc: 'Configura keydr'
day_streak: ' | %{days} giorni consecutivi'
key_progress: ' Progresso Tasti %{unlocked}/%{total} (%{mastered} padroneggiati) | Obiettivo %{target} WPM%{streak}'
hint_start: '[1-3] Avvia'
hint_skill_tree: '[t] Albero delle Abilità'
hint_keyboard: '[b] Tastiera'
hint_stats: '[s] Statistiche'
hint_settings: '[c] Impostazioni'
hint_quit: '[q] Esci'
# Schermata esercizio
drill:
title: ' Esercizio '
mode_adaptive: 'Adattivo'
mode_code: 'Codice (Non classificato)'
mode_passage: 'Brano (Non classificato)'
focus_char: 'Focus: ''%{ch}'''
focus_bigram: 'Focus: "%{bigram}"'
focus_both: 'Focus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Pre'
header_err: 'Err'
code_source: ' Sorgente codice '
passage_source: ' Sorgente brano '
footer: '[ESC] Fine [Backspace] Cancella'
keys_reenabled: 'Tasti riattivati in %{ms}ms'
hint_end: '[ESC] Fine esercizio'
hint_backspace: '[Backspace] Cancella'
# Pannello / risultato dell'esercizio
dashboard:
title: ' Esercizio Completato '
results: 'Risultati'
unranked_note_prefix: ' (Non classificato'
unranked_note_suffix: ' non conta per l''albero delle abilità)'
speed: ' Velocità: '
accuracy_label: ' Precisione: '
time_label: ' Tempo: '
errors_label: ' Errori: '
correct_detail: ' (%{correct}/%{total} corretti)'
input_blocked: ' Input temporaneamente bloccato '
input_blocked_ms: '(%{ms}ms rimanenti)'
hint_continue: '[c/Enter/Space] Continua'
hint_retry: '[r] Riprova'
hint_menu: '[q] Menu'
hint_stats: '[s] Statistiche'
hint_delete: '[x] Elimina'
# Barra laterale statistiche (durante l'esercizio)
sidebar:
title: ' Statistiche '
wpm: 'WPM: '
target: 'Obiettivo: '
target_wpm: '%{wpm} WPM'
accuracy: 'Precisione: '
progress: 'Progresso: '
correct: 'Corretti: '
errors: 'Errori: '
time: 'Tempo: '
last_drill: ' Ultimo Esercizio '
vs_avg: ' vs med: '
# Pannello statistiche
stats:
title: ' Statistiche '
empty: 'Nessun esercizio completato. Inizia a digitare!'
tab_dashboard: '[1] Pannello'
tab_history: '[2] Cronologia'
tab_activity: '[3] Attività'
tab_accuracy: '[4] Precisione'
tab_timing: '[5] Tempistica'
tab_ngrams: '[6] N-grammi'
hint_back: '[ESC] Indietro'
hint_next_tab: '[Tab] Scheda successiva'
hint_switch_tab: '[1-6] Cambia scheda'
hint_navigate: '[j/k] Naviga'
hint_page: '[PgUp/PgDn] Pagina'
hint_delete: '[x] Elimina'
summary_title: ' Riepilogo '
drills: ' Esercizi: '
avg_wpm: ' WPM Med: '
best_wpm: ' Miglior WPM: '
accuracy_label: ' Precisione: '
total_time: ' Tempo totale: '
wpm_chart_title: ' WPM per Esercizio (Ultimi 20, Obiettivo: %{target}) '
accuracy_chart_title: ' Precisione %% (Ultimi 50 Esercizi) '
chart_drill: 'Esercizio #'
chart_accuracy_pct: 'Precisione %%'
sessions_title: ' Sessioni Recenti '
session_header: ' # WPM Raw Pre%% Tempo Data/Ora Modo Class. Parziale'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Eliminare sessione #%{idx}? (y/n)'
confirm_title: ' Conferma '
yes: 'sì'
no: 'no'
keyboard_accuracy_title: ' Precisione Tastiera %% '
keyboard_timing_title: ' Tempistica Tastiera (ms) '
slowest_keys_title: ' Tasti più Lenti (ms) '
fastest_keys_title: ' Tasti più Veloci (ms) '
worst_accuracy_title: ' Peggiore Precisione (%%) '
best_accuracy_title: ' Migliore Precisione (%%) '
not_enough_data: ' Dati insufficienti'
streaks_title: ' Serie '
current_streak: ' Attuale: '
best_streak: ' Migliore: '
active_days: ' Giorni attivi: '
top_days_none: ' Giorni migliori: nessuno'
top_days: ' Giorni migliori: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Pre: %{pct}%%'
keys_label: ' Tasti: %{unlocked}/%{total} (%{mastered} padroneggiati)'
ngram_empty: 'Completa esercizi adattivi per vedere dati n-grammi'
ngram_header_speed_narrow: ' Bgrm Vel Att Anom%'
ngram_header_error_narrow: ' Bgrm Err Cmp Tasso Att Anom%'
ngram_header_speed: ' Bigramma Vel Att Camp. Anom%'
ngram_header_error: ' Bigramma Errori Camp. Tasso Att Anom%'
focus_title: ' Focus attivo '
focus_char_label: ' Focus: '
focus_bigram_value: 'Bigramma %{label}'
focus_plus: ' + '
anomaly_error: 'errore'
anomaly_speed: 'velocità'
focus_detail_both: ' Carattere ''%{ch}'': tasto più debole | Bigramma %{label}: anomalia di %{type} %{pct}%%'
focus_detail_char_only: ' Carattere ''%{ch}'': tasto più debole, nessuna anomalia di bigramma confermata'
focus_detail_bigram_only: ' (anomalia di %{type}: %{pct}%%)'
focus_empty: ' Completa esercizi adattivi per vedere dati di focus'
error_anomalies_title: ' Anomalie di Errore (%{count}) '
no_error_anomalies: ' Nessuna anomalia di errore rilevata'
speed_anomalies_title: ' Anomalie di Velocità (%{count}) '
no_speed_anomalies: ' Nessuna anomalia di velocità rilevata'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Esi: >%{ms}ms'
focus_char_value: 'Carattere ''%{ch}'''
# Mappa di attività
heatmap:
title: ' Attività Giornaliera (Sessioni per Giorno) '
jan: 'Gen'
feb: 'Feb'
mar: 'Mar'
apr: 'Apr'
may: 'Mag'
jun: 'Giu'
jul: 'Lug'
aug: 'Ago'
sep: 'Set'
oct: 'Ott'
nov: 'Nov'
dec: 'Dic'
# Grafico
chart:
wpm_over_time: ' WPM nel Tempo '
drill_number: 'Esercizio #'
# Impostazioni
settings:
title: ' Impostazioni '
subtitle: 'Usa le frecce per navigare, Invio/Destra per cambiare, ESC per salvare e uscire'
target_wpm: 'WPM Obiettivo'
theme: 'Tema'
word_count: 'Numero di Parole'
ui_language: 'Lingua dell''Interfaccia'
dictionary_language: 'Lingua del Dizionario'
keyboard_layout: 'Layout della Tastiera'
code_language: 'Linguaggio di Codice'
code_downloads: 'Download di Codice'
on: 'Sì'
off: 'No'
code_download_dir: 'Dir. Download Codice'
snippets_per_repo: 'Frammenti per Repo'
unlimited: 'Illimitato'
download_code_now: 'Scarica Codice Ora'
run_downloader: 'Avvia downloader'
passage_downloads: 'Download di Brani'
passage_download_dir: 'Dir. Download Brani'
paragraphs_per_book: 'Paragrafi per Libro'
whole_book: 'Libro intero'
download_passages_now: 'Scarica Brani Ora'
export_path: 'Percorso di Esportazione'
export_data: 'Esporta Dati'
export_now: 'Esporta ora'
import_path: 'Percorso di Importazione'
import_data: 'Importa Dati'
import_now: 'Importa ora'
hint_save_back: '[ESC] Salva e indietro'
hint_change_value: '[Enter/frecce] Cambia valore'
hint_edit_path: '[Enter su percorso] Modifica'
hint_move: '[←→] Sposta'
hint_tab_complete: '[Tab] Completa (alla fine)'
hint_confirm: '[Enter] Conferma'
hint_cancel: '[Esc] Annulla'
success_title: ' Successo '
error_title: ' Errore '
press_any_key: 'Premi un tasto qualsiasi'
file_exists_title: ' File Esistente '
file_exists: 'Un file esiste già in questo percorso.'
overwrite_rename: '[d] Sovrascrivi [r] Rinomina [Esc] Annulla'
erase_warning: 'Questo cancellerà i tuoi dati attuali.'
export_first: 'Esporta prima se vuoi conservarli.'
proceed_yn: 'Procedere? (y/n)'
confirm_import_title: ' Conferma Importazione '
# Schermate di selezione
select:
dictionary_language_title: ' Seleziona Lingua del Dizionario '
keyboard_layout_title: ' Seleziona Layout della Tastiera '
code_language_title: ' Seleziona Linguaggio di Codice '
passage_source_title: ' Seleziona Sorgente Brani '
ui_language_title: ' Seleziona Lingua dell''Interfaccia '
more_above: '... %{count} altri sopra ...'
more_below: '... %{count} altri sotto ...'
current: ' (attuale)'
disabled: ' (disattivato)'
enabled_default: ' (attivato, predefinito: %{layout})'
enabled: ' (attivato)'
disabled_blocked: ' (disattivato: bloccato)'
built_in: ' (integrato)'
cached: ' (in cache)'
disabled_download: ' (disattivato: download richiesto)'
download_required: ' (download richiesto)'
hint_navigate: '[Up/Down/PgUp/PgDn] Naviga'
hint_confirm: '[Enter] Conferma'
hint_back: '[ESC] Indietro'
language_resets_layout: 'Selezionare una lingua reimposta il layout a quello predefinito di quella lingua.'
layout_no_language_change: 'Cambiare layout non cambia la lingua del dizionario.'
disabled_network_notice: 'Alcune lingue sono disattivate: attiva i download in intro/impostazioni.'
disabled_sources_notice: 'Alcune sorgenti sono disattivate: attiva i download in intro/impostazioni.'
passage_all: 'Tutti (Integrati + tutti i libri)'
passage_builtin: 'Solo brani integrati'
passage_book_prefix: 'Libro: %{title}'
# Progresso
progress:
overall_key_progress: 'Progresso Globale dei Tasti'
unlocked_mastered: '%{unlocked}/%{total} sbloccati (%{mastered} padroneggiati)'
# Albero delle abilità
skill_tree:
title: ' Albero delle Abilità '
locked: 'Bloccato'
unlocked: 'sbloccato'
mastered: 'padroneggiato'
in_progress: 'in corso'
complete: 'completato'
locked_status: 'bloccato'
locked_notice: 'Completa %{count} lettere primarie per sbloccare i rami'
branches_separator: 'Rami (disponibili dopo %{count} lettere primarie)'
unlocked_letters: 'Sbloccate %{unlocked}/%{total} lettere'
level: 'Livello %{current}/%{total}'
level_zero: 'Livello 0/%{total}'
in_focus: ' in focus'
hint_navigate: '[↑↓/jk] Naviga'
hint_scroll: '[PgUp/PgDn o Ctrl+U/Ctrl+D] Scorri'
hint_back: '[q] Indietro'
hint_unlock: '[Enter] Sblocca'
hint_start_drill: '[Enter] Avvia Esercizio'
unlock_msg_1: 'Una volta sbloccato, l''esercizio adattivo includerà i tasti di questo ramo che sono sbloccati.'
unlock_msg_2: 'Se vuoi concentrarti solo su questo ramo, avvia un esercizio direttamente da questo ramo nell''Albero delle Abilità.'
confirm_unlock: 'Sbloccare %{branch}?'
confirm_yn: '[y] Sblocca [n/ESC] Annulla'
lvl_prefix: 'Liv'
branch_primary_letters: 'Lettere Primarie'
branch_capital_letters: 'Lettere Maiuscole'
branch_numbers: 'Numeri 0-9'
branch_prose_punctuation: 'Punteggiatura di Prosa'
branch_whitespace: 'Spazi Bianchi'
branch_code_symbols: 'Simboli di Codice'
level_frequency_order: 'Ordine di Frequenza'
level_common_sentence_capitals: 'Maiuscole di Frase Comuni'
level_name_capitals: 'Maiuscole di Nomi'
level_remaining_capitals: 'Maiuscole Rimanenti'
level_common_digits: 'Cifre Comuni'
level_all_digits: 'Tutte le Cifre'
level_essential: 'Essenziale'
level_common: 'Comune'
level_expressive: 'Espressivo'
level_enter_return: 'Invio/Ritorno'
level_tab_indent: 'Tab/Rientro'
level_arithmetic_assignment: 'Aritmetica e Assegnazione'
level_grouping: 'Raggruppamento'
level_logic_reference: 'Logica e Riferimento'
level_special: 'Speciale'
# Traguardi
milestones:
unlock_title: ' Tasto Sbloccato! '
mastery_title: ' Tasto Padroneggiato! '
branches_title: ' Nuovi Rami Disponibili! '
branch_complete_title: ' Ramo Completato! '
all_unlocked_title: ' Tutti i Tasti Sbloccati! '
all_mastered_title: ' Padronanza Totale della Tastiera! '
unlocked: 'sbloccato'
mastered: 'padroneggiato'
use_finger: 'Usa il tuo %{finger}'
hold_right_shift: 'Tieni premuto Shift Destro (mignolo destro)'
hold_left_shift: 'Tieni premuto Shift Sinistro (mignolo sinistro)'
congratulations_all_letters: 'Congratulazioni! Hai padroneggiato tutte le %{count} lettere primarie'
new_branches_available: 'Nuovi rami di abilità sono disponibili:'
visit_skill_tree: 'Visita l''Albero delle Abilità per sbloccare un nuovo ramo'
and_start_training: 'e inizia ad allenarti!'
open_skill_tree: 'Premi [t] per aprire l''Albero delle Abilità'
branch_complete_msg: 'Hai completato il ramo %{branch}!'
all_levels_mastered: 'Tutti i %{count} livelli padroneggiati.'
all_keys_confident: 'Ogni tasto in questo ramo è a confidenza massima.'
all_unlocked_msg: 'Hai sbloccato tutti i tasti della tastiera!'
all_unlocked_desc: 'Ogni carattere, simbolo e modificatore è disponibile nei tuoi esercizi.'
keep_practicing_mastery: 'Continua a esercitarti per raggiungere la padronanza — quando ogni tasto raggiungerà'
confidence_complete: 'la confidenza massima, avrai raggiunto la padronanza totale della tastiera!'
all_mastered_msg: 'Congratulazioni — hai raggiunto la padronanza totale della tastiera!'
all_mastered_desc: 'Ogni tasto della tastiera è a confidenza massima.'
mastery_takes_practice: 'La padronanza non è una destinazione — richiede pratica continua.'
keep_drilling: 'Continua ad esercitarti per mantenere il tuo livello.'
hint_skill_tree_continue: '[t] Apri Albero delle Abilità [Altro tasto] Continua'
hint_any_key: 'Premi un tasto qualsiasi per continuare'
input_blocked: 'Input temporaneamente bloccato (%{ms}ms rimanenti)'
unlock_msg_1: 'Ottimo lavoro! Continua a migliorare le tue abilità.'
unlock_msg_2: 'Un altro tasto aggiunto al tuo arsenale!'
unlock_msg_3: 'La tua tastiera cresce! Continua così.'
unlock_msg_4: 'Un passo più vicino alla padronanza totale!'
mastery_msg_1: 'Questo tasto è a confidenza massima!'
mastery_msg_2: 'Hai questo tasto sotto controllo!'
mastery_msg_3: 'Memoria muscolare acquisita!'
mastery_msg_4: 'Un altro tasto conquistato!'
# Esploratore tastiera
keyboard:
title: ' Tastiera '
subtitle: 'Premi o clicca su un tasto'
hint_navigate: '[←→↑↓/hjkl/Tab] Naviga'
hint_back: '[q/ESC] Indietro'
key_label: 'Tasto: '
finger_label: 'Dito: '
hand_left: 'Sinistra'
hand_right: 'Destra'
finger_index: 'Indice'
finger_middle: 'Medio'
finger_ring: 'Anulare'
finger_pinky: 'Mignolo'
finger_thumb: 'Pollice'
overall_accuracy: ' Precisione globale: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Precisione classificata: %{correct}/%{total} (%{pct}%%)'
confidence: 'Confidenza: '
no_data: 'Nessun dato ancora'
no_data_short: 'Nessun dato'
key_details: ' Dettagli Tasto '
key_details_char: ' Dettagli Tasto: ''%{ch}'' '
key_details_name: ' Dettagli Tasto: %{name} '
press_key_hint: 'Premi un tasto per vedere i suoi dettagli'
shift_label: 'Shift: '
shift_no: 'No'
overall_avg_time: 'Tempo Med. Globale: '
overall_best_time: 'Miglior Tempo Globale: '
overall_samples: 'Campioni Globali: '
overall_accuracy_label: 'Precisione Globale: '
branch_label: 'Ramo: '
level_label: 'Livello: '
built_in_key: 'Tasto Integrato'
unlocked_label: 'Sbloccato: '
yes: 'Sì'
no: 'No'
in_focus_label: 'In Focus?: '
mastery_label: 'Padronanza: '
mastery_locked: 'Bloccato'
ranked_avg_time: 'Tempo Med. Classificato: '
ranked_best_time: 'Miglior Tempo Classificato: '
ranked_samples: 'Campioni Classificati: '
ranked_accuracy_label: 'Precisione Classificata: '
# Dialoghi di introduzione
intro:
passage_title: ' Configurazione Download Brani '
code_title: ' Configurazione Download Codice '
enable_downloads: 'Attiva download di rete'
download_dir: 'Directory di download'
paragraphs_per_book: 'Paragrafi per libro (0 = intero)'
whole_book: 'libro intero'
snippets_per_repo: 'Frammenti per repo (0 = illimitato)'
unlimited: 'illimitato'
start_passage_drill: 'Avvia esercizio di brano'
start_code_drill: 'Avvia esercizio di codice'
confirm: 'Conferma'
hint_navigate: '[Up/Down] Naviga'
hint_adjust: '[Left/Right] Regola'
hint_edit: '[Type/Backspace] Modifica'
hint_confirm: '[Enter] Conferma'
hint_cancel: '[ESC] Annulla'
preparing_download: 'Preparazione download...'
download_passage_title: ' Download Sorgente Brano '
download_code_title: ' Download Sorgente Codice '
book_label: ' Libro: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bytes'
downloaded_bytes: 'Scaricato: %{bytes} bytes'
downloading_book_progress: 'Download libro attuale: [%{bar}] %{downloaded}/%{total} bytes'
downloading_book_bytes: 'Download libro attuale: %{bytes} bytes'
downloading_code_progress: 'Download: [%{bar}] %{downloaded}/%{total} bytes'
downloading_code_bytes: 'Download: %{bytes} bytes'
current_book: 'Attuale: %{name} (libro %{done}/%{total})'
current_repo: 'Attuale: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr può scaricare brani da Project Gutenberg per la pratica di digitazione.'
passage_instructions_2: 'I libri vengono scaricati una volta e salvati localmente.'
passage_instructions_3: 'Configura le impostazioni di download qui sotto, poi avvia un esercizio di brano.'
code_instructions_1: 'keydr può scaricare codice open-source da GitHub per la pratica di digitazione.'
code_instructions_2: 'Il codice viene scaricato una volta e salvato localmente.'
code_instructions_3: 'Configura le impostazioni di download qui sotto, poi avvia un esercizio di codice.'
# Messaggi di stato (da app.rs)
status:
recovery_files: 'Trovati file di recupero da un''importazione interrotta. I dati potrebbero essere incoerenti — considera di reimportare.'
dir_not_exist: 'La directory non esiste: %{path}'
no_data_store: 'Nessun archivio dati disponibile'
serialization_error: 'Errore di serializzazione: %{error}'
exported_to: 'Esportato in %{path}'
export_failed: 'Esportazione fallita: %{error}'
could_not_read: 'Impossibile leggere il file: %{error}'
invalid_export: 'File di esportazione non valido: %{error}'
unsupported_version: 'Versione di esportazione non supportata: %{got} (prevista %{expected})'
import_failed: 'Importazione fallita: %{error}'
imported_theme_fallback: 'Importato con successo (tema ''%{theme}'' non trovato, usando predefinito)'
imported_success: 'Importato con successo'
adaptive_unavailable: 'Modalità adattiva classificata non disponibile: %{error}'
switched_to: 'Passato a %{name}'
layout_changed: 'Layout cambiato in %{name}'
# Errori (per traduzione limiti UI)
errors:
unknown_language: 'Lingua sconosciuta: %{key}'
unknown_layout: 'Layout tastiera sconosciuto: %{key}'
unsupported_pair: 'Coppia lingua/layout non supportata: %{language} + %{layout}'
language_blocked: 'Lingua bloccata dal livello di supporto: %{key}'
# Comune
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Indietro'

454
locales/lt.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminalo spausdinimo treniruoklis'
adaptive_drill: 'Adaptyvi pratybos'
adaptive_drill_desc: 'Fonetiniai žodžiai su adaptyviniu raidžių atrakinimo'
code_drill: 'Kodo pratybos'
code_drill_desc: 'Praktikuokite kodo sintaksės spausdinimą'
passage_drill: 'Teksto pratybos'
passage_drill_desc: 'Spausdinkite ištraukas iš knygų'
skill_tree: 'Įgūdžių medis'
skill_tree_desc: 'Peržiūrėkite pažangos šakas ir pradėkite pratybas'
keyboard: 'Klaviatūra'
keyboard_desc: 'Naršykite klaviatūros išdėstymą ir klavišų statistiką'
statistics: 'Statistika'
statistics_desc: 'Peržiūrėkite spausdinimo statistiką'
settings: 'Nustatymai'
settings_desc: 'Konfigūruokite keydr'
day_streak: ' | %{days} d. serija'
key_progress: ' Klavišų pažanga %{unlocked}/%{total} (%{mastered} įvaldyta) | Tikslas %{target} WPM%{streak}'
hint_start: '[1-3] Pradėti'
hint_skill_tree: '[t] Įgūdžių medis'
hint_keyboard: '[b] Klaviatūra'
hint_stats: '[s] Statistika'
hint_settings: '[c] Nustatymai'
hint_quit: '[q] Išeiti'
# Drill screen
drill:
title: ' Pratybos '
mode_adaptive: 'Adaptyvi'
mode_code: 'Kodas (be vertinimo)'
mode_passage: 'Tekstas (be vertinimo)'
focus_char: 'Fokusuotis: ''%{ch}'''
focus_bigram: 'Fokusuotis: "%{bigram}"'
focus_both: 'Fokusuotis: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Tiksl'
header_err: 'Kld'
code_source: ' Kodo šaltinis '
passage_source: ' Teksto šaltinis '
footer: '[ESC] Baigti pratybas [Backspace] Trinti'
keys_reenabled: 'Klavišai vėl aktyvūs po %{ms}ms'
hint_end: '[ESC] Baigti pratybas'
hint_backspace: '[Backspace] Trinti'
# Dashboard / drill result
dashboard:
title: ' Pratybos baigtos '
results: 'Rezultatai'
unranked_note_prefix: ' (Be vertinimo'
unranked_note_suffix: ' neskaičiuojama įgūdžių medyje)'
speed: ' Greitis: '
accuracy_label: ' Tikslumas:'
time_label: ' Laikas: '
errors_label: ' Klaidos: '
correct_detail: ' (%{correct}/%{total} teisingai)'
input_blocked: ' Įvestis laikinai blokuota '
input_blocked_ms: '(%{ms}ms liko)'
hint_continue: '[c/Enter/Space] Tęsti'
hint_retry: '[r] Iš naujo'
hint_menu: '[q] Meniu'
hint_stats: '[s] Statistika'
hint_delete: '[x] Trinti'
# Stats sidebar (during drill)
sidebar:
title: ' Statistika '
wpm: 'WPM: '
target: 'Tikslas: '
target_wpm: '%{wpm} WPM'
accuracy: 'Tikslumas: '
progress: 'Pažanga: '
correct: 'Teisingai: '
errors: 'Klaidos: '
time: 'Laikas: '
last_drill: ' Paskutinės pratybos '
vs_avg: ' vs vidurk.: '
# Statistics dashboard
stats:
title: ' Statistika '
empty: 'Nėra baigtų pratybų. Pradėkite spausdinti!'
tab_dashboard: '[1] Suvestinė'
tab_history: '[2] Istorija'
tab_activity: '[3] Aktyvumas'
tab_accuracy: '[4] Tikslumas'
tab_timing: '[5] Laikas'
tab_ngrams: '[6] N-gramos'
hint_back: '[ESC] Atgal'
hint_next_tab: '[Tab] Kita kortelė'
hint_switch_tab: '[1-6] Kortelė'
hint_navigate: '[j/k] Navigacija'
hint_page: '[PgUp/PgDn] Puslapis'
hint_delete: '[x] Trinti'
summary_title: ' Santrauka '
drills: ' Pratybos: '
avg_wpm: ' Vid. WPM: '
best_wpm: ' Geriausias WPM: '
accuracy_label: ' Tikslumas: '
total_time: ' Bendras laikas: '
wpm_chart_title: ' WPM per pratybas (Paskutinės 20, Tikslas: %{target}) '
accuracy_chart_title: ' Tikslumas %% (Paskutinės 50 pratybų) '
chart_drill: 'Pratybos #'
chart_accuracy_pct: 'Tikslumas %%'
sessions_title: ' Naujausios sesijos '
session_header: ' # WPM Raw Tiksl%% Laikas Data/Laikas Režimas Vertin. Dalin.'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Trinti sesiją #%{idx}? (y/n)'
confirm_title: ' Patvirtinimas '
yes: 'taip'
no: 'ne'
keyboard_accuracy_title: ' Klaviatūros tikslumas %% '
keyboard_timing_title: ' Klaviatūros laikas (ms) '
slowest_keys_title: ' Lėčiausi klavišai (ms) '
fastest_keys_title: ' Greičiausi klavišai (ms) '
worst_accuracy_title: ' Blogiausias tikslumas (%%) '
best_accuracy_title: ' Geriausias tikslumas (%%) '
not_enough_data: ' Nepakanka duomenų'
streaks_title: ' Serijos '
current_streak: ' Dabartinė: '
best_streak: ' Geriausia: '
active_days: ' Aktyvios dienos: '
top_days_none: ' Geriausios dienos: nėra'
top_days: ' Geriausios dienos: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Tiksl: %{pct}%%'
keys_label: ' Klavišai: %{unlocked}/%{total} (%{mastered} įvaldyta)'
ngram_empty: 'Atlikite keletą adaptyvių pratybų n-gramų duomenims pamatyti'
ngram_header_speed_narrow: ' Bgrm Greit Tikėt Anom%'
ngram_header_error_narrow: ' Bgrm Kld Imč Norma Tkt Anom%'
ngram_header_speed: ' Bigrama Greit Tikėtina Imčiai Anom%'
ngram_header_error: ' Bigrama Klaidos Imčiai Norma Tikėtina Anom%'
focus_title: ' Aktyvus fokusas '
focus_char_label: ' Fokusas: '
focus_bigram_value: 'Bigrama %{label}'
focus_plus: ' + '
anomaly_error: 'klaida'
anomaly_speed: 'greitis'
focus_detail_both: ' Simb. ''%{ch}'': silpniausias klavišas | Bigrama %{label}: %{type} anomalija %{pct}%%'
focus_detail_char_only: ' Simb. ''%{ch}'': silpniausias klavišas, nėra patvirtintų bigramų anomalijų'
focus_detail_bigram_only: ' (%{type} anomalija: %{pct}%%)'
focus_empty: ' Atlikite keletą adaptyvių pratybų fokuso duomenims pamatyti'
error_anomalies_title: ' Klaidų anomalijos (%{count}) '
no_error_anomalies: ' Klaidų anomalijų nerasta'
speed_anomalies_title: ' Greičio anomalijos (%{count}) '
no_speed_anomalies: ' Greičio anomalijų nerasta'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Simb. ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Dienos aktyvumas (sesijų per dieną) '
jan: 'Sau'
feb: 'Vas'
mar: 'Kov'
apr: 'Bal'
may: 'Geg'
jun: 'Bir'
jul: 'Lie'
aug: 'Rgp'
sep: 'Rgs'
oct: 'Spa'
nov: 'Lap'
dec: 'Grd'
# Chart
chart:
wpm_over_time: ' WPM per laiką '
drill_number: 'Pratybos #'
# Settings
settings:
title: ' Nustatymai '
subtitle: 'Rodyklėmis naršykite, Enter/Dešinėn keisti, ESC išsaugoti'
target_wpm: 'Tikslinis WPM'
theme: 'Tema'
word_count: 'Žodžių skaičius'
ui_language: 'Sąsajos kalba'
dictionary_language: 'Žodyno kalba'
keyboard_layout: 'Klaviatūros išdėstymas'
code_language: 'Programavimo kalba'
code_downloads: 'Kodo atsisiuntimai'
on: 'Įjungta'
off: 'Išjungta'
code_download_dir: 'Kodo atsisiuntimų aplankas'
snippets_per_repo: 'Fragmentų per repozitoriją'
unlimited: 'Neribota'
download_code_now: 'Atsisiųsti kodą dabar'
run_downloader: 'Paleisti atsisiuntimą'
passage_downloads: 'Tekstų atsisiuntimai'
passage_download_dir: 'Tekstų atsisiuntimų aplankas'
paragraphs_per_book: 'Pastraipų per knygą'
whole_book: 'Visa knyga'
download_passages_now: 'Atsisiųsti tekstus dabar'
export_path: 'Eksporto kelias'
export_data: 'Eksportuoti duomenis'
export_now: 'Eksportuoti dabar'
import_path: 'Importo kelias'
import_data: 'Importuoti duomenis'
import_now: 'Importuoti dabar'
hint_save_back: '[ESC] Išsaugoti ir atgal'
hint_change_value: '[Enter/rodyklės] Keisti reikšmę'
hint_edit_path: '[Enter ant kelio] Redaguoti'
hint_move: '[←→] Judėti'
hint_tab_complete: '[Tab] Užbaigti (gale)'
hint_confirm: '[Enter] Patvirtinti'
hint_cancel: '[Esc] Atšaukti'
success_title: ' Sėkmė '
error_title: ' Klaida '
press_any_key: 'Paspauskite bet kurį klavišą'
file_exists_title: ' Failas egzistuoja '
file_exists: 'Failas jau egzistuoja šiuo keliu.'
overwrite_rename: '[d] Perrašyti [r] Pervadinti [Esc] Atšaukti'
erase_warning: 'Tai ištrins jūsų dabartinius duomenis.'
export_first: 'Pirmiausia eksportuokite, jei norite išsaugoti.'
proceed_yn: 'Tęsti? (y/n)'
confirm_import_title: ' Importo patvirtinimas '
# Selection screens
select:
dictionary_language_title: ' Pasirinkite žodyno kalbą '
keyboard_layout_title: ' Pasirinkite klaviatūros išdėstymą '
code_language_title: ' Pasirinkite programavimo kalbą '
passage_source_title: ' Pasirinkite teksto šaltinį '
ui_language_title: ' Pasirinkite sąsajos kalbą '
more_above: '... dar %{count} aukščiau ...'
more_below: '... dar %{count} žemiau ...'
current: ' (dabartinis)'
disabled: ' (išjungta)'
enabled_default: ' (įjungta, numatytasis: %{layout})'
enabled: ' (įjungta)'
disabled_blocked: ' (išjungta: blokuota)'
built_in: ' (integruota)'
cached: ' (podėlyje)'
disabled_download: ' (išjungta: reikia atsisiųsti)'
download_required: ' (reikia atsisiųsti)'
hint_navigate: '[Aukštyn/Žemyn/PgUp/PgDn] Navigacija'
hint_confirm: '[Enter] Patvirtinti'
hint_back: '[ESC] Atgal'
language_resets_layout: 'Kalbos pasirinkimas atstato klaviatūros išdėstymą į tos kalbos numatytąjį.'
layout_no_language_change: 'Išdėstymo pakeitimas nekeičia žodyno kalbos.'
disabled_network_notice: 'Kai kurios kalbos išjungtos: įjunkite tinklo atsisiuntimus įvade/nustatymuose.'
disabled_sources_notice: 'Kai kurie šaltiniai išjungti: įjunkite tinklo atsisiuntimus įvade/nustatymuose.'
passage_all: 'Visos (Integruotos + visos knygos)'
passage_builtin: 'Tik integruoti tekstai'
passage_book_prefix: 'Knyga: %{title}'
# Progress
progress:
overall_key_progress: 'Bendra klavišų pažanga'
unlocked_mastered: '%{unlocked}/%{total} atrakinta (%{mastered} įvaldyta)'
# Skill tree
skill_tree:
title: ' Įgūdžių medis '
locked: 'Užrakinta'
unlocked: 'atrakinta'
mastered: 'įvaldyta'
in_progress: 'vykdoma'
complete: 'baigta'
locked_status: 'užrakinta'
locked_notice: 'Užbaikite %{count} pirminių raidžių šakoms atrakinti'
branches_separator: 'Šakos (prieinamos po %{count} pirminių raidžių)'
unlocked_letters: 'Atrakinta %{unlocked}/%{total} raidžių'
level: 'Lygis %{current}/%{total}'
level_zero: 'Lygis 0/%{total}'
in_focus: ' fokuse'
hint_navigate: '[↑↓/jk] Navigacija'
hint_scroll: '[PgUp/PgDn arba Ctrl+U/Ctrl+D] Slinkti'
hint_back: '[q] Atgal'
hint_unlock: '[Enter] Atrakinti'
hint_start_drill: '[Enter] Pradėti pratybas'
unlock_msg_1: 'Atrakinus, numatytosios adaptyvios pratybos įtrauks šios šakos atrakintus klavišus.'
unlock_msg_2: 'Jei norite sutelkti dėmesį tik į šią šaką, pradėkite pratybas tiesiogiai iš įgūdžių medžio.'
confirm_unlock: 'Atrakinti %{branch}?'
confirm_yn: '[y] Atrakinti [n/ESC] Atšaukti'
lvl_prefix: 'Lyg'
branch_primary_letters: 'Pirminės raidės'
branch_capital_letters: 'Didžiosios raidės'
branch_numbers: 'Skaičiai 0-9'
branch_prose_punctuation: 'Skyrybos ženklai'
branch_whitespace: 'Tarpai'
branch_code_symbols: 'Kodo simboliai'
level_frequency_order: 'Dažnumo tvarka'
level_common_sentence_capitals: 'Dažnos sakinio didžiosios'
level_name_capitals: 'Vardų didžiosios'
level_remaining_capitals: 'Likusios didžiosios'
level_common_digits: 'Dažni skaitmenys'
level_all_digits: 'Visi skaitmenys'
level_essential: 'Būtina'
level_common: 'Dažna'
level_expressive: 'Išraiškinga'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Įtrauka'
level_arithmetic_assignment: 'Aritmetika ir priskyrimas'
level_grouping: 'Grupavimas'
level_logic_reference: 'Logika ir nuorodos'
level_special: 'Specialūs'
# Milestones
milestones:
unlock_title: ' Klavišas atrakintas! '
mastery_title: ' Klavišas įvaldytas! '
branches_title: ' Naujos įgūdžių šakos prieinamos! '
branch_complete_title: ' Šaka baigta! '
all_unlocked_title: ' Visi klavišai atrakinti! '
all_mastered_title: ' Visiškas klaviatūros įvaldymas! '
unlocked: 'atrakinta'
mastered: 'įvaldyta'
use_finger: 'Naudokite %{finger}'
hold_right_shift: 'Laikykite dešinį Shift (dešinysis mažylis)'
hold_left_shift: 'Laikykite kairį Shift (kairysis mažylis)'
congratulations_all_letters: 'Sveikiname! Įvaldėte visas %{count} pirminių raidžių'
new_branches_available: 'Naujos įgūdžių šakos dabar prieinamos:'
visit_skill_tree: 'Aplankykite įgūdžių medį naujai šakai atrakinti'
and_start_training: 'ir pradėkite treniruotis!'
open_skill_tree: 'Paspauskite [t] įgūdžių medžiui atidaryti'
branch_complete_msg: 'Baigėte %{branch} šaką!'
all_levels_mastered: 'Visi %{count} lygiai įvaldyti.'
all_keys_confident: 'Kiekvienas šios šakos klavišas pasiekė pilną patikimumą.'
all_unlocked_msg: 'Atrakinote kiekvieną klaviatūros klavišą!'
all_unlocked_desc: 'Kiekvienas simbolis, ženklas ir modifikatorius dabar prieinamas pratybose.'
keep_practicing_mastery: 'Tęskite praktiką meistriškumui ugdyti — kai kiekvienas klavišas pasieks pilną'
confidence_complete: 'patikimumą, būsite pasiekę visišką klaviatūros įvaldymą!'
all_mastered_msg: 'Sveikiname — pasiekėte visišką klaviatūros įvaldymą!'
all_mastered_desc: 'Kiekvienas klaviatūros klavišas yra maksimalaus patikimumo.'
mastery_takes_practice: 'Meistriškumas nėra tikslas — jis reikalauja nuolatinės praktikos.'
keep_drilling: 'Tęskite pratybas, kad išlaikytumėte formą.'
hint_skill_tree_continue: '[t] Atidaryti įgūdžių medį [Bet kuris klavišas] Tęsti'
hint_any_key: 'Paspauskite bet kurį klavišą tęsti'
input_blocked: 'Įvestis laikinai blokuota (%{ms}ms liko)'
unlock_msg_1: 'Puiku! Toliau tobulinkite spausdinimo įgūdžius.'
unlock_msg_2: 'Dar vienas klavišas jūsų arsenale!'
unlock_msg_3: 'Jūsų klaviatūra auga! Taip ir toliau.'
unlock_msg_4: 'Vienu žingsniu arčiau visiško klaviatūros įvaldymo!'
mastery_msg_1: 'Šis klavišas dabar pilno patikimumo!'
mastery_msg_2: 'Šį klavišą mokate puikiai!'
mastery_msg_3: 'Raumenų atmintis užfiksuota!'
mastery_msg_4: 'Dar vienas klavišas užkariauta!'
# Keyboard explorer
keyboard:
title: ' Klaviatūra '
subtitle: 'Paspauskite bet kurį klavišą arba spustelėkite'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigacija'
hint_back: '[q/ESC] Atgal'
key_label: 'Klavišas: '
finger_label: 'Pirštas: '
hand_left: 'Kairė'
hand_right: 'Dešinė'
finger_index: 'Smilius'
finger_middle: 'Didysis'
finger_ring: 'Bevardis'
finger_pinky: 'Mažylis'
finger_thumb: 'Nykštys'
overall_accuracy: ' Bendras tikslumas: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Vertintas tikslumas: %{correct}/%{total} (%{pct}%%)'
confidence: 'Patikimumas: '
no_data: 'Dar nėra duomenų'
no_data_short: 'Nėra duom.'
key_details: ' Klavišo detalės '
key_details_char: ' Klavišo detalės: ''%{ch}'' '
key_details_name: ' Klavišo detalės: %{name} '
press_key_hint: 'Paspauskite klavišą detalėms pamatyti'
shift_label: 'Shift: '
shift_no: 'Ne'
overall_avg_time: 'Bend. vid. laikas: '
overall_best_time: 'Bend. geriausias: '
overall_samples: 'Bend. imčių: '
overall_accuracy_label: 'Bend. tikslumas: '
branch_label: 'Šaka: '
level_label: 'Lygis: '
built_in_key: 'Integruotas klavišas'
unlocked_label: 'Atrakinta: '
yes: 'Taip'
no: 'Ne'
in_focus_label: 'Fokuse?: '
mastery_label: 'Įvaldymas: '
mastery_locked: 'Užrakinta'
ranked_avg_time: 'Vert. vid. laikas: '
ranked_best_time: 'Vert. geriausias: '
ranked_samples: 'Vert. imčių: '
ranked_accuracy_label: 'Vert. tikslumas: '
# Intro dialogs
intro:
passage_title: ' Tekstų atsisiuntimo nustatymai '
code_title: ' Kodo atsisiuntimo nustatymai '
enable_downloads: 'Įjungti tinklo atsisiuntimus'
download_dir: 'Atsisiuntimų aplankas'
paragraphs_per_book: 'Pastraipų per knygą (0 = visa)'
whole_book: 'visa knyga'
snippets_per_repo: 'Fragmentų per repozitoriją (0 = neribota)'
unlimited: 'neribota'
start_passage_drill: 'Pradėti teksto pratybas'
start_code_drill: 'Pradėti kodo pratybas'
confirm: 'Patvirtinti'
hint_navigate: '[Aukštyn/Žemyn] Navigacija'
hint_adjust: '[Kairėn/Dešinėn] Reguliuoti'
hint_edit: '[Rašymas/Backspace] Redaguoti'
hint_confirm: '[Enter] Patvirtinti'
hint_cancel: '[ESC] Atšaukti'
preparing_download: 'Ruošiamas atsisiuntimas...'
download_passage_title: ' Atsisiunčiamas teksto šaltinis '
download_code_title: ' Atsisiunčiamas kodo šaltinis '
book_label: ' Knyga: %{name}'
repo_label: ' Repozitorija: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} baitų'
downloaded_bytes: 'Atsisiųsta: %{bytes} baitų'
downloading_book_progress: 'Atsisiunčiama knyga: [%{bar}] %{downloaded}/%{total} baitų'
downloading_book_bytes: 'Atsisiunčiama knyga: %{bytes} baitų'
downloading_code_progress: 'Atsisiunčiama: [%{bar}] %{downloaded}/%{total} baitų'
downloading_code_bytes: 'Atsisiunčiama: %{bytes} baitų'
current_book: 'Dabartinė: %{name} (knyga %{done}/%{total})'
current_repo: 'Dabartinė: %{name} (repozitorija %{done}/%{total})'
passage_instructions_1: 'keydr gali atsisiųsti tekstus iš Project Gutenberg spausdinimo praktikai.'
passage_instructions_2: 'Knygos atsisiunčiamos vieną kartą ir saugomos vietoje.'
passage_instructions_3: 'Sukonfigūruokite atsisiuntimo nustatymus, tada pradėkite teksto pratybas.'
code_instructions_1: 'keydr gali atsisiųsti atvirąjį kodą iš GitHub spausdinimo praktikai.'
code_instructions_2: 'Kodas atsisiunčiamas vieną kartą ir saugomas vietoje.'
code_instructions_3: 'Sukonfigūruokite atsisiuntimo nustatymus, tada pradėkite kodo pratybas.'
# Status messages (from app.rs)
status:
recovery_files: 'Rasti atkūrimo failai iš nutraukto importo. Duomenys gali būti nesuderinti — apsvarstykite pakartotinį importą.'
dir_not_exist: 'Aplankas neegzistuoja: %{path}'
no_data_store: 'Nėra prieinamos duomenų saugyklos'
serialization_error: 'Serializacijos klaida: %{error}'
exported_to: 'Eksportuota į %{path}'
export_failed: 'Eksportas nepavyko: %{error}'
could_not_read: 'Nepavyko perskaityti failo: %{error}'
invalid_export: 'Neteisingas eksporto failas: %{error}'
unsupported_version: 'Nepalaikoma eksporto versija: %{got} (tikėtasi %{expected})'
import_failed: 'Importas nepavyko: %{error}'
imported_theme_fallback: 'Importas sėkmingas (tema ''%{theme}'' nerasta, naudojama numatytoji)'
imported_success: 'Importas sėkmingas'
adaptive_unavailable: 'Adaptyvus vertintas režimas neprieinamas: %{error}'
switched_to: 'Perjungta į %{name}'
layout_changed: 'Išdėstymas pakeistas į %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Nežinoma kalba: %{key}'
unknown_layout: 'Nežinomas klaviatūros išdėstymas: %{key}'
unsupported_pair: 'Nepalaikoma kalbos/išdėstymo pora: %{language} + %{layout}'
language_blocked: 'Kalba blokuota pagal palaikymo lygį: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Atgal'

454
locales/lv.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Termināļa rakstīšanas treneris'
adaptive_drill: 'Adaptīvs vingrinājums'
adaptive_drill_desc: 'Fonētiski vārdi ar adaptīvu burtu atbloķēšanu'
code_drill: 'Koda vingrinājums'
code_drill_desc: 'Praktizējiet koda sintakses rakstīšanu'
passage_drill: 'Teksta vingrinājums'
passage_drill_desc: 'Rakstiet fragmentus no grāmatām'
skill_tree: 'Prasmju koks'
skill_tree_desc: 'Skatiet progresa zarus un sāciet vingrinājumus'
keyboard: 'Tastatūra'
keyboard_desc: 'Izpētiet tastatūras izkārtojumu un taustiņu statistiku'
statistics: 'Statistika'
statistics_desc: 'Skatiet rakstīšanas statistiku'
settings: 'Iestatījumi'
settings_desc: 'Konfigurēt keydr'
day_streak: ' | %{days} dienu sērija'
key_progress: ' Taustiņu progress %{unlocked}/%{total} (%{mastered} apgūti) | Mērķis %{target} WPM%{streak}'
hint_start: '[1-3] Sākt'
hint_skill_tree: '[t] Prasmju koks'
hint_keyboard: '[b] Tastatūra'
hint_stats: '[s] Statistika'
hint_settings: '[c] Iestatījumi'
hint_quit: '[q] Iziet'
# Drill screen
drill:
title: ' Vingrinājums '
mode_adaptive: 'Adaptīvs'
mode_code: 'Kods (bez vērtējuma)'
mode_passage: 'Teksts (bez vērtējuma)'
focus_char: 'Fokuss: ''%{ch}'''
focus_bigram: 'Fokuss: "%{bigram}"'
focus_both: 'Fokuss: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Prec'
header_err: 'Kļūd'
code_source: ' Koda avots '
passage_source: ' Teksta avots '
footer: '[ESC] Beigt vingrinājumu [Backspace] Dzēst'
keys_reenabled: 'Taustiņi atkal aktīvi pēc %{ms}ms'
hint_end: '[ESC] Beigt vingrinājumu'
hint_backspace: '[Backspace] Dzēst'
# Dashboard / drill result
dashboard:
title: ' Vingrinājums pabeigts '
results: 'Rezultāti'
unranked_note_prefix: ' (Bez vērtējuma'
unranked_note_suffix: ' netiek ieskaitīts prasmju kokā)'
speed: ' Ātrums: '
accuracy_label: ' Precizitāte:'
time_label: ' Laiks: '
errors_label: ' Kļūdas: '
correct_detail: ' (%{correct}/%{total} pareizi)'
input_blocked: ' Ievade īslaicīgi bloķēta '
input_blocked_ms: '(%{ms}ms atlicis)'
hint_continue: '[c/Enter/Space] Turpināt'
hint_retry: '[r] Atkārtot'
hint_menu: '[q] Izvēlne'
hint_stats: '[s] Statistika'
hint_delete: '[x] Dzēst'
# Stats sidebar (during drill)
sidebar:
title: ' Statistika '
wpm: 'WPM: '
target: 'Mērķis: '
target_wpm: '%{wpm} WPM'
accuracy: 'Precizitāte: '
progress: 'Progress: '
correct: 'Pareizi: '
errors: 'Kļūdas: '
time: 'Laiks: '
last_drill: ' Pēdējais vingrinājums '
vs_avg: ' vs vidēji: '
# Statistics dashboard
stats:
title: ' Statistika '
empty: 'Nav pabeigtu vingrinājumu. Sāciet rakstīt!'
tab_dashboard: '[1] Pārskats'
tab_history: '[2] Vēsture'
tab_activity: '[3] Aktivitāte'
tab_accuracy: '[4] Precizitāte'
tab_timing: '[5] Laiki'
tab_ngrams: '[6] N-gramas'
hint_back: '[ESC] Atpakaļ'
hint_next_tab: '[Tab] Nākamā cilne'
hint_switch_tab: '[1-6] Cilne'
hint_navigate: '[j/k] Navigācija'
hint_page: '[PgUp/PgDn] Lapa'
hint_delete: '[x] Dzēst'
summary_title: ' Kopsavilkums '
drills: ' Vingrinājumi: '
avg_wpm: ' Vid. WPM: '
best_wpm: ' Labākais WPM: '
accuracy_label: ' Precizitāte: '
total_time: ' Kopējais laiks: '
wpm_chart_title: ' WPM pa vingrinājumiem (Pēdējie 20, Mērķis: %{target}) '
accuracy_chart_title: ' Precizitāte %% (Pēdējie 50 vingrinājumi) '
chart_drill: 'Vingrinājums #'
chart_accuracy_pct: 'Precizitāte %%'
sessions_title: ' Nesenās sesijas '
session_header: ' # WPM Raw Prec%% Laiks Datums/Laiks Režīms Vērtēts Daļējs'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Dzēst sesiju #%{idx}? (y/n)'
confirm_title: ' Apstiprināt '
yes: 'jā'
no: 'nē'
keyboard_accuracy_title: ' Tastatūras precizitāte %% '
keyboard_timing_title: ' Tastatūras laiki (ms) '
slowest_keys_title: ' Lēnākie taustiņi (ms) '
fastest_keys_title: ' Ātrākie taustiņi (ms) '
worst_accuracy_title: ' Sliktākā precizitāte (%%) '
best_accuracy_title: ' Labākā precizitāte (%%) '
not_enough_data: ' Nepietiek datu'
streaks_title: ' Sērijas '
current_streak: ' Pašreizējā: '
best_streak: ' Labākā: '
active_days: ' Aktīvās dienas: '
top_days_none: ' Labākās dienas: nav'
top_days: ' Labākās dienas: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Prec: %{pct}%%'
keys_label: ' Taustiņi: %{unlocked}/%{total} (%{mastered} apgūti)'
ngram_empty: 'Pabeidziet dažus adaptīvus vingrinājumus n-gramu datu attēlošanai'
ngram_header_speed_narrow: ' Bgrm Ātr Sagaid Anom%'
ngram_header_error_narrow: ' Bgrm Kļūd Par Likme Sag Anom%'
ngram_header_speed: ' Bigrama Ātrums Sagaid Paraugi Anom%'
ngram_header_error: ' Bigrama Kļūdas Paraugi Likme Sagaid Anom%'
focus_title: ' Aktīvais fokuss '
focus_char_label: ' Fokuss: '
focus_bigram_value: 'Bigrama %{label}'
focus_plus: ' + '
anomaly_error: 'kļūda'
anomaly_speed: 'ātrums'
focus_detail_both: ' Simb. ''%{ch}'': vājākais taustiņš | Bigrama %{label}: %{type} anomālija %{pct}%%'
focus_detail_char_only: ' Simb. ''%{ch}'': vājākais taustiņš, nav apstiprinātu bigramu anomāliju'
focus_detail_bigram_only: ' (%{type} anomālija: %{pct}%%)'
focus_empty: ' Pabeidziet dažus adaptīvus vingrinājumus fokusa datu attēlošanai'
error_anomalies_title: ' Kļūdu anomālijas (%{count}) '
no_error_anomalies: ' Kļūdu anomālijas nav atrastas'
speed_anomalies_title: ' Ātruma anomālijas (%{count}) '
no_speed_anomalies: ' Ātruma anomālijas nav atrastas'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Simb. ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Dienas aktivitāte (sesijas dienā) '
jan: 'Jan'
feb: 'Feb'
mar: 'Mar'
apr: 'Apr'
may: 'Mai'
jun: 'Jūn'
jul: 'Jūl'
aug: 'Aug'
sep: 'Sep'
oct: 'Okt'
nov: 'Nov'
dec: 'Dec'
# Chart
chart:
wpm_over_time: ' WPM laika gaitā '
drill_number: 'Vingrinājums #'
# Settings
settings:
title: ' Iestatījumi '
subtitle: 'Bultiņām navigēt, Enter/Pa labi mainīt, ESC saglabāt'
target_wpm: 'Mērķa WPM'
theme: 'Tēma'
word_count: 'Vārdu skaits'
ui_language: 'Saskarnes valoda'
dictionary_language: 'Vārdnīcas valoda'
keyboard_layout: 'Tastatūras izkārtojums'
code_language: 'Programmēšanas valoda'
code_downloads: 'Koda lejupielādes'
on: 'Ieslēgts'
off: 'Izslēgts'
code_download_dir: 'Koda lejupielādes mape'
snippets_per_repo: 'Fragmenti uz repozitoriju'
unlimited: 'Neierobežots'
download_code_now: 'Lejupielādēt kodu tagad'
run_downloader: 'Palaist lejupielādi'
passage_downloads: 'Tekstu lejupielādes'
passage_download_dir: 'Tekstu lejupielādes mape'
paragraphs_per_book: 'Rindkopas uz grāmatu'
whole_book: 'Visa grāmata'
download_passages_now: 'Lejupielādēt tekstus tagad'
export_path: 'Eksporta ceļš'
export_data: 'Eksportēt datus'
export_now: 'Eksportēt tagad'
import_path: 'Importa ceļš'
import_data: 'Importēt datus'
import_now: 'Importēt tagad'
hint_save_back: '[ESC] Saglabāt un atpakaļ'
hint_change_value: '[Enter/bultiņas] Mainīt vērtību'
hint_edit_path: '[Enter uz ceļa] Rediģēt'
hint_move: '[←→] Pārvietot'
hint_tab_complete: '[Tab] Pabeigt (beigās)'
hint_confirm: '[Enter] Apstiprināt'
hint_cancel: '[Esc] Atcelt'
success_title: ' Veiksmīgi '
error_title: ' Kļūda '
press_any_key: 'Nospiediet jebkuru taustiņu'
file_exists_title: ' Fails eksistē '
file_exists: 'Fails jau eksistē šajā ceļā.'
overwrite_rename: '[d] Pārrakstīt [r] Pārdēvēt [Esc] Atcelt'
erase_warning: 'Tas izdzēsīs jūsu pašreizējos datus.'
export_first: 'Vispirms eksportējiet, ja vēlaties saglabāt.'
proceed_yn: 'Turpināt? (y/n)'
confirm_import_title: ' Importa apstiprināšana '
# Selection screens
select:
dictionary_language_title: ' Izvēlieties vārdnīcas valodu '
keyboard_layout_title: ' Izvēlieties tastatūras izkārtojumu '
code_language_title: ' Izvēlieties programmēšanas valodu '
passage_source_title: ' Izvēlieties teksta avotu '
ui_language_title: ' Izvēlieties saskarnes valodu '
more_above: '... vēl %{count} augstāk ...'
more_below: '... vēl %{count} zemāk ...'
current: ' (pašreizējais)'
disabled: ' (atspējots)'
enabled_default: ' (iespējots, noklusējums: %{layout})'
enabled: ' (iespējots)'
disabled_blocked: ' (atspējots: bloķēts)'
built_in: ' (iebūvēts)'
cached: ' (kešots)'
disabled_download: ' (atspējots: nepieciešama lejupielāde)'
download_required: ' (nepieciešama lejupielāde)'
hint_navigate: '[Augšup/Lejup/PgUp/PgDn] Navigācija'
hint_confirm: '[Enter] Apstiprināt'
hint_back: '[ESC] Atpakaļ'
language_resets_layout: 'Valodas izvēle atjauno tastatūras izkārtojumu uz šīs valodas noklusējumu.'
layout_no_language_change: 'Izkārtojuma maiņa nemaina vārdnīcas valodu.'
disabled_network_notice: 'Dažas valodas atspējotas: iespējojiet tīkla lejupielādes ievadā/iestatījumos.'
disabled_sources_notice: 'Daži avoti atspējoti: iespējojiet tīkla lejupielādes ievadā/iestatījumos.'
passage_all: 'Visi (Iebūvētie + visas grāmatas)'
passage_builtin: 'Tikai iebūvētie teksti'
passage_book_prefix: 'Grāmata: %{title}'
# Progress
progress:
overall_key_progress: 'Kopējais taustiņu progress'
unlocked_mastered: '%{unlocked}/%{total} atbloķēti (%{mastered} apgūti)'
# Skill tree
skill_tree:
title: ' Prasmju koks '
locked: 'Bloķēts'
unlocked: 'atbloķēts'
mastered: 'apgūts'
in_progress: 'procesā'
complete: 'pabeigts'
locked_status: 'bloķēts'
locked_notice: 'Pabeidziet %{count} primāros burtus zaru atbloķēšanai'
branches_separator: 'Zari (pieejami pēc %{count} primārajiem burtiem)'
unlocked_letters: 'Atbloķēti %{unlocked}/%{total} burti'
level: 'Līmenis %{current}/%{total}'
level_zero: 'Līmenis 0/%{total}'
in_focus: ' fokusā'
hint_navigate: '[↑↓/jk] Navigācija'
hint_scroll: '[PgUp/PgDn vai Ctrl+U/Ctrl+D] Ritināt'
hint_back: '[q] Atpakaļ'
hint_unlock: '[Enter] Atbloķēt'
hint_start_drill: '[Enter] Sākt vingrinājumu'
unlock_msg_1: 'Pēc atbloķēšanas noklusējuma adaptīvais vingrinājums iekļaus šī zara atbloķētos taustiņus.'
unlock_msg_2: 'Ja vēlaties fokusēties tikai uz šo zaru, sāciet vingrinājumu tieši no prasmju koka.'
confirm_unlock: 'Atbloķēt %{branch}?'
confirm_yn: '[y] Atbloķēt [n/ESC] Atcelt'
lvl_prefix: 'Līm'
branch_primary_letters: 'Primārie burti'
branch_capital_letters: 'Lielie burti'
branch_numbers: 'Cipari 0-9'
branch_prose_punctuation: 'Pieturzīmes'
branch_whitespace: 'Atstarpes'
branch_code_symbols: 'Koda simboli'
level_frequency_order: 'Biežuma secībā'
level_common_sentence_capitals: 'Bieži teikumu lielie burti'
level_name_capitals: 'Vārdu lielie burti'
level_remaining_capitals: 'Atlikušie lielie burti'
level_common_digits: 'Biežie cipari'
level_all_digits: 'Visi cipari'
level_essential: 'Būtiski'
level_common: 'Bieži'
level_expressive: 'Izteiksmīgi'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Atkāpe'
level_arithmetic_assignment: 'Aritmētika un piešķiršana'
level_grouping: 'Grupēšana'
level_logic_reference: 'Loģika un atsauces'
level_special: 'Speciāli'
# Milestones
milestones:
unlock_title: ' Taustiņš atbloķēts! '
mastery_title: ' Taustiņš apgūts! '
branches_title: ' Jauni prasmju zari pieejami! '
branch_complete_title: ' Zars pabeigts! '
all_unlocked_title: ' Visi taustiņi atbloķēti! '
all_mastered_title: ' Pilnīga tastatūras apguve! '
unlocked: 'atbloķēts'
mastered: 'apgūts'
use_finger: 'Izmantojiet %{finger}'
hold_right_shift: 'Turiet labo Shift (labais mazais pirksts)'
hold_left_shift: 'Turiet kreiso Shift (kreisais mazais pirksts)'
congratulations_all_letters: 'Apsveicam! Esat apguvis visus %{count} primāros burtus'
new_branches_available: 'Jauni prasmju zari tagad pieejami:'
visit_skill_tree: 'Apmeklējiet prasmju koku jauna zara atbloķēšanai'
and_start_training: 'un sāciet trenēties!'
open_skill_tree: 'Nospiediet [t] prasmju koka atvēršanai'
branch_complete_msg: 'Esat pabeidzis %{branch} zaru!'
all_levels_mastered: 'Visi %{count} līmeņi apgūti.'
all_keys_confident: 'Katrs taustiņš šajā zarā ir pilnā uzticamībā.'
all_unlocked_msg: 'Esat atbloķējis katru tastatūras taustiņu!'
all_unlocked_desc: 'Katra rakstzīme, simbols un modifikators tagad pieejams vingrinājumos.'
keep_practicing_mastery: 'Turpiniet praktizēt meistarības veidošanai — kad katrs taustiņš sasniegs pilnu'
confidence_complete: 'uzticamību, būsiet sasniedzis pilnīgu tastatūras apguvi!'
all_mastered_msg: 'Apsveicam — esat sasniedzis pilnīgu tastatūras apguvi!'
all_mastered_desc: 'Katrs tastatūras taustiņš ir maksimālā uzticamībā.'
mastery_takes_practice: 'Meistarība nav galamērķis — tā prasa pastāvīgu praksi.'
keep_drilling: 'Turpiniet vingrinājumus, lai uzturētu formu.'
hint_skill_tree_continue: '[t] Atvērt prasmju koku [Jebkurš taustiņš] Turpināt'
hint_any_key: 'Nospiediet jebkuru taustiņu lai turpinātu'
input_blocked: 'Ievade īslaicīgi bloķēta (%{ms}ms atlicis)'
unlock_msg_1: 'Lielisks darbs! Turpiniet veidot rakstīšanas prasmes.'
unlock_msg_2: 'Vēl viens taustiņš jūsu arsenālā!'
unlock_msg_3: 'Jūsu tastatūra aug! Tā turpiniet.'
unlock_msg_4: 'Soli tuvāk pilnīgai tastatūras apguvei!'
mastery_msg_1: 'Šis taustiņš tagad ir pilnā uzticamībā!'
mastery_msg_2: 'Šo taustiņu jūs protat lieliski!'
mastery_msg_3: 'Muskuļu atmiņa nostiprināta!'
mastery_msg_4: 'Vēl viens taustiņš iekarots!'
# Keyboard explorer
keyboard:
title: ' Tastatūra '
subtitle: 'Nospiediet jebkuru taustiņu vai noklikšķiniet'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigācija'
hint_back: '[q/ESC] Atpakaļ'
key_label: 'Taustiņš: '
finger_label: 'Pirksts: '
hand_left: 'Kreisā'
hand_right: 'Labā'
finger_index: 'Rādītājpirksts'
finger_middle: 'Vidējais'
finger_ring: 'Zeltnesis'
finger_pinky: 'Mazais pirksts'
finger_thumb: 'Īkšķis'
overall_accuracy: ' Kopējā precizitāte: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Vērtētā precizitāte: %{correct}/%{total} (%{pct}%%)'
confidence: 'Uzticamība: '
no_data: 'Vēl nav datu'
no_data_short: 'Nav datu'
key_details: ' Taustiņa detaļas '
key_details_char: ' Taustiņa detaļas: ''%{ch}'' '
key_details_name: ' Taustiņa detaļas: %{name} '
press_key_hint: 'Nospiediet taustiņu detaļu apskatei'
shift_label: 'Shift: '
shift_no: 'Nē'
overall_avg_time: 'Kop. vid. laiks: '
overall_best_time: 'Kop. labākais laiks: '
overall_samples: 'Kop. paraugi: '
overall_accuracy_label: 'Kop. precizitāte: '
branch_label: 'Zars: '
level_label: 'Līmenis: '
built_in_key: 'Iebūvēts taustiņš'
unlocked_label: 'Atbloķēts: '
yes: 'Jā'
no: 'Nē'
in_focus_label: 'Fokusā?: '
mastery_label: 'Apguve: '
mastery_locked: 'Bloķēts'
ranked_avg_time: 'Vērt. vid. laiks: '
ranked_best_time: 'Vērt. labākais laiks: '
ranked_samples: 'Vērt. paraugi: '
ranked_accuracy_label: 'Vērt. precizitāte: '
# Intro dialogs
intro:
passage_title: ' Tekstu lejupielādes iestatīšana '
code_title: ' Koda lejupielādes iestatīšana '
enable_downloads: 'Iespējot tīkla lejupielādes'
download_dir: 'Lejupielādes mape'
paragraphs_per_book: 'Rindkopas uz grāmatu (0 = visa)'
whole_book: 'visa grāmata'
snippets_per_repo: 'Fragmenti uz repozitoriju (0 = neierobežots)'
unlimited: 'neierobežots'
start_passage_drill: 'Sākt teksta vingrinājumu'
start_code_drill: 'Sākt koda vingrinājumu'
confirm: 'Apstiprināt'
hint_navigate: '[Augšup/Lejup] Navigācija'
hint_adjust: '[Pa kreisi/Pa labi] Pielāgot'
hint_edit: '[Rakstīšana/Backspace] Rediģēt'
hint_confirm: '[Enter] Apstiprināt'
hint_cancel: '[ESC] Atcelt'
preparing_download: 'Gatavo lejupielādi...'
download_passage_title: ' Lejupielādē teksta avotu '
download_code_title: ' Lejupielādē koda avotu '
book_label: ' Grāmata: %{name}'
repo_label: ' Repozitorijs: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} baiti'
downloaded_bytes: 'Lejupielādēts: %{bytes} baiti'
downloading_book_progress: 'Lejupielādē grāmatu: [%{bar}] %{downloaded}/%{total} baiti'
downloading_book_bytes: 'Lejupielādē grāmatu: %{bytes} baiti'
downloading_code_progress: 'Lejupielādē: [%{bar}] %{downloaded}/%{total} baiti'
downloading_code_bytes: 'Lejupielādē: %{bytes} baiti'
current_book: 'Pašreizējā: %{name} (grāmata %{done}/%{total})'
current_repo: 'Pašreizējais: %{name} (repozitorijs %{done}/%{total})'
passage_instructions_1: 'keydr var lejupielādēt tekstus no Project Gutenberg rakstīšanas praksei.'
passage_instructions_2: 'Grāmatas tiek lejupielādētas vienreiz un glabātas lokāli.'
passage_instructions_3: 'Konfigurējiet lejupielādes iestatījumus, tad sāciet teksta vingrinājumu.'
code_instructions_1: 'keydr var lejupielādēt atvērtā koda projektus no GitHub rakstīšanas praksei.'
code_instructions_2: 'Kods tiek lejupielādēts vienreiz un glabāts lokāli.'
code_instructions_3: 'Konfigurējiet lejupielādes iestatījumus, tad sāciet koda vingrinājumu.'
# Status messages (from app.rs)
status:
recovery_files: 'Atrasti atkopšanas faili no pārtraukta importa. Dati var būt nekonsekventi — apsveriet atkārtotu importu.'
dir_not_exist: 'Mape neeksistē: %{path}'
no_data_store: 'Nav pieejama datu krātuve'
serialization_error: 'Serializācijas kļūda: %{error}'
exported_to: 'Eksportēts uz %{path}'
export_failed: 'Eksports neizdevās: %{error}'
could_not_read: 'Nevarēja nolasīt failu: %{error}'
invalid_export: 'Nederīgs eksporta fails: %{error}'
unsupported_version: 'Neatbalstīta eksporta versija: %{got} (gaidīta %{expected})'
import_failed: 'Imports neizdevās: %{error}'
imported_theme_fallback: 'Imports veiksmīgs (tēma ''%{theme}'' nav atrasta, izmanto noklusējumu)'
imported_success: 'Imports veiksmīgs'
adaptive_unavailable: 'Adaptīvais vērtētais režīms nav pieejams: %{error}'
switched_to: 'Pārslēgts uz %{name}'
layout_changed: 'Izkārtojums mainīts uz %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Nezināma valoda: %{key}'
unknown_layout: 'Nezināms tastatūras izkārtojums: %{key}'
unsupported_pair: 'Neatbalstīts valodas/izkārtojuma pāris: %{language} + %{layout}'
language_blocked: 'Valoda bloķēta atbalsta līmeņa dēļ: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Atpakaļ'

454
locales/nb.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminal Skrivetrener'
adaptive_drill: 'Adaptiv oevelse'
adaptive_drill_desc: 'Fonetiske ord med adaptiv bokstavopplaasing'
code_drill: 'Kodeoevelse'
code_drill_desc: 'Oev paa aa skrive kodesyntaks'
passage_drill: 'Tekstoevelse'
passage_drill_desc: 'Skriv av passasjer fra boeker'
skill_tree: 'Ferdighetstre'
skill_tree_desc: 'Se fremgangsgrener og start oevelser'
keyboard: 'Tastatur'
keyboard_desc: 'Utforsk tastaturoppsett og tasterstatistikk'
statistics: 'Statistikk'
statistics_desc: 'Se skrivestatistikken din'
settings: 'Innstillinger'
settings_desc: 'Konfigurer keydr'
day_streak: ' | %{days} dagers rekke'
key_progress: ' Tastefremgang %{unlocked}/%{total} (%{mastered} mestret) | Maal %{target} WPM%{streak}'
hint_start: '[1-3] Start'
hint_skill_tree: '[t] Ferdighetstre'
hint_keyboard: '[b] Tastatur'
hint_stats: '[s] Statistikk'
hint_settings: '[c] Innstillinger'
hint_quit: '[q] Avslutt'
# Drill screen
drill:
title: ' Oevelse '
mode_adaptive: 'Adaptiv'
mode_code: 'Kode (Urangert)'
mode_passage: 'Tekst (Urangert)'
focus_char: 'Fokus: ''%{ch}'''
focus_bigram: 'Fokus: "%{bigram}"'
focus_both: 'Fokus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Noey'
header_err: 'Feil'
code_source: ' Kodekilde '
passage_source: ' Tekstkilde '
footer: '[ESC] Avslutt oevelse [Backspace] Slett'
keys_reenabled: 'Taster reaktivert etter %{ms}ms'
hint_end: '[ESC] Avslutt oevelse'
hint_backspace: '[Backspace] Slett'
# Dashboard / drill result
dashboard:
title: ' Oevelse fullfoert '
results: 'Resultater'
unranked_note_prefix: ' (Urangert'
unranked_note_suffix: ' teller ikke for ferdighetstreet)'
speed: ' Hastighet: '
accuracy_label: ' Noeyaktighet: '
time_label: ' Tid: '
errors_label: ' Feil: '
correct_detail: ' (%{correct}/%{total} korrekte)'
input_blocked: ' Inndata midlertidig blokkert '
input_blocked_ms: '(%{ms}ms gjenstaar)'
hint_continue: '[c/Enter/Space] Fortsett'
hint_retry: '[r] Proev igjen'
hint_menu: '[q] Meny'
hint_stats: '[s] Statistikk'
hint_delete: '[x] Slett'
# Stats sidebar (during drill)
sidebar:
title: ' Statistikk '
wpm: 'WPM: '
target: 'Maal: '
target_wpm: '%{wpm} WPM'
accuracy: 'Noeyaktighet: '
progress: 'Fremgang: '
correct: 'Korrekte: '
errors: 'Feil: '
time: 'Tid: '
last_drill: ' Forrige oevelse '
vs_avg: ' vs snitt: '
# Statistics dashboard
stats:
title: ' Statistikk '
empty: 'Ingen oevelser fullfoert ennaa. Begynn aa skrive!'
tab_dashboard: '[1] Dashboard'
tab_history: '[2] Historikk'
tab_activity: '[3] Aktivitet'
tab_accuracy: '[4] Noeyaktighet'
tab_timing: '[5] Timing'
tab_ngrams: '[6] N-grammer'
hint_back: '[ESC] Tilbake'
hint_next_tab: '[Tab] Neste fane'
hint_switch_tab: '[1-6] Bytt fane'
hint_navigate: '[j/k] Naviger'
hint_page: '[PgUp/PgDn] Side'
hint_delete: '[x] Slett'
summary_title: ' Sammendrag '
drills: ' Oevelser: '
avg_wpm: ' Snitt WPM: '
best_wpm: ' Beste WPM: '
accuracy_label: ' Noeyaktighet: '
total_time: ' Total tid: '
wpm_chart_title: ' WPM per oevelse (Siste 20, Maal: %{target}) '
accuracy_chart_title: ' Noeyaktighet %% (Siste 50 oevelser) '
chart_drill: 'Oevelse #'
chart_accuracy_pct: 'Noeyaktighet %%'
sessions_title: ' Siste oekter '
session_header: ' # WPM Raa Noey%% Tid Dato/Tid Modus Rangert Delvis'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Slett oekt #%{idx}? (y/n)'
confirm_title: ' Bekreft '
yes: 'ja'
no: 'nei'
keyboard_accuracy_title: ' Tastatur noeyaktighet %% '
keyboard_timing_title: ' Tastatur timing (ms) '
slowest_keys_title: ' Tregeste taster (ms) '
fastest_keys_title: ' Raskeste taster (ms) '
worst_accuracy_title: ' Daarligste noeyaktighet (%%) '
best_accuracy_title: ' Beste noeyaktighet (%%) '
not_enough_data: ' Ikke nok data'
streaks_title: ' Rekker '
current_streak: ' Naavaerende: '
best_streak: ' Beste: '
active_days: ' Aktive dager: '
top_days_none: ' Toppdager: ingen'
top_days: ' Toppdager: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Noey: %{pct}%%'
keys_label: ' Taster: %{unlocked}/%{total} (%{mastered} mestret)'
ngram_empty: 'Fullfaor noen adaptive oevelser for aa se n-gram data'
ngram_header_speed_narrow: ' Bgrm Hast Forv Anom%'
ngram_header_error_narrow: ' Bgrm Feil Stp Freq Forv Anom%'
ngram_header_speed: ' Bigram Hast Forvent Stikkpr. Anom%'
ngram_header_error: ' Bigram Feil Stikkpr. Freq Forvent Anom%'
focus_title: ' Aktivt fokus '
focus_char_label: ' Fokus: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'feil'
anomaly_speed: 'hastighet'
focus_detail_both: ' Tegn ''%{ch}'': svakeste tast | Bigram %{label}: %{type}-anomali %{pct}%%'
focus_detail_char_only: ' Tegn ''%{ch}'': svakeste tast, ingen bekreftede bigram-anomalier'
focus_detail_bigram_only: ' (%{type}-anomali: %{pct}%%)'
focus_empty: ' Fullfaor noen adaptive oevelser for aa se fokusdata'
error_anomalies_title: ' Feil-anomalier (%{count}) '
no_error_anomalies: ' Ingen feil-anomalier oppdaget'
speed_anomalies_title: ' Hastighets-anomalier (%{count}) '
no_speed_anomalies: ' Ingen hastighets-anomalier oppdaget'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Tegn ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Daglig aktivitet (Oekter per dag) '
jan: 'Jan'
feb: 'Feb'
mar: 'Mar'
apr: 'Apr'
may: 'Mai'
jun: 'Jun'
jul: 'Jul'
aug: 'Aug'
sep: 'Sep'
oct: 'Okt'
nov: 'Nov'
dec: 'Des'
# Chart
chart:
wpm_over_time: ' WPM over tid '
drill_number: 'Oevelse #'
# Settings
settings:
title: ' Innstillinger '
subtitle: 'Piltaster for aa navigere, Enter/Hoeyre for aa endre, ESC for aa lagre'
target_wpm: 'Maal-WPM'
theme: 'Tema'
word_count: 'Antall ord'
ui_language: 'Spraak (UI)'
dictionary_language: 'Ordlistespraak'
keyboard_layout: 'Tastaturoppsett'
code_language: 'Kodespraak'
code_downloads: 'Kode-nedlastinger'
on: 'Paa'
off: 'Av'
code_download_dir: 'Kode-nedlastingsmappe'
snippets_per_repo: 'Utdrag per repo'
unlimited: 'Ubegrenset'
download_code_now: 'Last ned kode naa'
run_downloader: 'Start nedlasting'
passage_downloads: 'Tekst-nedlastinger'
passage_download_dir: 'Tekst-nedlastingsmappe'
paragraphs_per_book: 'Avsnitt per bok'
whole_book: 'Hele boken'
download_passages_now: 'Last ned tekster naa'
export_path: 'Eksportsti'
export_data: 'Eksporter data'
export_now: 'Eksporter naa'
import_path: 'Importsti'
import_data: 'Importer data'
import_now: 'Importer naa'
hint_save_back: '[ESC] Lagre & tilbake'
hint_change_value: '[Enter/piler] Endre verdi'
hint_edit_path: '[Enter paa sti] Rediger'
hint_move: '[←→] Flytt'
hint_tab_complete: '[Tab] Fullfaor (paa slutten)'
hint_confirm: '[Enter] Bekreft'
hint_cancel: '[Esc] Avbryt'
success_title: ' Suksess '
error_title: ' Feil '
press_any_key: 'Trykk paa en tast'
file_exists_title: ' Filen finnes '
file_exists: 'Det finnes allerede en fil paa denne stien.'
overwrite_rename: '[d] Overskriv [r] Gi nytt navn [Esc] Avbryt'
erase_warning: 'Dette vil slette dine naavaerende data.'
export_first: 'Eksporter foerst hvis du vil beholde dem.'
proceed_yn: 'Fortsett? (y/n)'
confirm_import_title: ' Bekreft import '
# Selection screens
select:
dictionary_language_title: ' Velg ordlistespraak '
keyboard_layout_title: ' Velg tastaturoppsett '
code_language_title: ' Velg kodespraak '
passage_source_title: ' Velg tekstkilde '
ui_language_title: ' Velg spraak (UI) '
more_above: '... %{count} flere over ...'
more_below: '... %{count} flere under ...'
current: ' (naavaerende)'
disabled: ' (deaktivert)'
enabled_default: ' (aktivert, standard: %{layout})'
enabled: ' (aktivert)'
disabled_blocked: ' (deaktivert: blokkert)'
built_in: ' (innebygd)'
cached: ' (lagret)'
disabled_download: ' (deaktivert: nedlasting kraeves)'
download_required: ' (nedlasting kraeves)'
hint_navigate: '[Opp/Ned/PgUp/PgDn] Naviger'
hint_confirm: '[Enter] Bekreft'
hint_back: '[ESC] Tilbake'
language_resets_layout: 'Spraakvalg tilbakestiller tastaturoppsett til spraakets standard.'
layout_no_language_change: 'Oppsettendringer endrer ikke ordlistespraaket.'
disabled_network_notice: 'Noen spraak er deaktivert: aktiver nettverksnedlastinger i intro/innstillinger.'
disabled_sources_notice: 'Noen kilder er deaktivert: aktiver nettverksnedlastinger i intro/innstillinger.'
passage_all: 'Alle (Innebygde + alle boeker)'
passage_builtin: 'Kun innebygde passasjer'
passage_book_prefix: 'Bok: %{title}'
# Progress
progress:
overall_key_progress: 'Total tastefremgang'
unlocked_mastered: '%{unlocked}/%{total} laast opp (%{mastered} mestret)'
# Skill tree
skill_tree:
title: ' Ferdighetstre '
locked: 'Laast'
unlocked: 'laast opp'
mastered: 'mestret'
in_progress: 'paagaar'
complete: 'fullfoert'
locked_status: 'laast'
locked_notice: 'Fullfaor %{count} grunnbokstaver for aa laase opp grener'
branches_separator: 'Grener (tilgjengelige etter %{count} grunnbokstaver)'
unlocked_letters: '%{unlocked}/%{total} bokstaver laast opp'
level: 'Nivaa %{current}/%{total}'
level_zero: 'Nivaa 0/%{total}'
in_focus: ' i fokus'
hint_navigate: '[↑↓/jk] Naviger'
hint_scroll: '[PgUp/PgDn eller Ctrl+U/Ctrl+D] Rull'
hint_back: '[q] Tilbake'
hint_unlock: '[Enter] Laas opp'
hint_start_drill: '[Enter] Start oevelse'
unlock_msg_1: 'Etter opplaasing blandes oplaaste taster fra denne grenen inn i den adaptive oevelsen.'
unlock_msg_2: 'Vil du kun oeve denne grenen, start en oevelse direkte fra denne grenen i Ferdighetstreet.'
confirm_unlock: 'Laas opp %{branch}?'
confirm_yn: '[y] Laas opp [n/ESC] Avbryt'
lvl_prefix: 'Niv'
branch_primary_letters: 'Grunnbokstaver'
branch_capital_letters: 'Store bokstaver'
branch_numbers: 'Tall 0-9'
branch_prose_punctuation: 'Tegnsetting'
branch_whitespace: 'Mellomrom'
branch_code_symbols: 'Kodesymboler'
level_frequency_order: 'Frekvensrekkefoelge'
level_common_sentence_capitals: 'Vanlige setningsstorbokst.'
level_name_capitals: 'Navnestorbokstaver'
level_remaining_capitals: 'Oevrige storbokstaver'
level_common_digits: 'Vanlige siffer'
level_all_digits: 'Alle siffer'
level_essential: 'Grunnleggende'
level_common: 'Vanlige'
level_expressive: 'Uttrykksfull'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Innrykk'
level_arithmetic_assignment: 'Aritmetikk & Tilordning'
level_grouping: 'Gruppering'
level_logic_reference: 'Logikk & Referanse'
level_special: 'Spesial'
# Milestones
milestones:
unlock_title: ' Tast laast opp! '
mastery_title: ' Tast mestret! '
branches_title: ' Nye ferdighetsgrener tilgjengelige! '
branch_complete_title: ' Gren fullfoert! '
all_unlocked_title: ' Alle taster laast opp! '
all_mastered_title: ' Full tastaturmestring! '
unlocked: 'laast opp'
mastered: 'mestret'
use_finger: 'Bruk %{finger}'
hold_right_shift: 'Hold hoyre Shift (hoeyre lillefinger)'
hold_left_shift: 'Hold venstre Shift (venstre lillefinger)'
congratulations_all_letters: 'Gratulerer! Du har mestret alle %{count} grunnbokstaver'
new_branches_available: 'Nye ferdighetsgrener er naa tilgjengelige:'
visit_skill_tree: 'Besoek Ferdighetstreet for aa laase opp en ny gren'
and_start_training: 'og begynn aa trene!'
open_skill_tree: 'Trykk [t] for aa aapne Ferdighetstreet naa'
branch_complete_msg: 'Du har fullfoert grenen %{branch}!'
all_levels_mastered: 'Alle %{count} nivaaer mestret.'
all_keys_confident: 'Hver tast i denne grenen har full tillit.'
all_unlocked_msg: 'Du har laast opp hver tast paa tastaturet!'
all_unlocked_desc: 'Hvert tegn, symbol og modifikator er naa tilgjengelig i oevelsene dine.'
keep_practicing_mastery: 'Fortsett aa oeve for aa bygge mestring — naar hver tast naar full'
confidence_complete: 'tillit, har du oppnaad fullstendig tastaturmestring!'
all_mastered_msg: 'Gratulerer — du har oppnaad fullstendig tastaturmestring!'
all_mastered_desc: 'Hver tast paa tastaturet har maksimal tillit.'
mastery_takes_practice: 'Mestring er ikke et maal — det krever vedvarende oeving.'
keep_drilling: 'Fortsett aa oeve for aa holde deg skarp.'
hint_skill_tree_continue: '[t] Ferdighetstre [Annen tast] Fortsett'
hint_any_key: 'Trykk paa en tast for aa fortsette'
input_blocked: 'Inndata midlertidig blokkert (%{ms}ms gjenstaar)'
unlock_msg_1: 'Bra jobbet! Fortsett aa bygge skriveferdighetene dine.'
unlock_msg_2: 'Enda en tast i arsenalet ditt!'
unlock_msg_3: 'Tastaturet ditt vokser! Fortsett slik.'
unlock_msg_4: 'Et skritt naermere full tastaturmestring!'
mastery_msg_1: 'Denne tasten har naa full tillit!'
mastery_msg_2: 'Du mestrer denne tasten perfekt!'
mastery_msg_3: 'Muskelminne forankret!'
mastery_msg_4: 'Enda en tast erobret!'
# Keyboard explorer
keyboard:
title: ' Tastatur '
subtitle: 'Trykk paa en tast eller klikk paa en tast'
hint_navigate: '[←→↑↓/hjkl/Tab] Naviger'
hint_back: '[q/ESC] Tilbake'
key_label: 'Tast: '
finger_label: 'Finger: '
hand_left: 'Venstre'
hand_right: 'Hoeyre'
finger_index: 'Pekefinger'
finger_middle: 'Langfinger'
finger_ring: 'Ringfinger'
finger_pinky: 'Lillefinger'
finger_thumb: 'Tommel'
overall_accuracy: ' Total noeyaktighet: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Rangert noeyaktighet: %{correct}/%{total} (%{pct}%%)'
confidence: 'Tillit: '
no_data: 'Ingen data ennaa'
no_data_short: 'Ingen data'
key_details: ' Tastdetaljer '
key_details_char: ' Tastdetaljer: ''%{ch}'' '
key_details_name: ' Tastdetaljer: %{name} '
press_key_hint: 'Trykk paa en tast for detaljer'
shift_label: 'Shift: '
shift_no: 'Nei'
overall_avg_time: 'Total snittid: '
overall_best_time: 'Total beste tid: '
overall_samples: 'Total stikkproever: '
overall_accuracy_label: 'Total noeyaktighet: '
branch_label: 'Gren: '
level_label: 'Nivaa: '
built_in_key: 'Innebygd tast'
unlocked_label: 'Laast opp: '
yes: 'Ja'
no: 'Nei'
in_focus_label: 'I fokus?: '
mastery_label: 'Mestring: '
mastery_locked: 'Laast'
ranked_avg_time: 'Rangert snittid: '
ranked_best_time: 'Rangert beste tid: '
ranked_samples: 'Rangerte stikkproever: '
ranked_accuracy_label: 'Rangert noeyaktighet: '
# Intro dialogs
intro:
passage_title: ' Tekst-nedlasting oppsett '
code_title: ' Kode-nedlasting oppsett '
enable_downloads: 'Aktiver nettverksnedlastinger'
download_dir: 'Nedlastingsmappe'
paragraphs_per_book: 'Avsnitt per bok (0 = hele)'
whole_book: 'hele boken'
snippets_per_repo: 'Utdrag per repo (0 = ubegrenset)'
unlimited: 'ubegrenset'
start_passage_drill: 'Start tekstoevelse'
start_code_drill: 'Start kodeoevelse'
confirm: 'Bekreft'
hint_navigate: '[Opp/Ned] Naviger'
hint_adjust: '[Venstre/Hoeyre] Juster'
hint_edit: '[Skriv/Backspace] Rediger'
hint_confirm: '[Enter] Bekreft'
hint_cancel: '[ESC] Avbryt'
preparing_download: 'Forbereder nedlasting...'
download_passage_title: ' Laster ned tekstkilde '
download_code_title: ' Laster ned kodekilde '
book_label: ' Bok: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bytes'
downloaded_bytes: 'Lastet ned: %{bytes} bytes'
downloading_book_progress: 'Laster ned gjeldende bok: [%{bar}] %{downloaded}/%{total} bytes'
downloading_book_bytes: 'Laster ned gjeldende bok: %{bytes} bytes'
downloading_code_progress: 'Laster ned: [%{bar}] %{downloaded}/%{total} bytes'
downloading_code_bytes: 'Laster ned: %{bytes} bytes'
current_book: 'Gjeldende: %{name} (bok %{done}/%{total})'
current_repo: 'Gjeldende: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr kan laste ned passasjer fra Project Gutenberg for skriveoeving.'
passage_instructions_2: 'Boeker lastes ned en gang og lagres lokalt.'
passage_instructions_3: 'Konfigurer nedlastingsinnstillinger nedenfor og start en tekstoevelse.'
code_instructions_1: 'keydr kan laste ned aapen kildekode fra GitHub for skriveoeving.'
code_instructions_2: 'Kode lastes ned en gang og lagres lokalt.'
code_instructions_3: 'Konfigurer nedlastingsinnstillinger nedenfor og start en kodeoevelse.'
# Status messages (from app.rs)
status:
recovery_files: 'Gjenopprettingsfiler funnet fra avbrutt import. Data kan vaere inkonsistent — vurder aa importere paa nytt.'
dir_not_exist: 'Mappe finnes ikke: %{path}'
no_data_store: 'Ingen datalager tilgjengelig'
serialization_error: 'Serialiseringsfeil: %{error}'
exported_to: 'Eksportert til %{path}'
export_failed: 'Eksport mislyktes: %{error}'
could_not_read: 'Kunne ikke lese filen: %{error}'
invalid_export: 'Ugyldig eksportfil: %{error}'
unsupported_version: 'Eksportversjon stoettes ikke: %{got} (forventet %{expected})'
import_failed: 'Import mislyktes: %{error}'
imported_theme_fallback: 'Importert (tema ''%{theme}'' ikke funnet, standard brukes)'
imported_success: 'Importert'
adaptive_unavailable: 'Adaptiv rangert modus ikke tilgjengelig: %{error}'
switched_to: 'Byttet til %{name}'
layout_changed: 'Oppsett endret til %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Ukjent spraak: %{key}'
unknown_layout: 'Ukjent tastaturoppsett: %{key}'
unsupported_pair: 'Ikke-stoettet spraak-/oppsettpar: %{language} + %{layout}'
language_blocked: 'Spraak blokkert av stoettenivaa: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Tilbake'

454
locales/nl.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminal Typoefening'
adaptive_drill: 'Adaptieve Oefening'
adaptive_drill_desc: 'Fonetische woorden met adaptieve letterontgrendeling'
code_drill: 'Code-oefening'
code_drill_desc: 'Oefen met code-syntax typen'
passage_drill: 'Tekstoefening'
passage_drill_desc: 'Typ passages uit boeken'
skill_tree: 'Vaardigheidsboom'
skill_tree_desc: 'Bekijk voortgangstakken en start oefeningen'
keyboard: 'Toetsenbord'
keyboard_desc: 'Verken toetsenbordindeling en toetsstatistieken'
statistics: 'Statistieken'
statistics_desc: 'Bekijk je typstatistieken'
settings: 'Instellingen'
settings_desc: 'Configureer keydr'
day_streak: ' | %{days} dagen reeks'
key_progress: ' Toetsvoortgang %{unlocked}/%{total} (%{mastered} beheerst) | Doel %{target} WPM%{streak}'
hint_start: '[1-3] Start'
hint_skill_tree: '[t] Vaardigheidsboom'
hint_keyboard: '[b] Toetsenbord'
hint_stats: '[s] Statistieken'
hint_settings: '[c] Instellingen'
hint_quit: '[q] Stoppen'
# Drill screen
drill:
title: ' Oefening '
mode_adaptive: 'Adaptief'
mode_code: 'Code (Ongerangschikt)'
mode_passage: 'Tekst (Ongerangschikt)'
focus_char: 'Aandacht: ''%{ch}'''
focus_bigram: 'Aandacht: "%{bigram}"'
focus_both: 'Aandacht: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Nwk'
header_err: 'Ftn'
code_source: ' Codebron '
passage_source: ' Tekstbron '
footer: '[ESC] Oefening stoppen [Backspace] Wissen'
keys_reenabled: 'Toetsen weer actief na %{ms}ms'
hint_end: '[ESC] Oefening stoppen'
hint_backspace: '[Backspace] Wissen'
# Dashboard / drill result
dashboard:
title: ' Oefening voltooid '
results: 'Resultaten'
unranked_note_prefix: ' (Ongerangschikt'
unranked_note_suffix: ' telt niet mee voor de vaardigheidsboom)'
speed: ' Snelheid: '
accuracy_label: ' Nauwkeurigheid:'
time_label: ' Tijd: '
errors_label: ' Fouten: '
correct_detail: ' (%{correct}/%{total} juist)'
input_blocked: ' Invoer tijdelijk geblokkeerd '
input_blocked_ms: '(%{ms}ms resterend)'
hint_continue: '[c/Enter/Space] Doorgaan'
hint_retry: '[r] Opnieuw'
hint_menu: '[q] Menu'
hint_stats: '[s] Statistieken'
hint_delete: '[x] Verwijderen'
# Stats sidebar (during drill)
sidebar:
title: ' Statistieken '
wpm: 'WPM: '
target: 'Doel: '
target_wpm: '%{wpm} WPM'
accuracy: 'Nauwkeurigheid: '
progress: 'Voortgang: '
correct: 'Correct: '
errors: 'Fouten: '
time: 'Tijd: '
last_drill: ' Vorige oefening '
vs_avg: ' vs gem: '
# Statistics dashboard
stats:
title: ' Statistieken '
empty: 'Nog geen oefeningen voltooid. Begin met typen!'
tab_dashboard: '[1] Dashboard'
tab_history: '[2] Geschiedenis'
tab_activity: '[3] Activiteit'
tab_accuracy: '[4] Nauwkeurigheid'
tab_timing: '[5] Timing'
tab_ngrams: '[6] N-grammen'
hint_back: '[ESC] Terug'
hint_next_tab: '[Tab] Volgend tabblad'
hint_switch_tab: '[1-6] Tabblad wisselen'
hint_navigate: '[j/k] Navigeren'
hint_page: '[PgUp/PgDn] Pagina'
hint_delete: '[x] Verwijderen'
summary_title: ' Samenvatting '
drills: ' Oefeningen: '
avg_wpm: ' Gem. WPM: '
best_wpm: ' Beste WPM: '
accuracy_label: ' Nauwkeurigheid: '
total_time: ' Totale tijd: '
wpm_chart_title: ' WPM per oefening (Laatste 20, Doel: %{target}) '
accuracy_chart_title: ' Nauwkeurigheid %% (Laatste 50 oefeningen) '
chart_drill: 'Oefening #'
chart_accuracy_pct: 'Nauwkeurigheid %%'
sessions_title: ' Recente sessies '
session_header: ' # WPM Ruw Nwk%% Tijd Datum/Tijd Modus Gerangsch. Deels'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Sessie #%{idx} verwijderen? (y/n)'
confirm_title: ' Bevestigen '
yes: 'ja'
no: 'nee'
keyboard_accuracy_title: ' Toetsenbord nauwkeurigheid %% '
keyboard_timing_title: ' Toetsenbord timing (ms) '
slowest_keys_title: ' Langzaamste toetsen (ms) '
fastest_keys_title: ' Snelste toetsen (ms) '
worst_accuracy_title: ' Slechtste nauwkeurigheid (%%) '
best_accuracy_title: ' Beste nauwkeurigheid (%%) '
not_enough_data: ' Niet genoeg gegevens'
streaks_title: ' Reeksen '
current_streak: ' Huidig: '
best_streak: ' Beste: '
active_days: ' Actieve dagen: '
top_days_none: ' Topdagen: geen'
top_days: ' Topdagen: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Nwk: %{pct}%%'
keys_label: ' Toetsen: %{unlocked}/%{total} (%{mastered} beheerst)'
ngram_empty: 'Voltooi enkele adaptieve oefeningen om n-gram gegevens te zien'
ngram_header_speed_narrow: ' Bgrm Snelh Verw Anom%'
ngram_header_error_narrow: ' Bgrm Ftn Stp Freq Verw Anom%'
ngram_header_speed: ' Bigram Snelh Verwacht Steekpr. Anom%'
ngram_header_error: ' Bigram Fouten Steekpr. Freq Verwacht Anom%'
focus_title: ' Actieve focus '
focus_char_label: ' Aandacht: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'fout'
anomaly_speed: 'snelheid'
focus_detail_both: ' Teken ''%{ch}'': zwakste toets | Bigram %{label}: %{type}-anomalie %{pct}%%'
focus_detail_char_only: ' Teken ''%{ch}'': zwakste toets, geen bevestigde bigram-anomalieen'
focus_detail_bigram_only: ' (%{type}-anomalie: %{pct}%%)'
focus_empty: ' Voltooi enkele adaptieve oefeningen om focusgegevens te zien'
error_anomalies_title: ' Fout-anomalieen (%{count}) '
no_error_anomalies: ' Geen fout-anomalieen gedetecteerd'
speed_anomalies_title: ' Snelheid-anomalieen (%{count}) '
no_speed_anomalies: ' Geen snelheid-anomalieen gedetecteerd'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Teken ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Dagelijkse activiteit (Sessies per dag) '
jan: 'Jan'
feb: 'Feb'
mar: 'Mrt'
apr: 'Apr'
may: 'Mei'
jun: 'Jun'
jul: 'Jul'
aug: 'Aug'
sep: 'Sep'
oct: 'Okt'
nov: 'Nov'
dec: 'Dec'
# Chart
chart:
wpm_over_time: ' WPM in de tijd '
drill_number: 'Oefening #'
# Settings
settings:
title: ' Instellingen '
subtitle: 'Pijltjestoetsen om te navigeren, Enter/Rechts om te wijzigen, ESC om op te slaan'
target_wpm: 'Doel-WPM'
theme: 'Thema'
word_count: 'Aantal woorden'
ui_language: 'UI-taal'
dictionary_language: 'Woordenboektaal'
keyboard_layout: 'Toetsenbordindeling'
code_language: 'Codetaal'
code_downloads: 'Code-downloads'
on: 'Aan'
off: 'Uit'
code_download_dir: 'Code-downloadmap'
snippets_per_repo: 'Fragmenten per repo'
unlimited: 'Onbeperkt'
download_code_now: 'Code nu downloaden'
run_downloader: 'Download starten'
passage_downloads: 'Tekst-downloads'
passage_download_dir: 'Tekst-downloadmap'
paragraphs_per_book: 'Alinea''s per boek'
whole_book: 'Heel boek'
download_passages_now: 'Teksten nu downloaden'
export_path: 'Exportpad'
export_data: 'Gegevens exporteren'
export_now: 'Nu exporteren'
import_path: 'Importpad'
import_data: 'Gegevens importeren'
import_now: 'Nu importeren'
hint_save_back: '[ESC] Opslaan & terug'
hint_change_value: '[Enter/pijltjes] Waarde wijzigen'
hint_edit_path: '[Enter op pad] Bewerken'
hint_move: '[←→] Verplaatsen'
hint_tab_complete: '[Tab] Aanvullen (aan einde)'
hint_confirm: '[Enter] Bevestigen'
hint_cancel: '[Esc] Annuleren'
success_title: ' Geslaagd '
error_title: ' Fout '
press_any_key: 'Druk op een toets'
file_exists_title: ' Bestand bestaat '
file_exists: 'Er bestaat al een bestand op dit pad.'
overwrite_rename: '[d] Overschrijven [r] Hernoemen [Esc] Annuleren'
erase_warning: 'Dit zal je huidige gegevens wissen.'
export_first: 'Exporteer eerst als je ze wilt bewaren.'
proceed_yn: 'Doorgaan? (y/n)'
confirm_import_title: ' Import bevestigen '
# Selection screens
select:
dictionary_language_title: ' Woordenboektaal selecteren '
keyboard_layout_title: ' Toetsenbordindeling selecteren '
code_language_title: ' Codetaal selecteren '
passage_source_title: ' Tekstbron selecteren '
ui_language_title: ' UI-taal selecteren '
more_above: '... %{count} meer boven ...'
more_below: '... %{count} meer onder ...'
current: ' (huidig)'
disabled: ' (uitgeschakeld)'
enabled_default: ' (ingeschakeld, standaard: %{layout})'
enabled: ' (ingeschakeld)'
disabled_blocked: ' (uitgeschakeld: geblokkeerd)'
built_in: ' (ingebouwd)'
cached: ' (opgeslagen)'
disabled_download: ' (uitgeschakeld: download vereist)'
download_required: ' (download vereist)'
hint_navigate: '[Omhoog/Omlaag/PgUp/PgDn] Navigeren'
hint_confirm: '[Enter] Bevestigen'
hint_back: '[ESC] Terug'
language_resets_layout: 'Taalselectie herstelt de toetsenbordindeling naar de standaard van die taal.'
layout_no_language_change: 'Indelingswijzigingen veranderen de woordenboektaal niet.'
disabled_network_notice: 'Sommige talen zijn uitgeschakeld: schakel netwerkdownloads in via intro/instellingen.'
disabled_sources_notice: 'Sommige bronnen zijn uitgeschakeld: schakel netwerkdownloads in via intro/instellingen.'
passage_all: 'Alles (Ingebouwd + alle boeken)'
passage_builtin: 'Alleen ingebouwde passages'
passage_book_prefix: 'Boek: %{title}'
# Progress
progress:
overall_key_progress: 'Totale toetsvoortgang'
unlocked_mastered: '%{unlocked}/%{total} ontgrendeld (%{mastered} beheerst)'
# Skill tree
skill_tree:
title: ' Vaardigheidsboom '
locked: 'Vergrendeld'
unlocked: 'ontgrendeld'
mastered: 'beheerst'
in_progress: 'bezig'
complete: 'voltooid'
locked_status: 'vergrendeld'
locked_notice: 'Voltooi %{count} basisletters om takken te ontgrendelen'
branches_separator: 'Takken (beschikbaar na %{count} basisletters)'
unlocked_letters: '%{unlocked}/%{total} letters ontgrendeld'
level: 'Niveau %{current}/%{total}'
level_zero: 'Niveau 0/%{total}'
in_focus: ' in aandacht'
hint_navigate: '[↑↓/jk] Navigeren'
hint_scroll: '[PgUp/PgDn of Ctrl+U/Ctrl+D] Scrollen'
hint_back: '[q] Terug'
hint_unlock: '[Enter] Ontgrendelen'
hint_start_drill: '[Enter] Oefening starten'
unlock_msg_1: 'Na ontgrendeling worden ontgrendelde toetsen van deze tak gemengd in de adaptieve oefening.'
unlock_msg_2: 'Wil je alleen deze tak oefenen, start dan een oefening direct vanuit deze tak in de Vaardigheidsboom.'
confirm_unlock: '%{branch} ontgrendelen?'
confirm_yn: '[y] Ontgrendelen [n/ESC] Annuleren'
lvl_prefix: 'Niv'
branch_primary_letters: 'Basisletters'
branch_capital_letters: 'Hoofdletters'
branch_numbers: 'Cijfers 0-9'
branch_prose_punctuation: 'Leestekens'
branch_whitespace: 'Witruimte'
branch_code_symbols: 'Code-symbolen'
level_frequency_order: 'Frequentievolgorde'
level_common_sentence_capitals: 'Veelvoorkomende zinhoofdletters'
level_name_capitals: 'Naamhoofdletters'
level_remaining_capitals: 'Overige hoofdletters'
level_common_digits: 'Veelvoorkomende cijfers'
level_all_digits: 'Alle cijfers'
level_essential: 'Essentieel'
level_common: 'Veelvoorkomend'
level_expressive: 'Expressief'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Inspringen'
level_arithmetic_assignment: 'Rekenen & Toewijzing'
level_grouping: 'Groepering'
level_logic_reference: 'Logica & Referentie'
level_special: 'Speciaal'
# Milestones
milestones:
unlock_title: ' Toets ontgrendeld! '
mastery_title: ' Toets beheerst! '
branches_title: ' Nieuwe vaardigheidstakken beschikbaar! '
branch_complete_title: ' Tak voltooid! '
all_unlocked_title: ' Alle toetsen ontgrendeld! '
all_mastered_title: ' Volledig toetsenbordbheersing! '
unlocked: 'ontgrendeld'
mastered: 'beheerst'
use_finger: 'Gebruik je %{finger}'
hold_right_shift: 'Houd rechter Shift ingedrukt (rechter pink)'
hold_left_shift: 'Houd linker Shift ingedrukt (linker pink)'
congratulations_all_letters: 'Gefeliciteerd! Je hebt alle %{count} basisletters beheerst'
new_branches_available: 'Nieuwe vaardigheidstakken zijn nu beschikbaar:'
visit_skill_tree: 'Bezoek de Vaardigheidsboom om een nieuwe tak te ontgrendelen'
and_start_training: 'en begin met trainen!'
open_skill_tree: 'Druk op [t] om de Vaardigheidsboom te openen'
branch_complete_msg: 'Je hebt de tak %{branch} voltooid!'
all_levels_mastered: 'Alle %{count} niveaus beheerst.'
all_keys_confident: 'Elke toets in deze tak heeft vol vertrouwen.'
all_unlocked_msg: 'Je hebt elke toets op het toetsenbord ontgrendeld!'
all_unlocked_desc: 'Elk teken, symbool en elke modifier is nu beschikbaar in je oefeningen.'
keep_practicing_mastery: 'Blijf oefenen om meesterschap op te bouwen — zodra elke toets vol'
confidence_complete: 'vertrouwen bereikt, heb je volledige toetsenbordbeheersing!'
all_mastered_msg: 'Gefeliciteerd — je hebt volledige toetsenbordbeheersing bereikt!'
all_mastered_desc: 'Elke toets op het toetsenbord heeft maximaal vertrouwen.'
mastery_takes_practice: 'Meesterschap is geen bestemming — het vereist voortdurend oefenen.'
keep_drilling: 'Blijf oefenen om je niveau te behouden.'
hint_skill_tree_continue: '[t] Vaardigheidsboom [Andere toets] Doorgaan'
hint_any_key: 'Druk op een toets om door te gaan'
input_blocked: 'Invoer tijdelijk geblokkeerd (%{ms}ms resterend)'
unlock_msg_1: 'Goed gedaan! Blijf je typvaardigheden opbouwen.'
unlock_msg_2: 'Weer een toets erbij in je arsenaal!'
unlock_msg_3: 'Je toetsenbord groeit! Ga zo door.'
unlock_msg_4: 'Een stap dichter bij volledige toetsenbordbeheersing!'
mastery_msg_1: 'Deze toets heeft nu vol vertrouwen!'
mastery_msg_2: 'Je beheerst deze toets perfect!'
mastery_msg_3: 'Spiergeheugen verankerd!'
mastery_msg_4: 'Weer een toets veroverd!'
# Keyboard explorer
keyboard:
title: ' Toetsenbord '
subtitle: 'Druk op een toets of klik op een toets'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigeren'
hint_back: '[q/ESC] Terug'
key_label: 'Toets: '
finger_label: 'Vinger: '
hand_left: 'Links'
hand_right: 'Rechts'
finger_index: 'Wijsvinger'
finger_middle: 'Middelvinger'
finger_ring: 'Ringvinger'
finger_pinky: 'Pink'
finger_thumb: 'Duim'
overall_accuracy: ' Totale nauwkeurigheid: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Gerangschikte nauwkeurigheid: %{correct}/%{total} (%{pct}%%)'
confidence: 'Vertrouwen: '
no_data: 'Nog geen gegevens'
no_data_short: 'Geen gegevens'
key_details: ' Toetsdetails '
key_details_char: ' Toetsdetails: ''%{ch}'' '
key_details_name: ' Toetsdetails: %{name} '
press_key_hint: 'Druk op een toets voor details'
shift_label: 'Shift: '
shift_no: 'Nee'
overall_avg_time: 'Totaal gem. tijd: '
overall_best_time: 'Totaal beste tijd: '
overall_samples: 'Totaal steekproeven: '
overall_accuracy_label: 'Totaal nauwkeurigheid: '
branch_label: 'Tak: '
level_label: 'Niveau: '
built_in_key: 'Ingebouwde toets'
unlocked_label: 'Ontgrendeld: '
yes: 'Ja'
no: 'Nee'
in_focus_label: 'In focus?: '
mastery_label: 'Beheersing: '
mastery_locked: 'Vergrendeld'
ranked_avg_time: 'Gerangschikte gem. tijd: '
ranked_best_time: 'Gerangschikte beste tijd: '
ranked_samples: 'Gerangschikte steekproeven: '
ranked_accuracy_label: 'Gerangschikte nauwkeurigheid: '
# Intro dialogs
intro:
passage_title: ' Tekstdownload instellen '
code_title: ' Code-download instellen '
enable_downloads: 'Netwerkdownloads inschakelen'
download_dir: 'Downloadmap'
paragraphs_per_book: 'Alinea''s per boek (0 = geheel)'
whole_book: 'heel boek'
snippets_per_repo: 'Fragmenten per repo (0 = onbeperkt)'
unlimited: 'onbeperkt'
start_passage_drill: 'Tekstoefening starten'
start_code_drill: 'Code-oefening starten'
confirm: 'Bevestigen'
hint_navigate: '[Omhoog/Omlaag] Navigeren'
hint_adjust: '[Links/Rechts] Aanpassen'
hint_edit: '[Typen/Backspace] Bewerken'
hint_confirm: '[Enter] Bevestigen'
hint_cancel: '[ESC] Annuleren'
preparing_download: 'Download wordt voorbereid...'
download_passage_title: ' Tekstbron downloaden '
download_code_title: ' Codebron downloaden '
book_label: ' Boek: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bytes'
downloaded_bytes: 'Gedownload: %{bytes} bytes'
downloading_book_progress: 'Huidig boek downloaden: [%{bar}] %{downloaded}/%{total} bytes'
downloading_book_bytes: 'Huidig boek downloaden: %{bytes} bytes'
downloading_code_progress: 'Downloaden: [%{bar}] %{downloaded}/%{total} bytes'
downloading_code_bytes: 'Downloaden: %{bytes} bytes'
current_book: 'Huidig: %{name} (boek %{done}/%{total})'
current_repo: 'Huidig: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr kan passages van Project Gutenberg downloaden om te oefenen met typen.'
passage_instructions_2: 'Boeken worden eenmalig gedownload en lokaal opgeslagen.'
passage_instructions_3: 'Configureer de downloadinstellingen hieronder en start een tekstoefening.'
code_instructions_1: 'keydr kan open-source code van GitHub downloaden om te oefenen met typen.'
code_instructions_2: 'Code wordt eenmalig gedownload en lokaal opgeslagen.'
code_instructions_3: 'Configureer de downloadinstellingen hieronder en start een code-oefening.'
# Status messages (from app.rs)
status:
recovery_files: 'Herstelbestanden gevonden van onderbroken import. Gegevens kunnen inconsistent zijn — overweeg opnieuw importeren.'
dir_not_exist: 'Map bestaat niet: %{path}'
no_data_store: 'Geen gegevensopslag beschikbaar'
serialization_error: 'Serialisatiefout: %{error}'
exported_to: 'Geexporteerd naar %{path}'
export_failed: 'Export mislukt: %{error}'
could_not_read: 'Kan bestand niet lezen: %{error}'
invalid_export: 'Ongeldig exportbestand: %{error}'
unsupported_version: 'Niet-ondersteunde exportversie: %{got} (verwacht %{expected})'
import_failed: 'Import mislukt: %{error}'
imported_theme_fallback: 'Succesvol geimporteerd (thema ''%{theme}'' niet gevonden, standaard wordt gebruikt)'
imported_success: 'Succesvol geimporteerd'
adaptive_unavailable: 'Adaptieve gerangschikte modus niet beschikbaar: %{error}'
switched_to: 'Gewisseld naar %{name}'
layout_changed: 'Indeling gewijzigd naar %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Onbekende taal: %{key}'
unknown_layout: 'Onbekende toetsenbordindeling: %{key}'
unsupported_pair: 'Niet-ondersteund taal-/indelingspaar: %{language} + %{layout}'
language_blocked: 'Taal geblokkeerd door ondersteuningsniveau: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Terug'

454
locales/pl.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminalowy trener pisania'
adaptive_drill: 'Trening adaptacyjny'
adaptive_drill_desc: 'Fonetyczne slowa z adaptacyjnym odblokowywaniem liter'
code_drill: 'Trening kodu'
code_drill_desc: 'Cwicz pisanie skladni kodu'
passage_drill: 'Trening tekstu'
passage_drill_desc: 'Przepisuj fragmenty ksiazek'
skill_tree: 'Drzewo umiejetnosci'
skill_tree_desc: 'Przegladaj sciezki postepu i uruchamiaj treningi'
keyboard: 'Klawiatura'
keyboard_desc: 'Poznaj uklad klawiatury i statystyki klawiszy'
statistics: 'Statystyki'
statistics_desc: 'Przegladaj statystyki pisania'
settings: 'Ustawienia'
settings_desc: 'Konfiguruj keydr'
day_streak: ' | %{days} dni z rzedu'
key_progress: ' Postep klawiszy %{unlocked}/%{total} (%{mastered} opanowanych) | Cel %{target} WPM%{streak}'
hint_start: '[1-3] Rozpocznij'
hint_skill_tree: '[t] Drzewo'
hint_keyboard: '[b] Klawiatura'
hint_stats: '[s] Statystyki'
hint_settings: '[c] Ustawienia'
hint_quit: '[q] Wyjdz'
# Drill screen
drill:
title: ' Trening '
mode_adaptive: 'Adaptacyjny'
mode_code: 'Kod (bez rankingu)'
mode_passage: 'Tekst (bez rankingu)'
focus_char: 'Fokus: ''%{ch}'''
focus_bigram: 'Fokus: "%{bigram}"'
focus_both: 'Fokus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Dok'
header_err: 'Bld'
code_source: ' Zrodlo kodu '
passage_source: ' Zrodlo tekstu '
footer: '[ESC] Zakoncz trening [Backspace] Usun'
keys_reenabled: 'Klawisze przywrocone w %{ms}ms'
hint_end: '[ESC] Zakoncz trening'
hint_backspace: '[Backspace] Usun'
# Dashboard / drill result
dashboard:
title: ' Trening zakonczony '
results: 'Wyniki'
unranked_note_prefix: ' (Bez rankingu'
unranked_note_suffix: ' nie liczy sie do drzewa umiejetnosci)'
speed: ' Predkosc: '
accuracy_label: ' Dokladnosc:'
time_label: ' Czas: '
errors_label: ' Bledy: '
correct_detail: ' (%{correct}/%{total} poprawnych)'
input_blocked: ' Wejscie tymczasowo zablokowane '
input_blocked_ms: '(%{ms}ms pozostalo)'
hint_continue: '[c/Enter/Space] Kontynuuj'
hint_retry: '[r] Powtorz'
hint_menu: '[q] Menu'
hint_stats: '[s] Statystyki'
hint_delete: '[x] Usun'
# Stats sidebar (during drill)
sidebar:
title: ' Statystyki '
wpm: 'WPM: '
target: 'Cel: '
target_wpm: '%{wpm} WPM'
accuracy: 'Dokladnosc: '
progress: 'Postep: '
correct: 'Poprawne: '
errors: 'Bledy: '
time: 'Czas: '
last_drill: ' Ostatni trening '
vs_avg: ' vs sr: '
# Statistics dashboard
stats:
title: ' Statystyki '
empty: 'Brak ukonczonych treningow. Zacznij pisac!'
tab_dashboard: '[1] Przeglad'
tab_history: '[2] Historia'
tab_activity: '[3] Aktywnosc'
tab_accuracy: '[4] Dokladnosc'
tab_timing: '[5] Czas reakcji'
tab_ngrams: '[6] N-gramy'
hint_back: '[ESC] Wstecz'
hint_next_tab: '[Tab] Nastepna karta'
hint_switch_tab: '[1-6] Zmien karte'
hint_navigate: '[j/k] Nawiguj'
hint_page: '[PgUp/PgDn] Strona'
hint_delete: '[x] Usun'
summary_title: ' Podsumowanie '
drills: ' Treningi: '
avg_wpm: ' Sr WPM: '
best_wpm: ' Najlepszy WPM: '
accuracy_label: ' Dokladnosc: '
total_time: ' Laczny czas: '
wpm_chart_title: ' WPM na trening (ostatnie 20, cel: %{target}) '
accuracy_chart_title: ' Dokladnosc %% (ostatnie 50 treningow) '
chart_drill: 'Tren #'
chart_accuracy_pct: 'Dokladnosc %%'
sessions_title: ' Ostatnie sesje '
session_header: ' # WPM Raw Dok%% Czas Data/Czas Tryb Ranking Czesciowy'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Usunac sesje #%{idx}? (t/n)'
confirm_title: ' Potwierdz '
yes: 'tak'
no: 'nie'
keyboard_accuracy_title: ' Dokladnosc klawiatury %% '
keyboard_timing_title: ' Czas reakcji klawiatury (ms) '
slowest_keys_title: ' Najwolniejsze klawisze (ms) '
fastest_keys_title: ' Najszybsze klawisze (ms) '
worst_accuracy_title: ' Najgorsza dokladnosc (%%) '
best_accuracy_title: ' Najlepsza dokladnosc (%%) '
not_enough_data: ' Za malo danych'
streaks_title: ' Serie '
current_streak: ' Biezaca: '
best_streak: ' Najlepsza: '
active_days: ' Aktywne dni: '
top_days_none: ' Najlepsze dni: brak'
top_days: ' Najlepsze dni: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Dok: %{pct}%%'
keys_label: ' Klawisze: %{unlocked}/%{total} (%{mastered} opanowanych)'
ngram_empty: 'Wykonaj treningi adaptacyjne, aby zobaczyc dane n-gramow'
ngram_header_speed_narrow: ' Bgrm Predk Oczek Anom%'
ngram_header_error_narrow: ' Bgrm Bld Prb Czest Oczek Anom%'
ngram_header_speed: ' Bigram Predkosc Oczekiw Probki Anom%'
ngram_header_error: ' Bigram Bledy Probki Czest Oczekiw Anom%'
focus_title: ' Aktywny fokus '
focus_char_label: ' Fokus: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'blad'
anomaly_speed: 'predkosc'
focus_detail_both: ' Znak ''%{ch}'': najslabszy klawisz | Bigram %{label}: anomalia %{type} %{pct}%%'
focus_detail_char_only: ' Znak ''%{ch}'': najslabszy klawisz, brak potwierdzonych anomalii bigramow'
focus_detail_bigram_only: ' (anomalia %{type}: %{pct}%%)'
focus_empty: ' Wykonaj treningi adaptacyjne, aby zobaczyc dane fokusu'
error_anomalies_title: ' Anomalie bledow (%{count}) '
no_error_anomalies: ' Nie wykryto anomalii bledow'
speed_anomalies_title: ' Anomalie predkosci (%{count}) '
no_speed_anomalies: ' Nie wykryto anomalii predkosci'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Wah: >%{ms}ms'
focus_char_value: 'Znak ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Dzienna aktywnosc (sesje na dzien) '
jan: 'Sty'
feb: 'Lut'
mar: 'Mar'
apr: 'Kwi'
may: 'Maj'
jun: 'Cze'
jul: 'Lip'
aug: 'Sie'
sep: 'Wrz'
oct: 'Paz'
nov: 'Lis'
dec: 'Gru'
# Chart
chart:
wpm_over_time: ' WPM w czasie '
drill_number: 'Tren #'
# Settings
settings:
title: ' Ustawienia '
subtitle: 'Uzyj strzalek do nawigacji, Enter/prawo zmienia, ESC zapisuje i wychodzi'
target_wpm: 'Docelowy WPM'
theme: 'Motyw'
word_count: 'Liczba slow'
ui_language: 'Jezyk interfejsu'
dictionary_language: 'Jezyk slownika'
keyboard_layout: 'Uklad klawiatury'
code_language: 'Jezyk programowania'
code_downloads: 'Pobieranie kodu'
on: 'Wlacz'
off: 'Wylacz'
code_download_dir: 'Katalog pobierania kodu'
snippets_per_repo: 'Fragmenty na repo'
unlimited: 'Bez limitu'
download_code_now: 'Pobierz kod teraz'
run_downloader: 'Uruchom pobieranie'
passage_downloads: 'Pobieranie tekstow'
passage_download_dir: 'Katalog pobierania tekstow'
paragraphs_per_book: 'Akapity na ksiazke'
whole_book: 'Cala ksiazka'
download_passages_now: 'Pobierz teksty teraz'
export_path: 'Sciezka eksportu'
export_data: 'Eksportuj dane'
export_now: 'Eksportuj teraz'
import_path: 'Sciezka importu'
import_data: 'Importuj dane'
import_now: 'Importuj teraz'
hint_save_back: '[ESC] Zapisz i wstecz'
hint_change_value: '[Enter/strzalki] Zmien wartosc'
hint_edit_path: '[Enter na sciezce] Edytuj'
hint_move: '[←→] Przesun'
hint_tab_complete: '[Tab] Uzupelnij (na koncu)'
hint_confirm: '[Enter] Potwierdz'
hint_cancel: '[Esc] Anuluj'
success_title: ' Sukces '
error_title: ' Blad '
press_any_key: 'Nacisnij dowolny klawisz'
file_exists_title: ' Plik istnieje '
file_exists: 'Plik juz istnieje w tej sciezce.'
overwrite_rename: '[d] Nadpisz [r] Zmien nazwe [Esc] Anuluj'
erase_warning: 'To skasuje Twoje obecne dane.'
export_first: 'Najpierw wyeksportuj, jesli chcesz je zachowac.'
proceed_yn: 'Kontynuowac? (t/n)'
confirm_import_title: ' Potwierdz import '
# Selection screens
select:
dictionary_language_title: ' Wybierz jezyk slownika '
keyboard_layout_title: ' Wybierz uklad klawiatury '
code_language_title: ' Wybierz jezyk programowania '
passage_source_title: ' Wybierz zrodlo tekstu '
ui_language_title: ' Wybierz jezyk interfejsu '
more_above: '... %{count} wiecej powyzej ...'
more_below: '... %{count} wiecej ponizej ...'
current: ' (biezacy)'
disabled: ' (wylaczony)'
enabled_default: ' (wlaczony, domyslny: %{layout})'
enabled: ' (wlaczony)'
disabled_blocked: ' (wylaczony: zablokowany)'
built_in: ' (wbudowany)'
cached: ' (w pamieci podrecznej)'
disabled_download: ' (wylaczony: wymagane pobranie)'
download_required: ' (wymagane pobranie)'
hint_navigate: '[Gora/Dol/PgUp/PgDn] Nawiguj'
hint_confirm: '[Enter] Potwierdz'
hint_back: '[ESC] Wstecz'
language_resets_layout: 'Wybor jezyka resetuje uklad klawiatury do domyslnego dla tego jezyka.'
layout_no_language_change: 'Zmiana ukladu nie zmienia jezyka slownika.'
disabled_network_notice: 'Niektorej jezyki sa wylaczone: wlacz pobieranie sieciowe w ustawieniach.'
disabled_sources_notice: 'Niektore zrodla sa wylaczone: wlacz pobieranie sieciowe w ustawieniach.'
passage_all: 'Wszystkie (wbudowane + wszystkie ksiazki)'
passage_builtin: 'Tylko wbudowane teksty'
passage_book_prefix: 'Ksiazka: %{title}'
# Progress
progress:
overall_key_progress: 'Ogolny postep klawiszy'
unlocked_mastered: '%{unlocked}/%{total} odblokowanych (%{mastered} opanowanych)'
# Skill tree
skill_tree:
title: ' Drzewo umiejetnosci '
locked: 'Zablokowany'
unlocked: 'odblokowany'
mastered: 'opanowany'
in_progress: 'w toku'
complete: 'ukonczony'
locked_status: 'zablokowany'
locked_notice: 'Ukoncz %{count} podstawowych liter, aby odblokowac galezi'
branches_separator: 'Galezi (dostepne po %{count} podstawowych literach)'
unlocked_letters: 'Odblokowano %{unlocked}/%{total} liter'
level: 'Poziom %{current}/%{total}'
level_zero: 'Poziom 0/%{total}'
in_focus: ' w fokusie'
hint_navigate: '[↑↓/jk] Nawiguj'
hint_scroll: '[PgUp/PgDn lub Ctrl+U/Ctrl+D] Przewin'
hint_back: '[q] Wstecz'
hint_unlock: '[Enter] Odblokuj'
hint_start_drill: '[Enter] Rozpocznij trening'
unlock_msg_1: 'Po odblokowaniu domyslny trening adaptacyjny bedzie wlaczal klawisze z tej galezi.'
unlock_msg_2: 'Jesli chcesz skupic sie tylko na tej galezi, uruchom trening bezposrednio z drzewa.'
confirm_unlock: 'Odblokowac %{branch}?'
confirm_yn: '[y] Odblokuj [n/ESC] Anuluj'
lvl_prefix: 'Poz'
branch_primary_letters: 'Podstawowe litery'
branch_capital_letters: 'Wielkie litery'
branch_numbers: 'Cyfry 0-9'
branch_prose_punctuation: 'Interpunkcja'
branch_whitespace: 'Biale znaki'
branch_code_symbols: 'Symbole kodu'
level_frequency_order: 'Kolejnosc czestotliwosci'
level_common_sentence_capitals: 'Popularne wielkie litery zdan'
level_name_capitals: 'Wielkie litery imion'
level_remaining_capitals: 'Pozostale wielkie litery'
level_common_digits: 'Popularne cyfry'
level_all_digits: 'Wszystkie cyfry'
level_essential: 'Niezbedne'
level_common: 'Popularne'
level_expressive: 'Ekspresyjne'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Wciecie'
level_arithmetic_assignment: 'Arytmetyka i przypisanie'
level_grouping: 'Grupowanie'
level_logic_reference: 'Logika i referencje'
level_special: 'Specjalne'
# Milestones
milestones:
unlock_title: ' Klawisz odblokowany! '
mastery_title: ' Klawisz opanowany! '
branches_title: ' Nowe galezi dostepne! '
branch_complete_title: ' Galaz ukonczona! '
all_unlocked_title: ' Wszystkie klawisze odblokowane! '
all_mastered_title: ' Pelne opanowanie klawiatury! '
unlocked: 'odblokowany'
mastered: 'opanowany'
use_finger: 'Uzyj %{finger}'
hold_right_shift: 'Przytrzymaj prawy Shift (prawy maly palec)'
hold_left_shift: 'Przytrzymaj lewy Shift (lewy maly palec)'
congratulations_all_letters: 'Gratulacje! Opanowales wszystkie %{count} podstawowych liter'
new_branches_available: 'Nowe galezi umiejetnosci sa teraz dostepne:'
visit_skill_tree: 'Odwiedz drzewo umiejetnosci, aby odblokowac nowa galaz'
and_start_training: 'i zacznij trening!'
open_skill_tree: 'Nacisnij [t], aby otworzyc drzewo umiejetnosci'
branch_complete_msg: 'Ukonczyles galaz %{branch}!'
all_levels_mastered: 'Wszystkie %{count} poziomow opanowanych.'
all_keys_confident: 'Kazdy klawisz w tej galezi jest na pelnej pewnosci.'
all_unlocked_msg: 'Odblokowano kazdy klawisz na klawiaturze!'
all_unlocked_desc: 'Kazdy znak, symbol i modyfikator jest teraz dostepny w Twoich treningach.'
keep_practicing_mastery: 'Kontynuuj cwiczenie, aby budowac bieglosc — gdy kazdy klawisz osiagnie pelna'
confidence_complete: 'pewnosc, osiagniesz pelne opanowanie klawiatury!'
all_mastered_msg: 'Gratulacje — osiagnales pelne opanowanie klawiatury!'
all_mastered_desc: 'Kazdy klawisz na klawiaturze jest na maksymalnej pewnosci.'
mastery_takes_practice: 'Bieglosc to nie cel — wymaga ciaglej praktyki.'
keep_drilling: 'Kontynuuj treningi, aby utrzymac swoj poziom.'
hint_skill_tree_continue: '[t] Otworz drzewo [Inny klawisz] Kontynuuj'
hint_any_key: 'Nacisnij dowolny klawisz, aby kontynuowac'
input_blocked: 'Wejscie tymczasowo zablokowane (%{ms}ms pozostalo)'
unlock_msg_1: 'Swietna robota! Rozwijaj swoje umiejetnosci pisania.'
unlock_msg_2: 'Kolejny klawisz w Twoim arsenale!'
unlock_msg_3: 'Twoja klawiatura rosnie! Tak trzymaj.'
unlock_msg_4: 'Krok blizej do pelnego opanowania klawiatury!'
mastery_msg_1: 'Ten klawisz jest teraz na pelnej pewnosci!'
mastery_msg_2: 'Ten klawisz masz w malym palcu!'
mastery_msg_3: 'Pamiec miesniowa zablokowana!'
mastery_msg_4: 'Kolejny klawisz podbity!'
# Keyboard explorer
keyboard:
title: ' Klawiatura '
subtitle: 'Nacisnij dowolny klawisz lub kliknij klawisz'
hint_navigate: '[←→↑↓/hjkl/Tab] Nawiguj'
hint_back: '[q/ESC] Wstecz'
key_label: 'Klawisz: '
finger_label: 'Palec: '
hand_left: 'Lewy'
hand_right: 'Prawy'
finger_index: 'Wskazujacy'
finger_middle: 'Srodkowy'
finger_ring: 'Serdeczny'
finger_pinky: 'Maly'
finger_thumb: 'Kciuk'
overall_accuracy: ' Ogolna dokladnosc: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Rankingowa dokladnosc: %{correct}/%{total} (%{pct}%%)'
confidence: 'Pewnosc: '
no_data: 'Brak danych'
no_data_short: 'Brak danych'
key_details: ' Szczegoly klawisza '
key_details_char: ' Szczegoly klawisza: ''%{ch}'' '
key_details_name: ' Szczegoly klawisza: %{name} '
press_key_hint: 'Nacisnij klawisz, aby zobaczyc szczegoly'
shift_label: 'Shift: '
shift_no: 'Nie'
overall_avg_time: 'Sredni czas: '
overall_best_time: 'Najlepszy czas: '
overall_samples: 'Probki: '
overall_accuracy_label: 'Ogolna dokladnosc: '
branch_label: 'Galaz: '
level_label: 'Poziom: '
built_in_key: 'Wbudowany klawisz'
unlocked_label: 'Odblokowany: '
yes: 'Tak'
no: 'Nie'
in_focus_label: 'W fokusie?: '
mastery_label: 'Bieglosc: '
mastery_locked: 'Zablokowany'
ranked_avg_time: 'Rankingowy sr czas: '
ranked_best_time: 'Rankingowy najl czas: '
ranked_samples: 'Rankingowe probki: '
ranked_accuracy_label: 'Rankingowa dokladnosc: '
# Intro dialogs
intro:
passage_title: ' Ustawienia pobierania tekstow '
code_title: ' Ustawienia pobierania kodu '
enable_downloads: 'Wlacz pobieranie sieciowe'
download_dir: 'Katalog pobierania'
paragraphs_per_book: 'Akapity na ksiazke (0 = cala)'
whole_book: 'cala ksiazka'
snippets_per_repo: 'Fragmenty na repo (0 = bez limitu)'
unlimited: 'bez limitu'
start_passage_drill: 'Rozpocznij trening tekstu'
start_code_drill: 'Rozpocznij trening kodu'
confirm: 'Potwierdz'
hint_navigate: '[Gora/Dol] Nawiguj'
hint_adjust: '[Lewo/Prawo] Dostosuj'
hint_edit: '[Pisz/Backspace] Edytuj'
hint_confirm: '[Enter] Potwierdz'
hint_cancel: '[ESC] Anuluj'
preparing_download: 'Przygotowywanie pobierania...'
download_passage_title: ' Pobieranie zrodla tekstu '
download_code_title: ' Pobieranie zrodla kodu '
book_label: ' Ksiazka: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bajtow'
downloaded_bytes: 'Pobrano: %{bytes} bajtow'
downloading_book_progress: 'Pobieranie ksiazki: [%{bar}] %{downloaded}/%{total} bajtow'
downloading_book_bytes: 'Pobieranie ksiazki: %{bytes} bajtow'
downloading_code_progress: 'Pobieranie: [%{bar}] %{downloaded}/%{total} bajtow'
downloading_code_bytes: 'Pobieranie: %{bytes} bajtow'
current_book: 'Biezaca: %{name} (ksiazka %{done}/%{total})'
current_repo: 'Biezace: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr moze pobierac teksty z Project Gutenberg do cwiczenia pisania.'
passage_instructions_2: 'Ksiazki sa pobierane raz i przechowywane lokalnie.'
passage_instructions_3: 'Skonfiguruj ustawienia pobierania ponizej, a nastepnie rozpocznij trening.'
code_instructions_1: 'keydr moze pobierac otwarty kod zrodlowy z GitHub do cwiczenia pisania.'
code_instructions_2: 'Kod jest pobierany raz i przechowywany lokalnie.'
code_instructions_3: 'Skonfiguruj ustawienia pobierania ponizej, a nastepnie rozpocznij trening.'
# Status messages (from app.rs)
status:
recovery_files: 'Znaleziono pliki odzyskiwania z przerwanego importu. Dane moga byc niespojne — rozważ ponowny import.'
dir_not_exist: 'Katalog nie istnieje: %{path}'
no_data_store: 'Brak dostepnego magazynu danych'
serialization_error: 'Blad serializacji: %{error}'
exported_to: 'Wyeksportowano do %{path}'
export_failed: 'Eksport nie powiodl sie: %{error}'
could_not_read: 'Nie mozna odczytac pliku: %{error}'
invalid_export: 'Nieprawidlowy plik eksportu: %{error}'
unsupported_version: 'Nieobslugiwana wersja eksportu: %{got} (oczekiwana %{expected})'
import_failed: 'Import nie powiodl sie: %{error}'
imported_theme_fallback: 'Zaimportowano pomyslnie (motyw ''%{theme}'' nie znaleziony, uzywam domyslnego)'
imported_success: 'Zaimportowano pomyslnie'
adaptive_unavailable: 'Tryb adaptacyjny rankingowy niedostepny: %{error}'
switched_to: 'Przelaczono na %{name}'
layout_changed: 'Uklad zmieniony na %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Nieznany jezyk: %{key}'
unknown_layout: 'Nieznany uklad klawiatury: %{key}'
unsupported_pair: 'Nieobslugiwana para jezyk/uklad: %{language} + %{layout}'
language_blocked: 'Jezyk zablokowany przez poziom wsparcia: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Wstecz'

454
locales/pt.yml Normal file
View File

@@ -0,0 +1,454 @@
# Menu principal
menu:
subtitle: 'Tutor de Digitação no Terminal'
adaptive_drill: 'Exercício Adaptativo'
adaptive_drill_desc: 'Palavras fonéticas com desbloqueio adaptativo de teclas'
code_drill: 'Exercício de Código'
code_drill_desc: 'Pratique digitando sintaxe de código'
passage_drill: 'Exercício de Passagem'
passage_drill_desc: 'Digite passagens de livros'
skill_tree: 'Árvore de Habilidades'
skill_tree_desc: 'Ver ramos de progressão e iniciar exercícios'
keyboard: 'Teclado'
keyboard_desc: 'Explore o layout do teclado e estatísticas'
statistics: 'Estatísticas'
statistics_desc: 'Ver suas estatísticas de digitação'
settings: 'Configurações'
settings_desc: 'Configurar keydr'
day_streak: ' | %{days} dias seguidos'
key_progress: ' Progresso de Teclas %{unlocked}/%{total} (%{mastered} dominadas) | Meta %{target} WPM%{streak}'
hint_start: '[1-3] Iniciar'
hint_skill_tree: '[t] Árvore de Habilidades'
hint_keyboard: '[b] Teclado'
hint_stats: '[s] Estatísticas'
hint_settings: '[c] Configurações'
hint_quit: '[q] Sair'
# Tela de exercício
drill:
title: ' Exercício '
mode_adaptive: 'Adaptativo'
mode_code: 'Código (Sem ranking)'
mode_passage: 'Passagem (Sem ranking)'
focus_char: 'Foco: ''%{ch}'''
focus_bigram: 'Foco: "%{bigram}"'
focus_both: 'Foco: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Pre'
header_err: 'Err'
code_source: ' Fonte do código '
passage_source: ' Fonte da passagem '
footer: '[ESC] Fim [Backspace] Apagar'
keys_reenabled: 'Teclas reativadas em %{ms}ms'
hint_end: '[ESC] Fim do exercício'
hint_backspace: '[Backspace] Apagar'
# Painel / resultado do exercício
dashboard:
title: ' Exercício Completo '
results: 'Resultados'
unranked_note_prefix: ' (Sem ranking'
unranked_note_suffix: ' não conta para a árvore de habilidades)'
speed: ' Velocidade: '
accuracy_label: ' Precisão: '
time_label: ' Tempo: '
errors_label: ' Erros: '
correct_detail: ' (%{correct}/%{total} corretos)'
input_blocked: ' Entrada temporariamente bloqueada '
input_blocked_ms: '(%{ms}ms restantes)'
hint_continue: '[c/Enter/Space] Continuar'
hint_retry: '[r] Tentar novamente'
hint_menu: '[q] Menu'
hint_stats: '[s] Estatísticas'
hint_delete: '[x] Excluir'
# Barra lateral de estatísticas (durante o exercício)
sidebar:
title: ' Estatísticas '
wpm: 'WPM: '
target: 'Meta: '
target_wpm: '%{wpm} WPM'
accuracy: 'Precisão: '
progress: 'Progresso: '
correct: 'Correto: '
errors: 'Erros: '
time: 'Tempo: '
last_drill: ' Último Exercício '
vs_avg: ' vs méd: '
# Painel de estatísticas
stats:
title: ' Estatísticas '
empty: 'Nenhum exercício completado ainda. Comece a digitar!'
tab_dashboard: '[1] Painel'
tab_history: '[2] Histórico'
tab_activity: '[3] Atividade'
tab_accuracy: '[4] Precisão'
tab_timing: '[5] Cronometragem'
tab_ngrams: '[6] N-gramas'
hint_back: '[ESC] Voltar'
hint_next_tab: '[Tab] Próxima aba'
hint_switch_tab: '[1-6] Trocar aba'
hint_navigate: '[j/k] Navegar'
hint_page: '[PgUp/PgDn] Página'
hint_delete: '[x] Excluir'
summary_title: ' Resumo '
drills: ' Exercícios: '
avg_wpm: ' WPM Méd: '
best_wpm: ' Melhor WPM: '
accuracy_label: ' Precisão: '
total_time: ' Tempo total: '
wpm_chart_title: ' WPM por Exercício (Últimos 20, Meta: %{target}) '
accuracy_chart_title: ' Precisão %% (Últimos 50 Exercícios) '
chart_drill: 'Exercício #'
chart_accuracy_pct: 'Precisão %%'
sessions_title: ' Sessões Recentes '
session_header: ' # WPM Raw Pre%% Tempo Data/Hora Modo Class. Parcial'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Excluir sessão #%{idx}? (y/n)'
confirm_title: ' Confirmar '
yes: 'sim'
no: 'não'
keyboard_accuracy_title: ' Precisão do Teclado %% '
keyboard_timing_title: ' Cronometragem do Teclado (ms) '
slowest_keys_title: ' Teclas mais Lentas (ms) '
fastest_keys_title: ' Teclas mais Rápidas (ms) '
worst_accuracy_title: ' Pior Precisão (%%) '
best_accuracy_title: ' Melhor Precisão (%%) '
not_enough_data: ' Dados insuficientes'
streaks_title: ' Sequências '
current_streak: ' Atual: '
best_streak: ' Melhor: '
active_days: ' Dias ativos: '
top_days_none: ' Melhores dias: nenhum'
top_days: ' Melhores dias: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Pre: %{pct}%%'
keys_label: ' Teclas: %{unlocked}/%{total} (%{mastered} dominadas)'
ngram_empty: 'Complete exercícios adaptativos para ver dados de n-gramas'
ngram_header_speed_narrow: ' Bgrm Vel Esper Anom%'
ngram_header_error_narrow: ' Bgrm Err Ams Taxa Esp Anom%'
ngram_header_speed: ' Bigrama Vel Esper Amostras Anom%'
ngram_header_error: ' Bigrama Erros Amostras Taxa Esper Anom%'
focus_title: ' Foco Ativo '
focus_char_label: ' Foco: '
focus_bigram_value: 'Bigrama %{label}'
focus_plus: ' + '
anomaly_error: 'erro'
anomaly_speed: 'velocidade'
focus_detail_both: ' Caractere ''%{ch}'': tecla mais fraca | Bigrama %{label}: anomalia de %{type} %{pct}%%'
focus_detail_char_only: ' Caractere ''%{ch}'': tecla mais fraca, sem anomalias de bigrama confirmadas'
focus_detail_bigram_only: ' (anomalia de %{type}: %{pct}%%)'
focus_empty: ' Complete exercícios adaptativos para ver dados de foco'
error_anomalies_title: ' Anomalias de Erro (%{count}) '
no_error_anomalies: ' Nenhuma anomalia de erro detectada'
speed_anomalies_title: ' Anomalias de Velocidade (%{count}) '
no_speed_anomalies: ' Nenhuma anomalia de velocidade detectada'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Caractere ''%{ch}'''
# Mapa de atividade
heatmap:
title: ' Atividade Diária (Sessões por Dia) '
jan: 'Jan'
feb: 'Fev'
mar: 'Mar'
apr: 'Abr'
may: 'Mai'
jun: 'Jun'
jul: 'Jul'
aug: 'Ago'
sep: 'Set'
oct: 'Out'
nov: 'Nov'
dec: 'Dez'
# Gráfico
chart:
wpm_over_time: ' WPM ao Longo do Tempo '
drill_number: 'Exercício #'
# Configurações
settings:
title: ' Configurações '
subtitle: 'Use as setas para navegar, Enter/Direita para alterar, ESC para salvar e sair'
target_wpm: 'WPM Meta'
theme: 'Tema'
word_count: 'Quantidade de Palavras'
ui_language: 'Idioma da Interface'
dictionary_language: 'Idioma do Dicionário'
keyboard_layout: 'Layout do Teclado'
code_language: 'Linguagem de Código'
code_downloads: 'Downloads de Código'
on: 'Sim'
off: 'Não'
code_download_dir: 'Dir. Download de Código'
snippets_per_repo: 'Trechos por Repo'
unlimited: 'Ilimitado'
download_code_now: 'Baixar Código Agora'
run_downloader: 'Executar downloader'
passage_downloads: 'Downloads de Passagens'
passage_download_dir: 'Dir. Download de Passagens'
paragraphs_per_book: 'Parágrafos por Livro'
whole_book: 'Livro inteiro'
download_passages_now: 'Baixar Passagens Agora'
export_path: 'Caminho de Exportação'
export_data: 'Exportar Dados'
export_now: 'Exportar agora'
import_path: 'Caminho de Importação'
import_data: 'Importar Dados'
import_now: 'Importar agora'
hint_save_back: '[ESC] Salvar e voltar'
hint_change_value: '[Enter/setas] Alterar valor'
hint_edit_path: '[Enter no caminho] Editar'
hint_move: '[←→] Mover'
hint_tab_complete: '[Tab] Completar (no final)'
hint_confirm: '[Enter] Confirmar'
hint_cancel: '[Esc] Cancelar'
success_title: ' Sucesso '
error_title: ' Erro '
press_any_key: 'Pressione qualquer tecla'
file_exists_title: ' Arquivo Existente '
file_exists: 'Um arquivo já existe neste caminho.'
overwrite_rename: '[d] Sobrescrever [r] Renomear [Esc] Cancelar'
erase_warning: 'Isso apagará seus dados atuais.'
export_first: 'Exporte primeiro se quiser mantê-los.'
proceed_yn: 'Prosseguir? (y/n)'
confirm_import_title: ' Confirmar Importação '
# Telas de seleção
select:
dictionary_language_title: ' Selecionar Idioma do Dicionário '
keyboard_layout_title: ' Selecionar Layout do Teclado '
code_language_title: ' Selecionar Linguagem de Código '
passage_source_title: ' Selecionar Fonte de Passagens '
ui_language_title: ' Selecionar Idioma da Interface '
more_above: '... %{count} mais acima ...'
more_below: '... %{count} mais abaixo ...'
current: ' (atual)'
disabled: ' (desativado)'
enabled_default: ' (ativado, padrão: %{layout})'
enabled: ' (ativado)'
disabled_blocked: ' (desativado: bloqueado)'
built_in: ' (integrado)'
cached: ' (em cache)'
disabled_download: ' (desativado: download necessário)'
download_required: ' (download necessário)'
hint_navigate: '[Up/Down/PgUp/PgDn] Navegar'
hint_confirm: '[Enter] Confirmar'
hint_back: '[ESC] Voltar'
language_resets_layout: 'Selecionar um idioma redefine o layout para o padrão desse idioma.'
layout_no_language_change: 'Alterar layout não muda o idioma do dicionário.'
disabled_network_notice: 'Alguns idiomas estão desativados: ative os downloads em intro/configurações.'
disabled_sources_notice: 'Algumas fontes estão desativadas: ative os downloads em intro/configurações.'
passage_all: 'Todos (Integrados + todos os livros)'
passage_builtin: 'Apenas passagens integradas'
passage_book_prefix: 'Livro: %{title}'
# Progresso
progress:
overall_key_progress: 'Progresso Geral das Teclas'
unlocked_mastered: '%{unlocked}/%{total} desbloqueadas (%{mastered} dominadas)'
# Árvore de habilidades
skill_tree:
title: ' Árvore de Habilidades '
locked: 'Bloqueado'
unlocked: 'desbloqueado'
mastered: 'dominado'
in_progress: 'em progresso'
complete: 'completo'
locked_status: 'bloqueado'
locked_notice: 'Complete %{count} letras primárias para desbloquear ramos'
branches_separator: 'Ramos (disponíveis após %{count} letras primárias)'
unlocked_letters: 'Desbloqueadas %{unlocked}/%{total} letras'
level: 'Nível %{current}/%{total}'
level_zero: 'Nível 0/%{total}'
in_focus: ' em foco'
hint_navigate: '[↑↓/jk] Navegar'
hint_scroll: '[PgUp/PgDn ou Ctrl+U/Ctrl+D] Rolar'
hint_back: '[q] Voltar'
hint_unlock: '[Enter] Desbloquear'
hint_start_drill: '[Enter] Iniciar Exercício'
unlock_msg_1: 'Uma vez desbloqueado, o exercício adaptativo incluirá teclas deste ramo que estejam desbloqueadas.'
unlock_msg_2: 'Se quiser focar apenas neste ramo, inicie um exercício diretamente deste ramo na Árvore de Habilidades.'
confirm_unlock: 'Desbloquear %{branch}?'
confirm_yn: '[y] Desbloquear [n/ESC] Cancelar'
lvl_prefix: 'Nív'
branch_primary_letters: 'Letras Primárias'
branch_capital_letters: 'Letras Maiúsculas'
branch_numbers: 'Números 0-9'
branch_prose_punctuation: 'Pontuação de Prosa'
branch_whitespace: 'Espaços em Branco'
branch_code_symbols: 'Símbolos de Código'
level_frequency_order: 'Ordem de Frequência'
level_common_sentence_capitals: 'Maiúsculas de Frase Comuns'
level_name_capitals: 'Maiúsculas de Nomes'
level_remaining_capitals: 'Maiúsculas Restantes'
level_common_digits: 'Dígitos Comuns'
level_all_digits: 'Todos os Dígitos'
level_essential: 'Essencial'
level_common: 'Comum'
level_expressive: 'Expressivo'
level_enter_return: 'Enter/Retorno'
level_tab_indent: 'Tab/Recuo'
level_arithmetic_assignment: 'Aritmética e Atribuição'
level_grouping: 'Agrupamento'
level_logic_reference: 'Lógica e Referência'
level_special: 'Especial'
# Marcos
milestones:
unlock_title: ' Tecla Desbloqueada! '
mastery_title: ' Tecla Dominada! '
branches_title: ' Novos Ramos Disponíveis! '
branch_complete_title: ' Ramo Completo! '
all_unlocked_title: ' Todas as Teclas Desbloqueadas! '
all_mastered_title: ' Domínio Total do Teclado! '
unlocked: 'desbloqueada'
mastered: 'dominada'
use_finger: 'Use seu dedo %{finger}'
hold_right_shift: 'Segure Shift Direito (mindinho direito)'
hold_left_shift: 'Segure Shift Esquerdo (mindinho esquerdo)'
congratulations_all_letters: 'Parabéns! Você dominou todas as %{count} letras primárias'
new_branches_available: 'Novos ramos de habilidades estão disponíveis:'
visit_skill_tree: 'Visite a Árvore de Habilidades para desbloquear um novo ramo'
and_start_training: 'e comece a treinar!'
open_skill_tree: 'Pressione [t] para abrir a Árvore de Habilidades'
branch_complete_msg: 'Você completou o ramo %{branch}!'
all_levels_mastered: 'Todos os %{count} níveis dominados.'
all_keys_confident: 'Cada tecla neste ramo está em confiança máxima.'
all_unlocked_msg: 'Você desbloqueou todas as teclas do teclado!'
all_unlocked_desc: 'Cada caractere, símbolo e modificador está disponível nos seus exercícios.'
keep_practicing_mastery: 'Continue praticando para alcançar o domínio — quando cada tecla atingir'
confidence_complete: 'confiança máxima, você terá alcançado o domínio total do teclado!'
all_mastered_msg: 'Parabéns — você alcançou o domínio total do teclado!'
all_mastered_desc: 'Cada tecla do teclado está em confiança máxima.'
mastery_takes_practice: 'O domínio não é um destino — requer prática contínua.'
keep_drilling: 'Continue praticando para manter seu nível.'
hint_skill_tree_continue: '[t] Abrir Árvore de Habilidades [Outra tecla] Continuar'
hint_any_key: 'Pressione qualquer tecla para continuar'
input_blocked: 'Entrada temporariamente bloqueada (%{ms}ms restantes)'
unlock_msg_1: 'Bom trabalho! Continue melhorando suas habilidades.'
unlock_msg_2: 'Mais uma tecla adicionada ao seu arsenal!'
unlock_msg_3: 'Seu teclado está crescendo! Continue assim.'
unlock_msg_4: 'Um passo mais perto do domínio total!'
mastery_msg_1: 'Esta tecla está em confiança máxima!'
mastery_msg_2: 'Você domina esta tecla perfeitamente!'
mastery_msg_3: 'Memória muscular adquirida!'
mastery_msg_4: 'Mais uma tecla conquistada!'
# Explorador de teclado
keyboard:
title: ' Teclado '
subtitle: 'Pressione ou clique em uma tecla'
hint_navigate: '[←→↑↓/hjkl/Tab] Navegar'
hint_back: '[q/ESC] Voltar'
key_label: 'Tecla: '
finger_label: 'Dedo: '
hand_left: 'Esquerda'
hand_right: 'Direita'
finger_index: 'Indicador'
finger_middle: 'Médio'
finger_ring: 'Anelar'
finger_pinky: 'Mindinho'
finger_thumb: 'Polegar'
overall_accuracy: ' Precisão geral: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Precisão classificada: %{correct}/%{total} (%{pct}%%)'
confidence: 'Confiança: '
no_data: 'Sem dados ainda'
no_data_short: 'Sem dados'
key_details: ' Detalhes da Tecla '
key_details_char: ' Detalhes da Tecla: ''%{ch}'' '
key_details_name: ' Detalhes da Tecla: %{name} '
press_key_hint: 'Pressione uma tecla para ver seus detalhes'
shift_label: 'Shift: '
shift_no: 'Não'
overall_avg_time: 'Tempo Méd. Geral: '
overall_best_time: 'Melhor Tempo Geral: '
overall_samples: 'Amostras Gerais: '
overall_accuracy_label: 'Precisão Geral: '
branch_label: 'Ramo: '
level_label: 'Nível: '
built_in_key: 'Tecla Integrada'
unlocked_label: 'Desbloqueada: '
yes: 'Sim'
no: 'Não'
in_focus_label: 'Em Foco?: '
mastery_label: 'Domínio: '
mastery_locked: 'Bloqueado'
ranked_avg_time: 'Tempo Méd. Classificado: '
ranked_best_time: 'Melhor Tempo Classificado: '
ranked_samples: 'Amostras Classificadas: '
ranked_accuracy_label: 'Precisão Classificada: '
# Diálogos de introdução
intro:
passage_title: ' Configurar Download de Passagens '
code_title: ' Configurar Download de Código '
enable_downloads: 'Ativar downloads de rede'
download_dir: 'Diretório de download'
paragraphs_per_book: 'Parágrafos por livro (0 = inteiro)'
whole_book: 'livro inteiro'
snippets_per_repo: 'Trechos por repo (0 = ilimitado)'
unlimited: 'ilimitado'
start_passage_drill: 'Iniciar exercício de passagem'
start_code_drill: 'Iniciar exercício de código'
confirm: 'Confirmar'
hint_navigate: '[Up/Down] Navegar'
hint_adjust: '[Left/Right] Ajustar'
hint_edit: '[Type/Backspace] Editar'
hint_confirm: '[Enter] Confirmar'
hint_cancel: '[ESC] Cancelar'
preparing_download: 'Preparando download...'
download_passage_title: ' Baixando Fonte de Passagem '
download_code_title: ' Baixando Fonte de Código '
book_label: ' Livro: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bytes'
downloaded_bytes: 'Baixado: %{bytes} bytes'
downloading_book_progress: 'Baixando livro atual: [%{bar}] %{downloaded}/%{total} bytes'
downloading_book_bytes: 'Baixando livro atual: %{bytes} bytes'
downloading_code_progress: 'Baixando: [%{bar}] %{downloaded}/%{total} bytes'
downloading_code_bytes: 'Baixando: %{bytes} bytes'
current_book: 'Atual: %{name} (livro %{done}/%{total})'
current_repo: 'Atual: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr pode baixar passagens do Project Gutenberg para prática de digitação.'
passage_instructions_2: 'Os livros são baixados uma vez e armazenados localmente.'
passage_instructions_3: 'Configure os ajustes de download abaixo, depois inicie um exercício de passagem.'
code_instructions_1: 'keydr pode baixar código open-source do GitHub para prática de digitação.'
code_instructions_2: 'O código é baixado uma vez e armazenado localmente.'
code_instructions_3: 'Configure os ajustes de download abaixo, depois inicie um exercício de código.'
# Mensagens de status (de app.rs)
status:
recovery_files: 'Arquivos de recuperação encontrados de uma importação interrompida. Os dados podem estar inconsistentes — considere reimportar.'
dir_not_exist: 'O diretório não existe: %{path}'
no_data_store: 'Nenhum armazenamento de dados disponível'
serialization_error: 'Erro de serialização: %{error}'
exported_to: 'Exportado para %{path}'
export_failed: 'Exportação falhou: %{error}'
could_not_read: 'Não foi possível ler o arquivo: %{error}'
invalid_export: 'Arquivo de exportação inválido: %{error}'
unsupported_version: 'Versão de exportação não suportada: %{got} (esperada %{expected})'
import_failed: 'Importação falhou: %{error}'
imported_theme_fallback: 'Importado com sucesso (tema ''%{theme}'' não encontrado, usando padrão)'
imported_success: 'Importado com sucesso'
adaptive_unavailable: 'Modo adaptativo classificado não disponível: %{error}'
switched_to: 'Alterado para %{name}'
layout_changed: 'Layout alterado para %{name}'
# Erros (para tradução de limites de UI)
errors:
unknown_language: 'Idioma desconhecido: %{key}'
unknown_layout: 'Layout de teclado desconhecido: %{key}'
unsupported_pair: 'Par idioma/layout não suportado: %{language} + %{layout}'
language_blocked: 'Idioma bloqueado pelo nível de suporte: %{key}'
# Comum
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Voltar'

454
locales/ro.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Antrenor de tastare in terminal'
adaptive_drill: 'Exercitiu adaptiv'
adaptive_drill_desc: 'Cuvinte fonetice cu deblocare adaptiva a literelor'
code_drill: 'Exercitiu de cod'
code_drill_desc: 'Exerseaza tastarea sintaxei de cod'
passage_drill: 'Exercitiu de text'
passage_drill_desc: 'Tasteaza pasaje din carti'
skill_tree: 'Arbore de abilitati'
skill_tree_desc: 'Vizualizeaza ramuri de progres si lanseaza exercitii'
keyboard: 'Tastatura'
keyboard_desc: 'Exploreaza aranjamentul si statisticile tastelor'
statistics: 'Statistici'
statistics_desc: 'Vizualizeaza statisticile de tastare'
settings: 'Setari'
settings_desc: 'Configureaza keydr'
day_streak: ' | %{days} zile consecutive'
key_progress: ' Progres taste %{unlocked}/%{total} (%{mastered} stapanite) | Tinta %{target} WPM%{streak}'
hint_start: '[1-3] Start'
hint_skill_tree: '[t] Arbore'
hint_keyboard: '[b] Tastatura'
hint_stats: '[s] Statistici'
hint_settings: '[c] Setari'
hint_quit: '[q] Iesire'
# Drill screen
drill:
title: ' Exercitiu '
mode_adaptive: 'Adaptiv'
mode_code: 'Cod (fara clasament)'
mode_passage: 'Text (fara clasament)'
focus_char: 'Focus: ''%{ch}'''
focus_bigram: 'Focus: "%{bigram}"'
focus_both: 'Focus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Prec'
header_err: 'Erori'
code_source: ' Sursa cod '
passage_source: ' Sursa text '
footer: '[ESC] Termina exercitiu [Backspace] Sterge'
keys_reenabled: 'Taste reactivate in %{ms}ms'
hint_end: '[ESC] Termina exercitiu'
hint_backspace: '[Backspace] Sterge'
# Dashboard / drill result
dashboard:
title: ' Exercitiu finalizat '
results: 'Rezultate'
unranked_note_prefix: ' (Fara clasament'
unranked_note_suffix: ' nu conteaza in arborele de abilitati)'
speed: ' Viteza: '
accuracy_label: ' Precizie: '
time_label: ' Timp: '
errors_label: ' Erori: '
correct_detail: ' (%{correct}/%{total} corecte)'
input_blocked: ' Intrare blocata temporar '
input_blocked_ms: '(%{ms}ms ramas)'
hint_continue: '[c/Enter/Space] Continua'
hint_retry: '[r] Reia'
hint_menu: '[q] Meniu'
hint_stats: '[s] Statistici'
hint_delete: '[x] Sterge'
# Stats sidebar (during drill)
sidebar:
title: ' Statistici '
wpm: 'WPM: '
target: 'Tinta: '
target_wpm: '%{wpm} WPM'
accuracy: 'Precizie: '
progress: 'Progres: '
correct: 'Corecte: '
errors: 'Erori: '
time: 'Timp: '
last_drill: ' Ultimul exercitiu '
vs_avg: ' vs med: '
# Statistics dashboard
stats:
title: ' Statistici '
empty: 'Niciun exercitiu finalizat. Incepe sa tastezi!'
tab_dashboard: '[1] Tablou'
tab_history: '[2] Istoric'
tab_activity: '[3] Activitate'
tab_accuracy: '[4] Precizie'
tab_timing: '[5] Cronometraj'
tab_ngrams: '[6] N-grame'
hint_back: '[ESC] Inapoi'
hint_next_tab: '[Tab] Tab urmator'
hint_switch_tab: '[1-6] Schimba tab'
hint_navigate: '[j/k] Navigheaza'
hint_page: '[PgUp/PgDn] Pagina'
hint_delete: '[x] Sterge'
summary_title: ' Sumar '
drills: ' Exercitii: '
avg_wpm: ' Med WPM: '
best_wpm: ' Cel mai bun WPM: '
accuracy_label: ' Precizie: '
total_time: ' Timp total: '
wpm_chart_title: ' WPM pe exercitiu (ultimele 20, tinta: %{target}) '
accuracy_chart_title: ' Precizie %% (ultimele 50 exercitii) '
chart_drill: 'Exerc #'
chart_accuracy_pct: 'Precizie %%'
sessions_title: ' Sesiuni recente '
session_header: ' # WPM Raw Prec%% Timp Data/Ora Mod Clasat Partial'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Stergi sesiunea #%{idx}? (d/n)'
confirm_title: ' Confirma '
yes: 'da'
no: 'nu'
keyboard_accuracy_title: ' Precizia tastaturii %% '
keyboard_timing_title: ' Cronometraj tastatura (ms) '
slowest_keys_title: ' Cele mai lente taste (ms) '
fastest_keys_title: ' Cele mai rapide taste (ms) '
worst_accuracy_title: ' Cea mai slaba precizie (%%) '
best_accuracy_title: ' Cea mai buna precizie (%%) '
not_enough_data: ' Date insuficiente'
streaks_title: ' Serii '
current_streak: ' Curenta: '
best_streak: ' Cea mai buna: '
active_days: ' Zile active: '
top_days_none: ' Cele mai bune zile: niciuna'
top_days: ' Cele mai bune zile: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Prec: %{pct}%%'
keys_label: ' Taste: %{unlocked}/%{total} (%{mastered} stapanite)'
ngram_empty: 'Finalizeaza exercitii adaptive pentru a vedea datele n-gramelor'
ngram_header_speed_narrow: ' Bgrm Vit Astpt Anom%'
ngram_header_error_narrow: ' Bgrm Erori Esnt Rata Astpt Anom%'
ngram_header_speed: ' Bigrama Viteza Asteptat Esant Anom%'
ngram_header_error: ' Bigrama Erori Esant Rata Asteptat Anom%'
focus_title: ' Focus activ '
focus_char_label: ' Focus: '
focus_bigram_value: 'Bigrama %{label}'
focus_plus: ' + '
anomaly_error: 'eroare'
anomaly_speed: 'viteza'
focus_detail_both: ' Caracter ''%{ch}'': cea mai slaba tasta | Bigrama %{label}: anomalie %{type} %{pct}%%'
focus_detail_char_only: ' Caracter ''%{ch}'': cea mai slaba tasta, fara anomalii confirmate'
focus_detail_bigram_only: ' (anomalie %{type}: %{pct}%%)'
focus_empty: ' Finalizeaza exercitii adaptive pentru a vedea datele de focus'
error_anomalies_title: ' Anomalii erori (%{count}) '
no_error_anomalies: ' Nicio anomalie de erori detectata'
speed_anomalies_title: ' Anomalii viteza (%{count}) '
no_speed_anomalies: ' Nicio anomalie de viteza detectata'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Ezit: >%{ms}ms'
focus_char_value: 'Caracter ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Activitate zilnica (sesiuni pe zi) '
jan: 'Ian'
feb: 'Feb'
mar: 'Mar'
apr: 'Apr'
may: 'Mai'
jun: 'Iun'
jul: 'Iul'
aug: 'Aug'
sep: 'Sep'
oct: 'Oct'
nov: 'Noi'
dec: 'Dec'
# Chart
chart:
wpm_over_time: ' WPM in timp '
drill_number: 'Exerc #'
# Settings
settings:
title: ' Setari '
subtitle: 'Foloseste sagetile pentru navigare, Enter/dreapta schimba, ESC salveaza si iese'
target_wpm: 'WPM tinta'
theme: 'Tema'
word_count: 'Numar cuvinte'
ui_language: 'Limba interfetei'
dictionary_language: 'Limba dictionarului'
keyboard_layout: 'Aranjament tastatura'
code_language: 'Limbaj programare'
code_downloads: 'Descarcari cod'
on: 'Pornit'
off: 'Oprit'
code_download_dir: 'Director descarcare cod'
snippets_per_repo: 'Fragmente pe repo'
unlimited: 'Nelimitat'
download_code_now: 'Descarca cod acum'
run_downloader: 'Lanseaza descarcarea'
passage_downloads: 'Descarcari texte'
passage_download_dir: 'Director descarcare texte'
paragraphs_per_book: 'Paragrafe pe carte'
whole_book: 'Cartea intreaga'
download_passages_now: 'Descarca texte acum'
export_path: 'Cale export'
export_data: 'Exporta date'
export_now: 'Exporta acum'
import_path: 'Cale import'
import_data: 'Importa date'
import_now: 'Importa acum'
hint_save_back: '[ESC] Salveaza si inapoi'
hint_change_value: '[Enter/sageti] Schimba valoarea'
hint_edit_path: '[Enter pe cale] Editeaza'
hint_move: '[←→] Muta'
hint_tab_complete: '[Tab] Completeaza (la sfarsit)'
hint_confirm: '[Enter] Confirma'
hint_cancel: '[Esc] Anuleaza'
success_title: ' Succes '
error_title: ' Eroare '
press_any_key: 'Apasa orice tasta'
file_exists_title: ' Fisierul exista '
file_exists: 'Un fisier exista deja la aceasta cale.'
overwrite_rename: '[d] Suprascrie [r] Redenumeste [Esc] Anuleaza'
erase_warning: 'Aceasta va sterge datele curente.'
export_first: 'Exporta mai intai daca vrei sa le pastrezi.'
proceed_yn: 'Continui? (d/n)'
confirm_import_title: ' Confirma importul '
# Selection screens
select:
dictionary_language_title: ' Selecteaza limba dictionarului '
keyboard_layout_title: ' Selecteaza aranjamentul tastaturii '
code_language_title: ' Selecteaza limbajul de programare '
passage_source_title: ' Selecteaza sursa textului '
ui_language_title: ' Selecteaza limba interfetei '
more_above: '... %{count} mai sus ...'
more_below: '... %{count} mai jos ...'
current: ' (curent)'
disabled: ' (dezactivat)'
enabled_default: ' (activat, implicit: %{layout})'
enabled: ' (activat)'
disabled_blocked: ' (dezactivat: blocat)'
built_in: ' (incorporat)'
cached: ' (in cache)'
disabled_download: ' (dezactivat: descarcare necesara)'
download_required: ' (descarcare necesara)'
hint_navigate: '[Sus/Jos/PgUp/PgDn] Navigheaza'
hint_confirm: '[Enter] Confirma'
hint_back: '[ESC] Inapoi'
language_resets_layout: 'Selectarea unei limbi reseteaza aranjamentul la implicit pentru acea limba.'
layout_no_language_change: 'Schimbarea aranjamentului nu schimba limba dictionarului.'
disabled_network_notice: 'Unele limbi sunt dezactivate: activeaza descarcarile de retea in setari.'
disabled_sources_notice: 'Unele surse sunt dezactivate: activeaza descarcarile de retea in setari.'
passage_all: 'Toate (incorporate + toate cartile)'
passage_builtin: 'Doar texte incorporate'
passage_book_prefix: 'Carte: %{title}'
# Progress
progress:
overall_key_progress: 'Progres general taste'
unlocked_mastered: '%{unlocked}/%{total} deblocate (%{mastered} stapanite)'
# Skill tree
skill_tree:
title: ' Arbore de abilitati '
locked: 'Blocat'
unlocked: 'deblocat'
mastered: 'stapanit'
in_progress: 'in curs'
complete: 'finalizat'
locked_status: 'blocat'
locked_notice: 'Finalizeaza %{count} litere primare pentru a debloca ramuri'
branches_separator: 'Ramuri (disponibile dupa %{count} litere primare)'
unlocked_letters: 'Deblocate %{unlocked}/%{total} litere'
level: 'Nivel %{current}/%{total}'
level_zero: 'Nivel 0/%{total}'
in_focus: ' in focus'
hint_navigate: '[↑↓/jk] Navigheaza'
hint_scroll: '[PgUp/PgDn sau Ctrl+U/Ctrl+D] Deruleaza'
hint_back: '[q] Inapoi'
hint_unlock: '[Enter] Deblocheaza'
hint_start_drill: '[Enter] Incepe exercitiu'
unlock_msg_1: 'Dupa deblocare, exercitiul adaptiv implicit va include taste din aceasta ramura.'
unlock_msg_2: 'Daca vrei sa te concentrezi doar pe aceasta ramura, lanseaza un exercitiu direct din arbore.'
confirm_unlock: 'Deblocheaza %{branch}?'
confirm_yn: '[y] Deblocheaza [n/ESC] Anuleaza'
lvl_prefix: 'Niv'
branch_primary_letters: 'Litere primare'
branch_capital_letters: 'Litere mari'
branch_numbers: 'Cifre 0-9'
branch_prose_punctuation: 'Punctuatie'
branch_whitespace: 'Spatii albe'
branch_code_symbols: 'Simboluri cod'
level_frequency_order: 'Ordinea frecventei'
level_common_sentence_capitals: 'Majuscule comune in propozitii'
level_name_capitals: 'Majuscule de nume'
level_remaining_capitals: 'Majuscule ramase'
level_common_digits: 'Cifre comune'
level_all_digits: 'Toate cifrele'
level_essential: 'Esentiale'
level_common: 'Comune'
level_expressive: 'Expresive'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Indentare'
level_arithmetic_assignment: 'Aritmetica si atribuire'
level_grouping: 'Grupare'
level_logic_reference: 'Logica si referinta'
level_special: 'Speciale'
# Milestones
milestones:
unlock_title: ' Tasta deblocata! '
mastery_title: ' Tasta stapanita! '
branches_title: ' Ramuri noi disponibile! '
branch_complete_title: ' Ramura finalizata! '
all_unlocked_title: ' Toate tastele deblocate! '
all_mastered_title: ' Stapanire completa a tastaturii! '
unlocked: 'deblocata'
mastered: 'stapanita'
use_finger: 'Foloseste %{finger}'
hold_right_shift: 'Tine Shift dreapta (degetul mic drept)'
hold_left_shift: 'Tine Shift stanga (degetul mic stang)'
congratulations_all_letters: 'Felicitari! Ai stapanit toate cele %{count} litere primare'
new_branches_available: 'Ramuri noi de abilitati sunt acum disponibile:'
visit_skill_tree: 'Viziteaza arborele de abilitati pentru a debloca o ramura noua'
and_start_training: 'si incepe antrenamentul!'
open_skill_tree: 'Apasa [t] pentru a deschide arborele acum'
branch_complete_msg: 'Ai finalizat ramura %{branch}!'
all_levels_mastered: 'Toate cele %{count} niveluri stapanite.'
all_keys_confident: 'Fiecare tasta din aceasta ramura este la incredere maxima.'
all_unlocked_msg: 'Ai deblocat fiecare tasta de pe tastatura!'
all_unlocked_desc: 'Fiecare caracter, simbol si modificator este acum disponibil in exercitiile tale.'
keep_practicing_mastery: 'Continua sa exersezi pentru a construi stapanirea — cand fiecare tasta atinge'
confidence_complete: 'incredere maxima, vei fi atins stapanirea completa a tastaturii!'
all_mastered_msg: 'Felicitari — ai atins stapanirea completa a tastaturii!'
all_mastered_desc: 'Fiecare tasta de pe tastatura este la incredere maxima.'
mastery_takes_practice: 'Stapanirea nu este o destinatie — necesita practica continua.'
keep_drilling: 'Continua sa exersezi pentru a-ti mentine nivelul.'
hint_skill_tree_continue: '[t] Deschide arborele [Alta tasta] Continua'
hint_any_key: 'Apasa orice tasta pentru a continua'
input_blocked: 'Intrare blocata temporar (%{ms}ms ramas)'
unlock_msg_1: 'Bine lucrat! Continua sa-ti dezvolti abilitatile.'
unlock_msg_2: 'Inca o tasta in arsenalul tau!'
unlock_msg_3: 'Tastatura ta creste! Continua tot asa.'
unlock_msg_4: 'Un pas mai aproape de stapanirea completa!'
mastery_msg_1: 'Aceasta tasta este acum la incredere maxima!'
mastery_msg_2: 'Ai aceasta tasta la degetul mic!'
mastery_msg_3: 'Memorie musculara fixata!'
mastery_msg_4: 'Inca o tasta cucerita!'
# Keyboard explorer
keyboard:
title: ' Tastatura '
subtitle: 'Apasa orice tasta sau click pe o tasta'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigheaza'
hint_back: '[q/ESC] Inapoi'
key_label: 'Tasta: '
finger_label: 'Deget: '
hand_left: 'Stang'
hand_right: 'Drept'
finger_index: 'Aratator'
finger_middle: 'Mijlociu'
finger_ring: 'Inelar'
finger_pinky: 'Mic'
finger_thumb: 'Degetul mare'
overall_accuracy: ' Precizie generala: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Precizie clasament: %{correct}/%{total} (%{pct}%%)'
confidence: 'Incredere: '
no_data: 'Fara date inca'
no_data_short: 'Fara date'
key_details: ' Detalii tasta '
key_details_char: ' Detalii tasta: ''%{ch}'' '
key_details_name: ' Detalii tasta: %{name} '
press_key_hint: 'Apasa o tasta pentru a vedea detaliile'
shift_label: 'Shift: '
shift_no: 'Nu'
overall_avg_time: 'Timp mediu: '
overall_best_time: 'Cel mai bun timp: '
overall_samples: 'Esantioane: '
overall_accuracy_label: 'Precizie generala: '
branch_label: 'Ramura: '
level_label: 'Nivel: '
built_in_key: 'Tasta incorporata'
unlocked_label: 'Deblocata: '
yes: 'Da'
no: 'Nu'
in_focus_label: 'In focus?: '
mastery_label: 'Stapanire: '
mastery_locked: 'Blocata'
ranked_avg_time: 'Timp mediu clasat: '
ranked_best_time: 'Cel mai bun timp clasat: '
ranked_samples: 'Esantioane clasate: '
ranked_accuracy_label: 'Precizie clasament: '
# Intro dialogs
intro:
passage_title: ' Configurare descarcari texte '
code_title: ' Configurare descarcari cod '
enable_downloads: 'Activeaza descarcarile de retea'
download_dir: 'Director descarcare'
paragraphs_per_book: 'Paragrafe pe carte (0 = toata)'
whole_book: 'cartea intreaga'
snippets_per_repo: 'Fragmente pe repo (0 = nelimitat)'
unlimited: 'nelimitat'
start_passage_drill: 'Incepe exercitiu de text'
start_code_drill: 'Incepe exercitiu de cod'
confirm: 'Confirma'
hint_navigate: '[Sus/Jos] Navigheaza'
hint_adjust: '[Stanga/Dreapta] Ajusteaza'
hint_edit: '[Tasteaza/Backspace] Editeaza'
hint_confirm: '[Enter] Confirma'
hint_cancel: '[ESC] Anuleaza'
preparing_download: 'Se pregateste descarcarea...'
download_passage_title: ' Se descarca sursa textului '
download_code_title: ' Se descarca sursa codului '
book_label: ' Carte: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} octeti'
downloaded_bytes: 'Descarcat: %{bytes} octeti'
downloading_book_progress: 'Se descarca cartea: [%{bar}] %{downloaded}/%{total} octeti'
downloading_book_bytes: 'Se descarca cartea: %{bytes} octeti'
downloading_code_progress: 'Se descarca: [%{bar}] %{downloaded}/%{total} octeti'
downloading_code_bytes: 'Se descarca: %{bytes} octeti'
current_book: 'Curent: %{name} (carte %{done}/%{total})'
current_repo: 'Curent: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr poate descarca texte din Project Gutenberg pentru exercitii de tastare.'
passage_instructions_2: 'Cartile se descarca o singura data si se stocheaza local.'
passage_instructions_3: 'Configureaza setarile de descarcare mai jos, apoi incepe un exercitiu.'
code_instructions_1: 'keydr poate descarca cod open-source din GitHub pentru exercitii de tastare.'
code_instructions_2: 'Codul se descarca o singura data si se stocheaza local.'
code_instructions_3: 'Configureaza setarile de descarcare mai jos, apoi incepe un exercitiu.'
# Status messages (from app.rs)
status:
recovery_files: 'Fisiere de recuperare gasite dintr-un import intrerupt. Datele pot fi inconsistente — ia in considerare reimportul.'
dir_not_exist: 'Directorul nu exista: %{path}'
no_data_store: 'Depozit de date indisponibil'
serialization_error: 'Eroare de serializare: %{error}'
exported_to: 'Exportat in %{path}'
export_failed: 'Exportul a esuat: %{error}'
could_not_read: 'Nu s-a putut citi fisierul: %{error}'
invalid_export: 'Fisier de export invalid: %{error}'
unsupported_version: 'Versiune export nesuportata: %{got} (asteptata %{expected})'
import_failed: 'Importul a esuat: %{error}'
imported_theme_fallback: 'Importat cu succes (tema ''%{theme}'' negasita, se foloseste implicita)'
imported_success: 'Importat cu succes'
adaptive_unavailable: 'Mod adaptiv clasat indisponibil: %{error}'
switched_to: 'Comutat la %{name}'
layout_changed: 'Aranjament schimbat la %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Limba necunoscuta: %{key}'
unknown_layout: 'Aranjament tastatura necunoscut: %{key}'
unsupported_pair: 'Pereche limba/aranjament nesuportata: %{language} + %{layout}'
language_blocked: 'Limba blocata de nivelul de suport: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Inapoi'

454
locales/sl.yml Normal file
View File

@@ -0,0 +1,454 @@
# Glavni meni
menu:
subtitle: 'Učitelj tipkanja v terminalu'
adaptive_drill: 'Prilagodljiva vaja'
adaptive_drill_desc: 'Fonetične besede s prilagodljivim odklepanjem črk'
code_drill: 'Vaja kode'
code_drill_desc: 'Vadite tipkanje sintakse kode'
passage_drill: 'Vaja odlomkov'
passage_drill_desc: 'Tipkajte odlomke iz knjig'
skill_tree: 'Drevo veščin'
skill_tree_desc: 'Oglejte si veje napredovanja in zaženite vaje'
keyboard: 'Tipkovnica'
keyboard_desc: 'Raziščite razporeditev tipk in statistiko'
statistics: 'Statistika'
statistics_desc: 'Oglejte si statistiko tipkanja'
settings: 'Nastavitve'
settings_desc: 'Nastavite keydr'
day_streak: ' | %{days} dni zapored'
key_progress: ' Napredek tipk %{unlocked}/%{total} (%{mastered} osvojenih) | Cilj %{target} WPM%{streak}'
hint_start: '[1-3] Začni'
hint_skill_tree: '[t] Drevo veščin'
hint_keyboard: '[b] Tipkovnica'
hint_stats: '[s] Statistika'
hint_settings: '[c] Nastavitve'
hint_quit: '[q] Izhod'
# Zaslon vaje
drill:
title: ' Vaja '
mode_adaptive: 'Prilagodljiva'
mode_code: 'Koda (neocenjeno)'
mode_passage: 'Odlomek (neocenjeno)'
focus_char: 'Fokus: ''%{ch}'''
focus_bigram: 'Fokus: "%{bigram}"'
focus_both: 'Fokus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Toč'
header_err: 'Nap'
code_source: ' Vir kode '
passage_source: ' Vir odlomka '
footer: '[ESC] Končaj vajo [Backspace] Izbriši'
keys_reenabled: 'Tipke ponovno omogočene čez %{ms}ms'
hint_end: '[ESC] Končaj vajo'
hint_backspace: '[Backspace] Izbriši'
# Nadzorna plošča / rezultat vaje
dashboard:
title: ' Vaja končana '
results: 'Rezultati'
unranked_note_prefix: ' (Neocenjeno'
unranked_note_suffix: ' ne šteje za drevo veščin)'
speed: ' Hitrost: '
accuracy_label: ' Točnost: '
time_label: ' Čas: '
errors_label: ' Napake: '
correct_detail: ' (%{correct}/%{total} pravilnih)'
input_blocked: ' Vnos začasno blokiran '
input_blocked_ms: '(še %{ms}ms)'
hint_continue: '[c/Enter/Space] Nadaljuj'
hint_retry: '[r] Ponovi'
hint_menu: '[q] Meni'
hint_stats: '[s] Statistika'
hint_delete: '[x] Izbriši'
# Stranska vrstica statistike (med vajo)
sidebar:
title: ' Statistika '
wpm: 'WPM: '
target: 'Cilj: '
target_wpm: '%{wpm} WPM'
accuracy: 'Točnost: '
progress: 'Napredek: '
correct: 'Pravilno: '
errors: 'Napake: '
time: 'Čas: '
last_drill: ' Zadnja vaja '
vs_avg: ' vs povpr: '
# Statistična nadzorna plošča
stats:
title: ' Statistika '
empty: 'Še ni opravljenih vaj. Začnite tipkati!'
tab_dashboard: '[1] Pregled'
tab_history: '[2] Zgodovina'
tab_activity: '[3] Aktivnost'
tab_accuracy: '[4] Točnost'
tab_timing: '[5] Časi'
tab_ngrams: '[6] N-grami'
hint_back: '[ESC] Nazaj'
hint_next_tab: '[Tab] Naslednji zavihek'
hint_switch_tab: '[1-6] Preklopi zavihek'
hint_navigate: '[j/k] Navigacija'
hint_page: '[PgUp/PgDn] Stran'
hint_delete: '[x] Izbriši'
summary_title: ' Povzetek '
drills: ' Vaje: '
avg_wpm: ' Povpr WPM: '
best_wpm: ' Najb WPM: '
accuracy_label: ' Točnost: '
total_time: ' Skupni čas: '
wpm_chart_title: ' WPM na vajo (zadnjih 20, cilj: %{target}) '
accuracy_chart_title: ' Točnost %% (zadnjih 50 vaj) '
chart_drill: 'Vaja #'
chart_accuracy_pct: 'Točnost %%'
sessions_title: ' Nedavne seje '
session_header: ' # WPM Sur Toč%% Čas Datum/Čas Način Ocenj Delno'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Izbriši sejo #%{idx}? (y/n)'
confirm_title: ' Potrdi '
yes: 'da'
no: 'ne'
keyboard_accuracy_title: ' Točnost tipkovnice %% '
keyboard_timing_title: ' Časi tipkovnice (ms) '
slowest_keys_title: ' Najpočasnejše tipke (ms) '
fastest_keys_title: ' Najhitrejše tipke (ms) '
worst_accuracy_title: ' Najslabša točnost (%%) '
best_accuracy_title: ' Najboljša točnost (%%) '
not_enough_data: ' Premalo podatkov'
streaks_title: ' Nizi '
current_streak: ' Trenutni: '
best_streak: ' Najboljši: '
active_days: ' Aktivni dnevi: '
top_days_none: ' Najboljši dnevi: brez'
top_days: ' Najboljši dnevi: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Toč: %{pct}%%'
keys_label: ' Tipke: %{unlocked}/%{total} (%{mastered} osvojenih)'
ngram_empty: 'Opravite nekaj prilagodljivih vaj za prikaz n-gramov'
ngram_header_speed_narrow: ' Bgrm Hitr Prič Anom%'
ngram_header_error_narrow: ' Bgrm Nap Vzr Stpn Prič Anom%'
ngram_header_speed: ' Bigram Hitrost Prič Vzorci Anom%'
ngram_header_error: ' Bigram Napake Vzorci Stpn Prič Anom%'
focus_title: ' Aktivni fokus '
focus_char_label: ' Fokus: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'napaka'
anomaly_speed: 'hitrost'
focus_detail_both: ' Znak ''%{ch}'': najšibkejša tipka | Bigram %{label}: anomalija %{type} %{pct}%%'
focus_detail_char_only: ' Znak ''%{ch}'': najšibkejša tipka, brez potrjenih anomalij bigramov'
focus_detail_bigram_only: ' (anomalija %{type}: %{pct}%%)'
focus_empty: ' Opravite nekaj prilagodljivih vaj za prikaz podatkov fokusa'
error_anomalies_title: ' Anomalije napak (%{count}) '
no_error_anomalies: ' Ni zaznanih anomalij napak'
speed_anomalies_title: ' Anomalije hitrosti (%{count}) '
no_speed_anomalies: ' Ni zaznanih anomalij hitrosti'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Okl: >%{ms}ms'
focus_char_value: 'Znak ''%{ch}'''
# Toplotna karta aktivnosti
heatmap:
title: ' Dnevna aktivnost (seje na dan) '
jan: 'Jan'
feb: 'Feb'
mar: 'Mar'
apr: 'Apr'
may: 'Maj'
jun: 'Jun'
jul: 'Jul'
aug: 'Avg'
sep: 'Sep'
oct: 'Okt'
nov: 'Nov'
dec: 'Dec'
# Grafikon
chart:
wpm_over_time: ' WPM skozi čas '
drill_number: 'Vaja #'
# Nastavitve
settings:
title: ' Nastavitve '
subtitle: 'S puščicami se premikajte, Enter/Desno za spremembo, ESC za shranitev in izhod'
target_wpm: 'Ciljni WPM'
theme: 'Tema'
word_count: 'Število besed'
ui_language: 'Jezik vmesnika'
dictionary_language: 'Jezik slovarja'
keyboard_layout: 'Razporeditev tipkovnice'
code_language: 'Programski jezik'
code_downloads: 'Prenosi kode'
on: 'Vklop'
off: 'Izklop'
code_download_dir: 'Mapa prenosov kode'
snippets_per_repo: 'Odrezkov na repo'
unlimited: 'Neomejeno'
download_code_now: 'Prenesi kodo zdaj'
run_downloader: 'Zaženi prenos'
passage_downloads: 'Prenosi odlomkov'
passage_download_dir: 'Mapa prenosov odlomkov'
paragraphs_per_book: 'Odstavkov na knjigo'
whole_book: 'Celotna knjiga'
download_passages_now: 'Prenesi odlomke zdaj'
export_path: 'Pot izvoza'
export_data: 'Izvozi podatke'
export_now: 'Izvozi zdaj'
import_path: 'Pot uvoza'
import_data: 'Uvozi podatke'
import_now: 'Uvozi zdaj'
hint_save_back: '[ESC] Shrani in nazaj'
hint_change_value: '[Enter/puščice] Spremeni vrednost'
hint_edit_path: '[Enter na poti] Uredi'
hint_move: '[←→] Premik'
hint_tab_complete: '[Tab] Dopolni (na koncu)'
hint_confirm: '[Enter] Potrdi'
hint_cancel: '[Esc] Prekliči'
success_title: ' Uspeh '
error_title: ' Napaka '
press_any_key: 'Pritisnite katerokoli tipko'
file_exists_title: ' Datoteka obstaja '
file_exists: 'Datoteka na tej poti že obstaja.'
overwrite_rename: '[d] Prepiši [r] Preimenuj [Esc] Prekliči'
erase_warning: 'To bo izbrisalo vaše trenutne podatke.'
export_first: 'Najprej izvozite, če jih želite obdržati.'
proceed_yn: 'Nadaljujem? (y/n)'
confirm_import_title: ' Potrdi uvoz '
# Zasloni izbire
select:
dictionary_language_title: ' Izberite jezik slovarja '
keyboard_layout_title: ' Izberite razporeditev tipkovnice '
code_language_title: ' Izberite programski jezik '
passage_source_title: ' Izberite vir odlomkov '
ui_language_title: ' Izberite jezik vmesnika '
more_above: '... še %{count} zgoraj ...'
more_below: '... še %{count} spodaj ...'
current: ' (trenutni)'
disabled: ' (onemogočeno)'
enabled_default: ' (omogočeno, privzeto: %{layout})'
enabled: ' (omogočeno)'
disabled_blocked: ' (onemogočeno: blokirano)'
built_in: ' (vgrajeno)'
cached: ' (predpomnjeno)'
disabled_download: ' (onemogočeno: potreben prenos)'
download_required: ' (potreben prenos)'
hint_navigate: '[Gor/Dol/PgUp/PgDn] Navigacija'
hint_confirm: '[Enter] Potrdi'
hint_back: '[ESC] Nazaj'
language_resets_layout: 'Izbira jezika ponastavi razporeditev tipkovnice na privzeto za ta jezik.'
layout_no_language_change: 'Sprememba razporeditve ne spremeni jezika slovarja.'
disabled_network_notice: 'Nekateri jeziki so onemogočeni: omogočite omrežne prenose v uvodu/nastavitvah.'
disabled_sources_notice: 'Nekateri viri so onemogočeni: omogočite omrežne prenose v uvodu/nastavitvah.'
passage_all: 'Vse (vgrajeno + vse knjige)'
passage_builtin: 'Samo vgrajeni odlomki'
passage_book_prefix: 'Knjiga: %{title}'
# Napredek
progress:
overall_key_progress: 'Skupni napredek tipk'
unlocked_mastered: '%{unlocked}/%{total} odklenjenih (%{mastered} osvojenih)'
# Drevo veščin
skill_tree:
title: ' Drevo veščin '
locked: 'Zaklenjeno'
unlocked: 'odklenjeno'
mastered: 'osvojeno'
in_progress: 'v teku'
complete: 'končano'
locked_status: 'zaklenjeno'
locked_notice: 'Dokončajte %{count} osnovnih črk za odklepanje vej'
branches_separator: 'Veje (na voljo po %{count} osnovnih črkah)'
unlocked_letters: 'Odklenjenih %{unlocked}/%{total} črk'
level: 'Raven %{current}/%{total}'
level_zero: 'Raven 0/%{total}'
in_focus: ' v fokusu'
hint_navigate: '[↑↓/jk] Navigacija'
hint_scroll: '[PgUp/PgDn ali Ctrl+U/Ctrl+D] Pomik'
hint_back: '[q] Nazaj'
hint_unlock: '[Enter] Odkleni'
hint_start_drill: '[Enter] Začni vajo'
unlock_msg_1: 'Ko je odklenjena, bo privzeta prilagodljiva vaja vključevala tipke iz te veje.'
unlock_msg_2: 'Če se želite osredotočiti le na to vejo, zaženite vajo neposredno iz drevesa veščin.'
confirm_unlock: 'Odkleniti %{branch}?'
confirm_yn: '[y] Odkleni [n/ESC] Prekliči'
lvl_prefix: 'Rav'
branch_primary_letters: 'Osnovne črke'
branch_capital_letters: 'Velike črke'
branch_numbers: 'Številke 0-9'
branch_prose_punctuation: 'Ločila v besedilu'
branch_whitespace: 'Presledki'
branch_code_symbols: 'Simboli kode'
level_frequency_order: 'Po pogostosti'
level_common_sentence_capitals: 'Pogoste velike začetnice'
level_name_capitals: 'Velike črke imen'
level_remaining_capitals: 'Preostale velike črke'
level_common_digits: 'Pogoste številke'
level_all_digits: 'Vse številke'
level_essential: 'Bistvena'
level_common: 'Pogosta'
level_expressive: 'Izrazna'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Zamik'
level_arithmetic_assignment: 'Aritmetika in prirejanje'
level_grouping: 'Združevanje'
level_logic_reference: 'Logika in referenca'
level_special: 'Posebno'
# Mejniki
milestones:
unlock_title: ' Tipka odklenjena! '
mastery_title: ' Tipka osvojena! '
branches_title: ' Nove veje veščin na voljo! '
branch_complete_title: ' Veja končana! '
all_unlocked_title: ' Vse tipke odklenjene! '
all_mastered_title: ' Popolna osvojitev tipkovnice! '
unlocked: 'odklenjena'
mastered: 'osvojena'
use_finger: 'Uporabite %{finger}'
hold_right_shift: 'Držite desni Shift (desni mezinec)'
hold_left_shift: 'Držite levi Shift (levi mezinec)'
congratulations_all_letters: 'Čestitke! Osvojili ste vseh %{count} osnovnih črk'
new_branches_available: 'Nove veje veščin so na voljo:'
visit_skill_tree: 'Obiščite drevo veščin za odklepanje nove veje'
and_start_training: 'in začnite z vadbo!'
open_skill_tree: 'Pritisnite [t] za odprtje drevesa veščin'
branch_complete_msg: 'Dokončali ste vejo %{branch}!'
all_levels_mastered: 'Vseh %{count} ravni osvojenih.'
all_keys_confident: 'Vse tipke v tej veji so na polnem zaupanju.'
all_unlocked_msg: 'Odklenili ste vse tipke na tipkovnici!'
all_unlocked_desc: 'Vsak znak, simbol in modifikator je zdaj na voljo v vajah.'
keep_practicing_mastery: 'Nadaljujte z vadbo za gradnjo osvojitve — ko vsaka tipka doseže polno'
confidence_complete: 'zaupanje, boste dosegli popolno osvojitev tipkovnice!'
all_mastered_msg: 'Čestitke — dosegli ste popolno osvojitev tipkovnice!'
all_mastered_desc: 'Vsaka tipka na tipkovnici je na maksimalnem zaupanju.'
mastery_takes_practice: 'Osvojitev ni cilj — zahteva stalno vadbo.'
keep_drilling: 'Nadaljujte z vajami, da ohranite prednost.'
hint_skill_tree_continue: '[t] Odpri drevo veščin [Katerakoli tipka] Nadaljuj'
hint_any_key: 'Pritisnite katerokoli tipko za nadaljevanje'
input_blocked: 'Vnos začasno blokiran (še %{ms}ms)'
unlock_msg_1: 'Odlično! Nadaljujte z gradenjem veščin tipkanja.'
unlock_msg_2: 'Še ena tipka dodana v vaš arzenal!'
unlock_msg_3: 'Vaša tipkovnica raste! Kar tako naprej.'
unlock_msg_4: 'En korak bližje popolni osvojitvi tipkovnice!'
mastery_msg_1: 'Ta tipka je zdaj na polnem zaupanju!'
mastery_msg_2: 'To tipko obvladate v celoti!'
mastery_msg_3: 'Mišični spomin zaklenjen!'
mastery_msg_4: 'Še ena tipka osvojena!'
# Raziskovalec tipkovnice
keyboard:
title: ' Tipkovnica '
subtitle: 'Pritisnite ali kliknite tipko'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigacija'
hint_back: '[q/ESC] Nazaj'
key_label: 'Tipka: '
finger_label: 'Prst: '
hand_left: 'Leva'
hand_right: 'Desna'
finger_index: 'Kazalec'
finger_middle: 'Sredinec'
finger_ring: 'Prstanec'
finger_pinky: 'Mezinec'
finger_thumb: 'Palec'
overall_accuracy: ' Skupna točnost: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Ocenjena točnost: %{correct}/%{total} (%{pct}%%)'
confidence: 'Zaupanje: '
no_data: 'Še ni podatkov'
no_data_short: 'Ni podatkov'
key_details: ' Podrobnosti tipke '
key_details_char: ' Podrobnosti tipke: ''%{ch}'' '
key_details_name: ' Podrobnosti tipke: %{name} '
press_key_hint: 'Pritisnite tipko za ogled podrobnosti'
shift_label: 'Shift: '
shift_no: 'Ne'
overall_avg_time: 'Skupni povpr. čas: '
overall_best_time: 'Skupni najb. čas: '
overall_samples: 'Skupni vzorci: '
overall_accuracy_label: 'Skupna točnost: '
branch_label: 'Veja: '
level_label: 'Raven: '
built_in_key: 'Vgrajena tipka'
unlocked_label: 'Odklenjena: '
yes: 'Da'
no: 'Ne'
in_focus_label: 'V fokusu?: '
mastery_label: 'Osvojitev: '
mastery_locked: 'Zaklenjeno'
ranked_avg_time: 'Ocenjeni povpr. čas: '
ranked_best_time: 'Ocenjeni najb. čas: '
ranked_samples: 'Ocenjeni vzorci: '
ranked_accuracy_label: 'Ocenjena točnost: '
# Uvodna pogovorna okna
intro:
passage_title: ' Nastavitev prenosov odlomkov '
code_title: ' Nastavitev prenosov kode '
enable_downloads: 'Omogoči omrežne prenose'
download_dir: 'Mapa prenosov'
paragraphs_per_book: 'Odstavkov na knjigo (0 = celotna)'
whole_book: 'celotna knjiga'
snippets_per_repo: 'Odrezkov na repo (0 = neomejeno)'
unlimited: 'neomejeno'
start_passage_drill: 'Začni vajo odlomkov'
start_code_drill: 'Začni vajo kode'
confirm: 'Potrdi'
hint_navigate: '[Gor/Dol] Navigacija'
hint_adjust: '[Levo/Desno] Prilagodi'
hint_edit: '[Tipkanje/Backspace] Uredi'
hint_confirm: '[Enter] Potrdi'
hint_cancel: '[ESC] Prekliči'
preparing_download: 'Pripravljam prenos...'
download_passage_title: ' Prenašanje vira odlomkov '
download_code_title: ' Prenašanje vira kode '
book_label: ' Knjiga: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bajtov'
downloaded_bytes: 'Preneseno: %{bytes} bajtov'
downloading_book_progress: 'Prenašam knjigo: [%{bar}] %{downloaded}/%{total} bajtov'
downloading_book_bytes: 'Prenašam knjigo: %{bytes} bajtov'
downloading_code_progress: 'Prenašam: [%{bar}] %{downloaded}/%{total} bajtov'
downloading_code_bytes: 'Prenašam: %{bytes} bajtov'
current_book: 'Trenutna: %{name} (knjiga %{done}/%{total})'
current_repo: 'Trenutni: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr lahko prenese odlomke iz Project Gutenberg za vajo tipkanja.'
passage_instructions_2: 'Knjige se prenesejo enkrat in se shranijo lokalno.'
passage_instructions_3: 'Nastavite prenose spodaj, nato zaženite vajo odlomkov.'
code_instructions_1: 'keydr lahko prenese odprtokodno kodo iz GitHub za vajo tipkanja.'
code_instructions_2: 'Koda se prenese enkrat in se shrani lokalno.'
code_instructions_3: 'Nastavite prenose spodaj, nato zaženite vajo kode.'
# Statusna sporočila (iz app.rs)
status:
recovery_files: 'Najdene obnovitvene datoteke iz prekinjenega uvoza. Podatki so morda nedosledni — razmislite o ponovnem uvozu.'
dir_not_exist: 'Mapa ne obstaja: %{path}'
no_data_store: 'Shramba podatkov ni na voljo'
serialization_error: 'Napaka serializacije: %{error}'
exported_to: 'Izvoženo v %{path}'
export_failed: 'Izvoz neuspešen: %{error}'
could_not_read: 'Ni mogoče prebrati datoteke: %{error}'
invalid_export: 'Neveljavna izvozna datoteka: %{error}'
unsupported_version: 'Nepodprta različica izvoza: %{got} (pričakovana %{expected})'
import_failed: 'Uvoz neuspešen: %{error}'
imported_theme_fallback: 'Uvoženo uspešno (tema ''%{theme}'' ni najdena, uporabljam privzeto)'
imported_success: 'Uvoženo uspešno'
adaptive_unavailable: 'Prilagodljiv ocenjen način ni na voljo: %{error}'
switched_to: 'Preklopljeno na %{name}'
layout_changed: 'Razporeditev spremenjena na %{name}'
# Napake (za prevod mejnih primerov vmesnika)
errors:
unknown_language: 'Neznan jezik: %{key}'
unknown_layout: 'Neznana razporeditev tipkovnice: %{key}'
unsupported_pair: 'Nepodprt par jezika/razporeditve: %{language} + %{layout}'
language_blocked: 'Jezik je blokiran glede na raven podpore: %{key}'
# Skupno
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Nazaj'

454
locales/sv.yml Normal file
View File

@@ -0,0 +1,454 @@
# Main menu
menu:
subtitle: 'Terminal Skrivtroenare'
adaptive_drill: 'Adaptiv oevning'
adaptive_drill_desc: 'Fonetiska ord med adaptiv bokstavsupplaasning'
code_drill: 'Kodoevning'
code_drill_desc: 'Oeva paa att skriva kodsyntax'
passage_drill: 'Textoevning'
passage_drill_desc: 'Skriv av stycken ur boecker'
skill_tree: 'Faerdighetstraed'
skill_tree_desc: 'Visa framstegsgrenar och starta oevningar'
keyboard: 'Tangentbord'
keyboard_desc: 'Utforska tangentbordslayout och tangentstatistik'
statistics: 'Statistik'
statistics_desc: 'Visa din skrivstatistik'
settings: 'Installningar'
settings_desc: 'Konfigurera keydr'
day_streak: ' | %{days} dagars svit'
key_progress: ' Tangentframsteg %{unlocked}/%{total} (%{mastered} behaerskade) | Maal %{target} WPM%{streak}'
hint_start: '[1-3] Start'
hint_skill_tree: '[t] Faerdighetstraed'
hint_keyboard: '[b] Tangentbord'
hint_stats: '[s] Statistik'
hint_settings: '[c] Installningar'
hint_quit: '[q] Avsluta'
# Drill screen
drill:
title: ' Oevning '
mode_adaptive: 'Adaptiv'
mode_code: 'Kod (Orankad)'
mode_passage: 'Text (Orankad)'
focus_char: 'Fokus: ''%{ch}'''
focus_bigram: 'Fokus: "%{bigram}"'
focus_both: 'Fokus: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Nog'
header_err: 'Fel'
code_source: ' Kodkaella '
passage_source: ' Textkaella '
footer: '[ESC] Avsluta oevning [Backspace] Radera'
keys_reenabled: 'Tangenter aateraktiverade efter %{ms}ms'
hint_end: '[ESC] Avsluta oevning'
hint_backspace: '[Backspace] Radera'
# Dashboard / drill result
dashboard:
title: ' Oevning klar '
results: 'Resultat'
unranked_note_prefix: ' (Orankad'
unranked_note_suffix: ' racknas inte i faerdighetsgrenen)'
speed: ' Hastighet: '
accuracy_label: ' Noggrannhet: '
time_label: ' Tid: '
errors_label: ' Fel: '
correct_detail: ' (%{correct}/%{total} korrekta)'
input_blocked: ' Inmatning tillfalligt blockerad '
input_blocked_ms: '(%{ms}ms aaterstar)'
hint_continue: '[c/Enter/Space] Fortsaett'
hint_retry: '[r] Foersoek igen'
hint_menu: '[q] Meny'
hint_stats: '[s] Statistik'
hint_delete: '[x] Radera'
# Stats sidebar (during drill)
sidebar:
title: ' Statistik '
wpm: 'WPM: '
target: 'Maal: '
target_wpm: '%{wpm} WPM'
accuracy: 'Noggrannhet: '
progress: 'Framsteg: '
correct: 'Korrekta: '
errors: 'Fel: '
time: 'Tid: '
last_drill: ' Senaste oevning '
vs_avg: ' vs snitt: '
# Statistics dashboard
stats:
title: ' Statistik '
empty: 'Inga oevningar avklarade aennu. Boerja skriva!'
tab_dashboard: '[1] Dashboard'
tab_history: '[2] Historik'
tab_activity: '[3] Aktivitet'
tab_accuracy: '[4] Noggrannhet'
tab_timing: '[5] Timing'
tab_ngrams: '[6] N-gram'
hint_back: '[ESC] Tillbaka'
hint_next_tab: '[Tab] Naesta flik'
hint_switch_tab: '[1-6] Byt flik'
hint_navigate: '[j/k] Navigera'
hint_page: '[PgUp/PgDn] Sida'
hint_delete: '[x] Radera'
summary_title: ' Sammanfattning '
drills: ' Oevningar: '
avg_wpm: ' Snitt WPM: '
best_wpm: ' Baesta WPM: '
accuracy_label: ' Noggrannhet: '
total_time: ' Total tid: '
wpm_chart_title: ' WPM per oevning (Senaste 20, Maal: %{target}) '
accuracy_chart_title: ' Noggrannhet %% (Senaste 50 oevningar) '
chart_drill: 'Oevning #'
chart_accuracy_pct: 'Noggrannhet %%'
sessions_title: ' Senaste sessioner '
session_header: ' # WPM Raa Nog%% Tid Datum/Tid Laege Rankad Delvis'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Radera session #%{idx}? (y/n)'
confirm_title: ' Bekraefta '
yes: 'ja'
no: 'nej'
keyboard_accuracy_title: ' Tangentbords noggrannhet %% '
keyboard_timing_title: ' Tangentbords timing (ms) '
slowest_keys_title: ' Laangsammaste tangenter (ms) '
fastest_keys_title: ' Snabbaste tangenter (ms) '
worst_accuracy_title: ' Saemst noggrannhet (%%) '
best_accuracy_title: ' Baest noggrannhet (%%) '
not_enough_data: ' Inte tillraeckligt med data'
streaks_title: ' Sviter '
current_streak: ' Nuvarande: '
best_streak: ' Baesta: '
active_days: ' Aktiva dagar: '
top_days_none: ' Toppdagar: inga'
top_days: ' Toppdagar: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Nog: %{pct}%%'
keys_label: ' Tangenter: %{unlocked}/%{total} (%{mastered} behaerskade)'
ngram_empty: 'Genomfoer naagra adaptiva oevningar foer att se n-gram data'
ngram_header_speed_narrow: ' Bgrm Hast Foerv Anom%'
ngram_header_error_narrow: ' Bgrm Fel Stp Freq Foerv Anom%'
ngram_header_speed: ' Bigram Hast Foervant Stickpr. Anom%'
ngram_header_error: ' Bigram Fel Stickpr. Freq Foervant Anom%'
focus_title: ' Aktivt fokus '
focus_char_label: ' Fokus: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'fel'
anomaly_speed: 'hastighet'
focus_detail_both: ' Tecken ''%{ch}'': svagaste tangent | Bigram %{label}: %{type}-anomali %{pct}%%'
focus_detail_char_only: ' Tecken ''%{ch}'': svagaste tangent, inga bekraeftade bigram-anomalier'
focus_detail_bigram_only: ' (%{type}-anomali: %{pct}%%)'
focus_empty: ' Genomfoer naagra adaptiva oevningar foer att se fokusdata'
error_anomalies_title: ' Fel-anomalier (%{count}) '
no_error_anomalies: ' Inga fel-anomalier upptaeckta'
speed_anomalies_title: ' Hastighets-anomalier (%{count}) '
no_speed_anomalies: ' Inga hastighets-anomalier upptaeckta'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Hes: >%{ms}ms'
focus_char_value: 'Tecken ''%{ch}'''
# Activity heatmap
heatmap:
title: ' Daglig aktivitet (Sessioner per dag) '
jan: 'Jan'
feb: 'Feb'
mar: 'Mar'
apr: 'Apr'
may: 'Maj'
jun: 'Jun'
jul: 'Jul'
aug: 'Aug'
sep: 'Sep'
oct: 'Okt'
nov: 'Nov'
dec: 'Dec'
# Chart
chart:
wpm_over_time: ' WPM oever tid '
drill_number: 'Oevning #'
# Settings
settings:
title: ' Installningar '
subtitle: 'Piltangenter foer att navigera, Enter/Hoeger foer att aendra, ESC foer att spara'
target_wpm: 'Maal-WPM'
theme: 'Tema'
word_count: 'Antal ord'
ui_language: 'Spraak (UI)'
dictionary_language: 'Ordlistespraak'
keyboard_layout: 'Tangentbordslayout'
code_language: 'Kodspraak'
code_downloads: 'Kod-nedladdningar'
on: 'Paa'
off: 'Av'
code_download_dir: 'Kod-nedladdningsmapp'
snippets_per_repo: 'Utdrag per repo'
unlimited: 'Obegransat'
download_code_now: 'Ladda ner kod nu'
run_downloader: 'Starta nedladdning'
passage_downloads: 'Text-nedladdningar'
passage_download_dir: 'Text-nedladdningsmapp'
paragraphs_per_book: 'Stycken per bok'
whole_book: 'Hela boken'
download_passages_now: 'Ladda ner texter nu'
export_path: 'Exportsoekvaeg'
export_data: 'Exportera data'
export_now: 'Exportera nu'
import_path: 'Importsoekvaeg'
import_data: 'Importera data'
import_now: 'Importera nu'
hint_save_back: '[ESC] Spara & tillbaka'
hint_change_value: '[Enter/pilar] AEndra vaerde'
hint_edit_path: '[Enter paa soekvaeg] Redigera'
hint_move: '[←→] Flytta'
hint_tab_complete: '[Tab] Komplettera (i slutet)'
hint_confirm: '[Enter] Bekraefta'
hint_cancel: '[Esc] Avbryt'
success_title: ' Lyckades '
error_title: ' Fel '
press_any_key: 'Tryck paa valfri tangent'
file_exists_title: ' Filen finns '
file_exists: 'En fil finns redan paa denna soekvaeg.'
overwrite_rename: '[d] Skriv oever [r] Byt namn [Esc] Avbryt'
erase_warning: 'Detta kommer att radera din nuvarande data.'
export_first: 'Exportera foerst om du vill behalla den.'
proceed_yn: 'Fortsaett? (y/n)'
confirm_import_title: ' Bekraefta import '
# Selection screens
select:
dictionary_language_title: ' Vaelj ordlistespraak '
keyboard_layout_title: ' Vaelj tangentbordslayout '
code_language_title: ' Vaelj kodspraak '
passage_source_title: ' Vaelj textkaella '
ui_language_title: ' Vaelj spraak (UI) '
more_above: '... %{count} fler ovan ...'
more_below: '... %{count} fler nedan ...'
current: ' (nuvarande)'
disabled: ' (inaktiverad)'
enabled_default: ' (aktiverad, standard: %{layout})'
enabled: ' (aktiverad)'
disabled_blocked: ' (inaktiverad: blockerad)'
built_in: ' (inbyggd)'
cached: ' (sparad)'
disabled_download: ' (inaktiverad: nedladdning kraevs)'
download_required: ' (nedladdning kraevs)'
hint_navigate: '[Upp/Ner/PgUp/PgDn] Navigera'
hint_confirm: '[Enter] Bekraefta'
hint_back: '[ESC] Tillbaka'
language_resets_layout: 'Spraakval aaterstaeller tangentbordslayouten till spraakens standard.'
layout_no_language_change: 'Layoutaendringar aendrar inte ordlistespraaket.'
disabled_network_notice: 'Vissa spraak aer inaktiverade: aktivera naetverksnedladdningar i intro/installningar.'
disabled_sources_notice: 'Vissa kaellor aer inaktiverade: aktivera naetverksnedladdningar i intro/installningar.'
passage_all: 'Alla (Inbyggda + alla boecker)'
passage_builtin: 'Endast inbyggda passager'
passage_book_prefix: 'Bok: %{title}'
# Progress
progress:
overall_key_progress: 'Total tangentframsteg'
unlocked_mastered: '%{unlocked}/%{total} uplaasta (%{mastered} behaerskade)'
# Skill tree
skill_tree:
title: ' Faerdighetstraed '
locked: 'Laast'
unlocked: 'uplaast'
mastered: 'behaerskad'
in_progress: 'paagaar'
complete: 'klar'
locked_status: 'laast'
locked_notice: 'Slutfoer %{count} grundboekstaver foer att laasa upp grenar'
branches_separator: 'Grenar (tillgaengliga efter %{count} grundboekstaver)'
unlocked_letters: '%{unlocked}/%{total} boekstaver uplaasta'
level: 'Nivaa %{current}/%{total}'
level_zero: 'Nivaa 0/%{total}'
in_focus: ' i fokus'
hint_navigate: '[↑↓/jk] Navigera'
hint_scroll: '[PgUp/PgDn eller Ctrl+U/Ctrl+D] Scrolla'
hint_back: '[q] Tillbaka'
hint_unlock: '[Enter] Laas upp'
hint_start_drill: '[Enter] Starta oevning'
unlock_msg_1: 'Efter upplaasning blandas uplaasta tangenter fraan denna gren in i den adaptiva oevningen.'
unlock_msg_2: 'Vill du bara oeva denna gren, starta en oevning direkt fraan denna gren i Faerdighetsgrenen.'
confirm_unlock: 'Laas upp %{branch}?'
confirm_yn: '[y] Laas upp [n/ESC] Avbryt'
lvl_prefix: 'Niv'
branch_primary_letters: 'Grundboekstaver'
branch_capital_letters: 'Versaler'
branch_numbers: 'Siffror 0-9'
branch_prose_punctuation: 'Skiljetecken'
branch_whitespace: 'Blanksteg'
branch_code_symbols: 'Kodsymboler'
level_frequency_order: 'Frekvensordning'
level_common_sentence_capitals: 'Vanliga meningsversaler'
level_name_capitals: 'Namnversaler'
level_remaining_capitals: 'Oevriga versaler'
level_common_digits: 'Vanliga siffror'
level_all_digits: 'Alla siffror'
level_essential: 'Grundlaeggande'
level_common: 'Vanliga'
level_expressive: 'Uttrycksfulla'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Indrag'
level_arithmetic_assignment: 'Aritmetik & Tilldelning'
level_grouping: 'Gruppering'
level_logic_reference: 'Logik & Referens'
level_special: 'Special'
# Milestones
milestones:
unlock_title: ' Tangent uplaast! '
mastery_title: ' Tangent behaerskad! '
branches_title: ' Nya faerdighetsgrenar tillgaengliga! '
branch_complete_title: ' Gren klar! '
all_unlocked_title: ' Alla tangenter uplaasta! '
all_mastered_title: ' Full tangentbordsbehaerskning! '
unlocked: 'uplaast'
mastered: 'behaerskad'
use_finger: 'Anvaend ditt %{finger}'
hold_right_shift: 'Haall hoeger Shift (hoeger lillfinger)'
hold_left_shift: 'Haall vaenster Shift (vaenster lillfinger)'
congratulations_all_letters: 'Grattis! Du har behaerskat alla %{count} grundboekstaver'
new_branches_available: 'Nya faerdighetsgrenar aer nu tillgaengliga:'
visit_skill_tree: 'Besoek Faerdighetsgrenen foer att laasa upp en ny gren'
and_start_training: 'och boerja traena!'
open_skill_tree: 'Tryck [t] foer att oeppna Faerdighetsgrenen nu'
branch_complete_msg: 'Du har slutfoert grenen %{branch}!'
all_levels_mastered: 'Alla %{count} nivaaer behaerskade.'
all_keys_confident: 'Varje tangent i denna gren har fullt foertroende.'
all_unlocked_msg: 'Du har laast upp varje tangent paa tangentbordet!'
all_unlocked_desc: 'Varje tecken, symbol och modifierare aer nu tillgaenglig i dina oevningar.'
keep_practicing_mastery: 'Fortsaett oeva foer att bygga behaerskning — naer varje tangent naar fullt'
confidence_complete: 'foertroende har du uppnaatt fullstaendig tangentbordsbehaerskning!'
all_mastered_msg: 'Grattis — du har uppnaatt fullstaendig tangentbordsbehaerskning!'
all_mastered_desc: 'Varje tangent paa tangentbordet har maximalt foertroende.'
mastery_takes_practice: 'Behaerskning aer inte en destination — det kraever staendig oevning.'
keep_drilling: 'Fortsaett oeva foer att behalla din skaerpa.'
hint_skill_tree_continue: '[t] Faerdighetstraed [Annan tangent] Fortsaett'
hint_any_key: 'Tryck paa valfri tangent foer att fortsaetta'
input_blocked: 'Inmatning tillfalligt blockerad (%{ms}ms aaterstar)'
unlock_msg_1: 'Bra jobbat! Fortsaett bygga dina skrivfaerdigheter.'
unlock_msg_2: 'Annu en tangent i din arsenal!'
unlock_msg_3: 'Ditt tangentbord vaexer! Fortsaett saa.'
unlock_msg_4: 'Ett steg naermare full tangentbordsbehaerskning!'
mastery_msg_1: 'Denna tangent har nu fullt foertroende!'
mastery_msg_2: 'Du behaerskar denna tangent perfekt!'
mastery_msg_3: 'Muskelminne foerankrat!'
mastery_msg_4: 'Annu en tangent eroevrrad!'
# Keyboard explorer
keyboard:
title: ' Tangentbord '
subtitle: 'Tryck paa en tangent eller klicka paa en tangent'
hint_navigate: '[←→↑↓/hjkl/Tab] Navigera'
hint_back: '[q/ESC] Tillbaka'
key_label: 'Tangent: '
finger_label: 'Finger: '
hand_left: 'Vaenster'
hand_right: 'Hoeger'
finger_index: 'Pekfinger'
finger_middle: 'Langfinger'
finger_ring: 'Ringfinger'
finger_pinky: 'Lillfinger'
finger_thumb: 'Tumme'
overall_accuracy: ' Total noggrannhet: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Rankad noggrannhet: %{correct}/%{total} (%{pct}%%)'
confidence: 'Foertroende: '
no_data: 'Ingen data aennu'
no_data_short: 'Ingen data'
key_details: ' Tangentdetaljer '
key_details_char: ' Tangentdetaljer: ''%{ch}'' '
key_details_name: ' Tangentdetaljer: %{name} '
press_key_hint: 'Tryck paa en tangent foer detaljer'
shift_label: 'Shift: '
shift_no: 'Nej'
overall_avg_time: 'Total snittid: '
overall_best_time: 'Total baesta tid: '
overall_samples: 'Total stickprov: '
overall_accuracy_label: 'Total noggrannhet: '
branch_label: 'Gren: '
level_label: 'Nivaa: '
built_in_key: 'Inbyggd tangent'
unlocked_label: 'Uplaast: '
yes: 'Ja'
no: 'Nej'
in_focus_label: 'I fokus?: '
mastery_label: 'Behaerskning: '
mastery_locked: 'Laast'
ranked_avg_time: 'Rankad snittid: '
ranked_best_time: 'Rankad baesta tid: '
ranked_samples: 'Rankade stickprov: '
ranked_accuracy_label: 'Rankad noggrannhet: '
# Intro dialogs
intro:
passage_title: ' Text-nedladdning installning '
code_title: ' Kod-nedladdning installning '
enable_downloads: 'Aktivera naetverksnedladdningar'
download_dir: 'Nedladdningsmapp'
paragraphs_per_book: 'Stycken per bok (0 = hela)'
whole_book: 'hela boken'
snippets_per_repo: 'Utdrag per repo (0 = obegransat)'
unlimited: 'obegransat'
start_passage_drill: 'Starta textoevning'
start_code_drill: 'Starta kodoevning'
confirm: 'Bekraefta'
hint_navigate: '[Upp/Ner] Navigera'
hint_adjust: '[Vaenster/Hoeger] Justera'
hint_edit: '[Skriv/Backspace] Redigera'
hint_confirm: '[Enter] Bekraefta'
hint_cancel: '[ESC] Avbryt'
preparing_download: 'Forbereder nedladdning...'
download_passage_title: ' Laddar ner textkaella '
download_code_title: ' Laddar ner kodkaella '
book_label: ' Bok: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bytes'
downloaded_bytes: 'Nedladdat: %{bytes} bytes'
downloading_book_progress: 'Laddar ner aktuell bok: [%{bar}] %{downloaded}/%{total} bytes'
downloading_book_bytes: 'Laddar ner aktuell bok: %{bytes} bytes'
downloading_code_progress: 'Laddar ner: [%{bar}] %{downloaded}/%{total} bytes'
downloading_code_bytes: 'Laddar ner: %{bytes} bytes'
current_book: 'Aktuell: %{name} (bok %{done}/%{total})'
current_repo: 'Aktuell: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr kan ladda ner texter fraan Project Gutenberg foer skrivoevning.'
passage_instructions_2: 'Boecker laddas ner en gaang och sparas lokalt.'
passage_instructions_3: 'Konfigurera nedladdningsinstallningar nedan och starta en textoevning.'
code_instructions_1: 'keydr kan ladda ner oeppenkallkod fraan GitHub foer skrivoevning.'
code_instructions_2: 'Kod laddas ner en gaang och sparas lokalt.'
code_instructions_3: 'Konfigurera nedladdningsinstallningar nedan och starta en kodoevning.'
# Status messages (from app.rs)
status:
recovery_files: 'Aaterstaellningsfiler hittade fraan avbruten import. Data kan vara inkonsekvent — oevervaeg att importera paa nytt.'
dir_not_exist: 'Mappen finns inte: %{path}'
no_data_store: 'Inget datalager tillgaengligt'
serialization_error: 'Serialiseringsfel: %{error}'
exported_to: 'Exporterad till %{path}'
export_failed: 'Export misslyckades: %{error}'
could_not_read: 'Kunde inte laesa filen: %{error}'
invalid_export: 'Ogiltig exportfil: %{error}'
unsupported_version: 'Exportversion stoeds inte: %{got} (foervaentad %{expected})'
import_failed: 'Import misslyckades: %{error}'
imported_theme_fallback: 'Importerad (tema ''%{theme}'' hittades inte, standard anvaends)'
imported_success: 'Importerad'
adaptive_unavailable: 'Adaptivt rankat laege inte tillgaengligt: %{error}'
switched_to: 'Bytt till %{name}'
layout_changed: 'Layout aendrad till %{name}'
# Errors (for UI boundary translation)
errors:
unknown_language: 'Okaent spraak: %{key}'
unknown_layout: 'Okaend tangentbordslayout: %{key}'
unsupported_pair: 'Spraak-/layoutpar stoeds inte: %{language} + %{layout}'
language_blocked: 'Spraak blockerat av stoednivaa: %{key}'
# Common
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Tillbaka'

454
locales/tr.yml Normal file
View File

@@ -0,0 +1,454 @@
# Ana menü
menu:
subtitle: 'Terminal Yazma Eğitmeni'
adaptive_drill: 'Uyarlanır Alıştırma'
adaptive_drill_desc: 'Uyarlanır harf açma ile fonetik kelimeler'
code_drill: 'Kod Alıştırması'
code_drill_desc: 'Kod sözdizimi yazma alıştırması'
passage_drill: 'Metin Alıştırması'
passage_drill_desc: 'Kitaplardan pasajları yazın'
skill_tree: 'Yetenek Ağacı'
skill_tree_desc: 'İlerleme dallarını görüntüle ve alıştırma başlat'
keyboard: 'Klavye'
keyboard_desc: 'Klavye düzenini ve tuş istatistiklerini incele'
statistics: 'İstatistikler'
statistics_desc: 'Yazma istatistiklerinizi görüntüleyin'
settings: 'Ayarlar'
settings_desc: 'keydr yapılandır'
day_streak: ' | %{days} gün seri'
key_progress: ' Tuş İlerlemesi %{unlocked}/%{total} (%{mastered} ustalaşmış) | Hedef %{target} WPM%{streak}'
hint_start: '[1-3] Başla'
hint_skill_tree: '[t] Yetenek Ağacı'
hint_keyboard: '[b] Klavye'
hint_stats: '[s] İstatistikler'
hint_settings: '[c] Ayarlar'
hint_quit: '[q] Çıkış'
# Alıştırma ekranı
drill:
title: ' Alıştırma '
mode_adaptive: 'Uyarlanır'
mode_code: 'Kod (Sıralamasız)'
mode_passage: 'Metin (Sıralamasız)'
focus_char: 'Odak: ''%{ch}'''
focus_bigram: 'Odak: "%{bigram}"'
focus_both: 'Odak: ''%{ch}'' + "%{bigram}"'
header_wpm: 'WPM'
header_acc: 'Doğ'
header_err: 'Hta'
code_source: ' Kod kaynağı '
passage_source: ' Metin kaynağı '
footer: '[ESC] Alıştırmayı bitir [Backspace] Sil'
keys_reenabled: 'Tuşlar %{ms}ms sonra yeniden etkin'
hint_end: '[ESC] Alıştırmayı bitir'
hint_backspace: '[Backspace] Sil'
# Pano / alıştırma sonucu
dashboard:
title: ' Alıştırma Tamamlandı '
results: 'Sonuçlar'
unranked_note_prefix: ' (Sırasız'
unranked_note_suffix: ' yetenek ağacına sayılmaz)'
speed: ' Hız: '
accuracy_label: ' Doğruluk: '
time_label: ' Süre: '
errors_label: ' Hatalar: '
correct_detail: ' (%{correct}/%{total} doğru)'
input_blocked: ' Giriş geçici olarak engellendi '
input_blocked_ms: '(%{ms}ms kaldı)'
hint_continue: '[c/Enter/Space] Devam'
hint_retry: '[r] Tekrar'
hint_menu: '[q] Menü'
hint_stats: '[s] İstatistikler'
hint_delete: '[x] Sil'
# İstatistik kenar çubuğu (alıştırma sırasında)
sidebar:
title: ' İstatistikler '
wpm: 'WPM: '
target: 'Hedef: '
target_wpm: '%{wpm} WPM'
accuracy: 'Doğruluk: '
progress: 'İlerleme: '
correct: 'Doğru: '
errors: 'Hatalar: '
time: 'Süre: '
last_drill: ' Son Alıştırma '
vs_avg: ' vs ort: '
# İstatistik panosu
stats:
title: ' İstatistikler '
empty: 'Henüz alıştırma yapılmadı. Yazmaya başlayın!'
tab_dashboard: '[1] Pano'
tab_history: '[2] Geçmiş'
tab_activity: '[3] Aktivite'
tab_accuracy: '[4] Doğruluk'
tab_timing: '[5] Zamanlama'
tab_ngrams: '[6] N-gramlar'
hint_back: '[ESC] Geri'
hint_next_tab: '[Tab] Sonraki sekme'
hint_switch_tab: '[1-6] Sekme değiştir'
hint_navigate: '[j/k] Gezin'
hint_page: '[PgUp/PgDn] Sayfa'
hint_delete: '[x] Sil'
summary_title: ' Özet '
drills: ' Alıştırmalar: '
avg_wpm: ' Ort WPM: '
best_wpm: ' En iyi WPM: '
accuracy_label: ' Doğruluk: '
total_time: ' Toplam süre: '
wpm_chart_title: ' Alıştırma başına WPM (son 20, hedef: %{target}) '
accuracy_chart_title: ' Doğruluk %% (Son 50 Alıştırma) '
chart_drill: 'Alıştırma #'
chart_accuracy_pct: 'Doğruluk %%'
sessions_title: ' Son Oturumlar '
session_header: ' # WPM Ham Doğ%% Süre Tarih/Saat Mod Sıralı Kısmi'
session_separator: ' ─────────────────────────────────────────────────────────────────────'
delete_confirm: 'Oturum #%{idx} silinsin mi? (y/n)'
confirm_title: ' Onayla '
yes: 'evet'
no: 'hayır'
keyboard_accuracy_title: ' Klavye Doğruluğu %% '
keyboard_timing_title: ' Klavye Zamanlaması (ms) '
slowest_keys_title: ' En Yavaş Tuşlar (ms) '
fastest_keys_title: ' En Hızlı Tuşlar (ms) '
worst_accuracy_title: ' En Kötü Doğruluk (%%) '
best_accuracy_title: ' En İyi Doğruluk (%%) '
not_enough_data: ' Yeterli veri yok'
streaks_title: ' Seriler '
current_streak: ' Mevcut: '
best_streak: ' En iyi: '
active_days: ' Aktif Günler: '
top_days_none: ' En iyi Günler: yok'
top_days: ' En iyi Günler: %{days}'
wpm_label: ' WPM: %{avg}/%{target} (%{pct}%%)'
acc_label: ' Doğ: %{pct}%%'
keys_label: ' Tuşlar: %{unlocked}/%{total} (%{mastered} ustalaşmış)'
ngram_empty: 'N-gram verilerini görmek için uyarlanır alıştırmalar yapın'
ngram_header_speed_narrow: ' Bgrm Hız Bekl Anom%'
ngram_header_error_narrow: ' Bgrm Hta Örn Oran Bekl Anom%'
ngram_header_speed: ' Bigram Hız Beklenen Örnekler Anom%'
ngram_header_error: ' Bigram Hatalar Örnekler Oran Beklenen Anom%'
focus_title: ' Aktif Odak '
focus_char_label: ' Odak: '
focus_bigram_value: 'Bigram %{label}'
focus_plus: ' + '
anomaly_error: 'hata'
anomaly_speed: 'hız'
focus_detail_both: ' Karakter ''%{ch}'': en zayıf tuş | Bigram %{label}: %{type} anomalisi %{pct}%%'
focus_detail_char_only: ' Karakter ''%{ch}'': en zayıf tuş, onaylanmış bigram anomalisi yok'
focus_detail_bigram_only: ' (%{type} anomalisi: %{pct}%%)'
focus_empty: ' Odak verilerini görmek için uyarlanır alıştırmalar yapın'
error_anomalies_title: ' Hata Anomalileri (%{count}) '
no_error_anomalies: ' Hata anomalisi tespit edilmedi'
speed_anomalies_title: ' Hız Anomalileri (%{count}) '
no_speed_anomalies: ' Hız anomalisi tespit edilmedi'
scope_label_prefix: ' '
bi_label: ' | Bi: %{count}'
hes_label: ' | Dur: >%{ms}ms'
focus_char_value: 'Karakter ''%{ch}'''
# Aktivite ısı haritası
heatmap:
title: ' Günlük Aktivite (Günlük Oturum Sayısı) '
jan: 'Oca'
feb: 'Şub'
mar: 'Mar'
apr: 'Nis'
may: 'May'
jun: 'Haz'
jul: 'Tem'
aug: 'Ağu'
sep: 'Eyl'
oct: 'Eki'
nov: 'Kas'
dec: 'Ara'
# Grafik
chart:
wpm_over_time: ' Zamana Göre WPM '
drill_number: 'Alıştırma #'
# Ayarlar
settings:
title: ' Ayarlar '
subtitle: 'Gezinmek için okları, değiştirmek için Enter/Sağ, kaydetmek için ESC kullanın'
target_wpm: 'Hedef WPM'
theme: 'Tema'
word_count: 'Kelime Sayısı'
ui_language: 'Arayüz Dili'
dictionary_language: 'Sözlük Dili'
keyboard_layout: 'Klavye Düzeni'
code_language: 'Programlama Dili'
code_downloads: 'Kod İndirmeleri'
on: 'Açık'
off: 'Kapalı'
code_download_dir: 'Kod İndirme Klasörü'
snippets_per_repo: 'Repo başına parçacık'
unlimited: 'Sınırsız'
download_code_now: 'Kodu Şimdi İndir'
run_downloader: 'İndiriciyi çalıştır'
passage_downloads: 'Metin İndirmeleri'
passage_download_dir: 'Metin İndirme Klasörü'
paragraphs_per_book: 'Kitap başına paragraf'
whole_book: 'Tüm kitap'
download_passages_now: 'Metinleri Şimdi İndir'
export_path: 'Dışa Aktarma Yolu'
export_data: 'Veriyi Dışa Aktar'
export_now: 'Şimdi dışa aktar'
import_path: 'İçe Aktarma Yolu'
import_data: 'Veriyi İçe Aktar'
import_now: 'Şimdi içe aktar'
hint_save_back: '[ESC] Kaydet ve geri'
hint_change_value: '[Enter/oklar] Değer değiştir'
hint_edit_path: '[Enter yolda] Düzenle'
hint_move: '[←→] Taşı'
hint_tab_complete: '[Tab] Tamamla (sonda)'
hint_confirm: '[Enter] Onayla'
hint_cancel: '[Esc] İptal'
success_title: ' Başarılı '
error_title: ' Hata '
press_any_key: 'Herhangi bir tuşa basın'
file_exists_title: ' Dosya Mevcut '
file_exists: 'Bu yolda zaten bir dosya var.'
overwrite_rename: '[d] Üzerine yaz [r] Yeniden adlandır [Esc] İptal'
erase_warning: 'Bu mevcut verilerinizi silecek.'
export_first: 'Saklamak istiyorsanız önce dışa aktarın.'
proceed_yn: 'Devam edilsin mi? (y/n)'
confirm_import_title: ' İçe Aktarmayı Onayla '
# Seçim ekranları
select:
dictionary_language_title: ' Sözlük Dilini Seçin '
keyboard_layout_title: ' Klavye Düzenini Seçin '
code_language_title: ' Programlama Dilini Seçin '
passage_source_title: ' Metin Kaynağını Seçin '
ui_language_title: ' Arayüz Dilini Seçin '
more_above: '... yukarıda %{count} daha ...'
more_below: '... aşağıda %{count} daha ...'
current: ' (mevcut)'
disabled: ' (devre dışı)'
enabled_default: ' (etkin, varsayılan: %{layout})'
enabled: ' (etkin)'
disabled_blocked: ' (devre dışı: engelli)'
built_in: ' (yerleşik)'
cached: ' (önbelleğe alınmış)'
disabled_download: ' (devre dışı: indirme gerekli)'
download_required: ' (indirme gerekli)'
hint_navigate: '[Yukarı/Aşağı/PgUp/PgDn] Gezin'
hint_confirm: '[Enter] Onayla'
hint_back: '[ESC] Geri'
language_resets_layout: 'Dil seçimi, klavye düzenini o dilin varsayılanına sıfırlar.'
layout_no_language_change: 'Düzen değişiklikleri sözlük dilini değiştirmez.'
disabled_network_notice: 'Bazı diller devre dışı: giriş/ayarlarda ağ indirmelerini etkinleştirin.'
disabled_sources_notice: 'Bazı kaynaklar devre dışı: giriş/ayarlarda ağ indirmelerini etkinleştirin.'
passage_all: 'Tümü (Yerleşik + tüm kitaplar)'
passage_builtin: 'Yalnızca yerleşik metinler'
passage_book_prefix: 'Kitap: %{title}'
# İlerleme
progress:
overall_key_progress: 'Genel Tuş İlerlemesi'
unlocked_mastered: '%{unlocked}/%{total} açılmış (%{mastered} ustalaşmış)'
# Yetenek ağacı
skill_tree:
title: ' Yetenek Ağacı '
locked: 'Kilitli'
unlocked: 'açılmış'
mastered: 'ustalaşmış'
in_progress: 'devam ediyor'
complete: 'tamamlanmış'
locked_status: 'kilitli'
locked_notice: 'Dalları açmak için %{count} ana harfi tamamlayın'
branches_separator: 'Dallar (%{count} ana harften sonra kullanılabilir)'
unlocked_letters: '%{unlocked}/%{total} harf açılmış'
level: 'Seviye %{current}/%{total}'
level_zero: 'Seviye 0/%{total}'
in_focus: ' odakta'
hint_navigate: '[↑↓/jk] Gezin'
hint_scroll: '[PgUp/PgDn veya Ctrl+U/Ctrl+D] Kaydır'
hint_back: '[q] Geri'
hint_unlock: '[Enter] Aç'
hint_start_drill: '[Enter] Alıştırma Başlat'
unlock_msg_1: 'Açıldıktan sonra varsayılan uyarlanır alıştırma bu dalın açılmış tuşlarını karıştıracak.'
unlock_msg_2: 'Yalnızca bu dala odaklanmak istiyorsanız Yetenek Ağacından doğrudan alıştırma başlatın.'
confirm_unlock: '%{branch} açılsın mı?'
confirm_yn: '[y] Aç [n/ESC] İptal'
lvl_prefix: 'Svy'
branch_primary_letters: 'Ana Harfler'
branch_capital_letters: 'Büyük Harfler'
branch_numbers: 'Rakamlar 0-9'
branch_prose_punctuation: 'Noktalama İşaretleri'
branch_whitespace: 'Boşluk Karakterleri'
branch_code_symbols: 'Kod Sembolleri'
level_frequency_order: 'Sıklık Sırası'
level_common_sentence_capitals: 'Yaygın Cümle Büyük Harfleri'
level_name_capitals: 'İsim Büyük Harfleri'
level_remaining_capitals: 'Kalan Büyük Harfler'
level_common_digits: 'Yaygın Rakamlar'
level_all_digits: 'Tüm Rakamlar'
level_essential: 'Temel'
level_common: 'Yaygın'
level_expressive: 'İfade Edici'
level_enter_return: 'Enter/Return'
level_tab_indent: 'Tab/Girinti'
level_arithmetic_assignment: 'Aritmetik ve Atama'
level_grouping: 'Gruplama'
level_logic_reference: 'Mantık ve Başvuru'
level_special: 'Özel'
# Kilometre taşları
milestones:
unlock_title: ' Tuş Açıldı! '
mastery_title: ' Tuş Ustalaşıldı! '
branches_title: ' Yeni Yetenek Dalları Mevcut! '
branch_complete_title: ' Dal Tamamlandı! '
all_unlocked_title: ' Tüm Tuşlar Açıldı! '
all_mastered_title: ' Tam Klavye Ustalığı! '
unlocked: 'açıldı'
mastered: 'ustalaşıldı'
use_finger: '%{finger} kullanın'
hold_right_shift: 'Sağ Shift''i tutun (sağ serçe parmak)'
hold_left_shift: 'Sol Shift''i tutun (sol serçe parmak)'
congratulations_all_letters: 'Tebrikler! Tüm %{count} ana harfte ustalaştınız'
new_branches_available: 'Yeni yetenek dalları artık mevcut:'
visit_skill_tree: 'Yeni bir dal açmak için Yetenek Ağacını ziyaret edin'
and_start_training: 've eğitime başlayın!'
open_skill_tree: 'Yetenek Ağacını açmak için [t] basın'
branch_complete_msg: '%{branch} dalını tamamladınız!'
all_levels_mastered: 'Tüm %{count} seviye ustalaşıldı.'
all_keys_confident: 'Bu daldaki her tuş tam güven seviyesinde.'
all_unlocked_msg: 'Klavyedeki tüm tuşları açtınız!'
all_unlocked_desc: 'Her karakter, sembol ve değiştirici artık alıştırmalarda mevcut.'
keep_practicing_mastery: 'Ustalık kazanmak için alıştırmaya devam edin — her tuş tam'
confidence_complete: 'güvene ulaştığında, tam klavye ustalığına erişmiş olacaksınız!'
all_mastered_msg: 'Tebrikler — tam klavye ustalığına ulaştınız!'
all_mastered_desc: 'Klavyedeki her tuş maksimum güven seviyesinde.'
mastery_takes_practice: 'Ustalık bir varış noktası değil — sürekli alıştırma gerektirir.'
keep_drilling: 'Avantajınızı korumak için alıştırmaya devam edin.'
hint_skill_tree_continue: '[t] Yetenek Ağacını Aç [Herhangi bir tuş] Devam'
hint_any_key: 'Devam etmek için herhangi bir tuşa basın'
input_blocked: 'Giriş geçici olarak engellendi (%{ms}ms kaldı)'
unlock_msg_1: 'Harika! Yazma becerilerinizi geliştirmeye devam edin.'
unlock_msg_2: 'Cephaneliğinize bir tuş daha eklendi!'
unlock_msg_3: 'Klavyeniz büyüyor! Böyle devam edin.'
unlock_msg_4: 'Tam klavye ustalığına bir adım daha yakın!'
mastery_msg_1: 'Bu tuş artık tam güven seviyesinde!'
mastery_msg_2: 'Bu tuşu tamamen kavradınız!'
mastery_msg_3: 'Kas hafızası kilitlendi!'
mastery_msg_4: 'Bir tuş daha fethedildi!'
# Klavye gezgini
keyboard:
title: ' Klavye '
subtitle: 'Bir tuşa basın veya tıklayın'
hint_navigate: '[←→↑↓/hjkl/Tab] Gezin'
hint_back: '[q/ESC] Geri'
key_label: 'Tuş: '
finger_label: 'Parmak: '
hand_left: 'Sol'
hand_right: 'Sağ'
finger_index: 'İşaret parmağı'
finger_middle: 'Orta parmak'
finger_ring: 'Yüzük parmağı'
finger_pinky: 'Serçe parmak'
finger_thumb: 'Başparmak'
overall_accuracy: ' Genel doğruluk: %{correct}/%{total} (%{pct}%%)'
ranked_accuracy: ' Sıralı doğruluk: %{correct}/%{total} (%{pct}%%)'
confidence: 'Güven: '
no_data: 'Henüz veri yok'
no_data_short: 'Veri yok'
key_details: ' Tuş Detayları '
key_details_char: ' Tuş Detayları: ''%{ch}'' '
key_details_name: ' Tuş Detayları: %{name} '
press_key_hint: 'Detayları görmek için bir tuşa basın'
shift_label: 'Shift: '
shift_no: 'Hayır'
overall_avg_time: 'Genel Ort. Süre: '
overall_best_time: 'Genel En İyi Süre: '
overall_samples: 'Genel Örnekler: '
overall_accuracy_label: 'Genel Doğruluk: '
branch_label: 'Dal: '
level_label: 'Seviye: '
built_in_key: 'Yerleşik Tuş'
unlocked_label: 'Açılmış: '
yes: 'Evet'
no: 'Hayır'
in_focus_label: 'Odakta mı?: '
mastery_label: 'Ustalık: '
mastery_locked: 'Kilitli'
ranked_avg_time: 'Sıralı Ort. Süre: '
ranked_best_time: 'Sıralı En İyi Süre: '
ranked_samples: 'Sıralı Örnekler: '
ranked_accuracy_label: 'Sıralı Doğruluk: '
# Giriş diyalogları
intro:
passage_title: ' Metin İndirme Ayarları '
code_title: ' Kod İndirme Ayarları '
enable_downloads: 'Ağ indirmelerini etkinleştir'
download_dir: 'İndirme klasörü'
paragraphs_per_book: 'Kitap başına paragraf (0 = tümü)'
whole_book: 'tüm kitap'
snippets_per_repo: 'Repo başına parçacık (0 = sınırsız)'
unlimited: 'sınırsız'
start_passage_drill: 'Metin alıştırmasını başlat'
start_code_drill: 'Kod alıştırmasını başlat'
confirm: 'Onayla'
hint_navigate: '[Yukarı/Aşağı] Gezin'
hint_adjust: '[Sol/Sağ] Ayarla'
hint_edit: '[Yazma/Backspace] Düzenle'
hint_confirm: '[Enter] Onayla'
hint_cancel: '[ESC] İptal'
preparing_download: 'İndirme hazırlanıyor...'
download_passage_title: ' Metin Kaynağı İndiriliyor '
download_code_title: ' Kod Kaynağı İndiriliyor '
book_label: ' Kitap: %{name}'
repo_label: ' Repo: %{name}'
progress_bytes: '[%{name}] %{downloaded}/%{total} bayt'
downloaded_bytes: 'İndirildi: %{bytes} bayt'
downloading_book_progress: 'Kitap indiriliyor: [%{bar}] %{downloaded}/%{total} bayt'
downloading_book_bytes: 'Kitap indiriliyor: %{bytes} bayt'
downloading_code_progress: 'İndiriliyor: [%{bar}] %{downloaded}/%{total} bayt'
downloading_code_bytes: 'İndiriliyor: %{bytes} bayt'
current_book: 'Mevcut: %{name} (kitap %{done}/%{total})'
current_repo: 'Mevcut: %{name} (repo %{done}/%{total})'
passage_instructions_1: 'keydr yazma alıştırması için Project Gutenberg''den pasajlar indirebilir.'
passage_instructions_2: 'Kitaplar bir kez indirilir ve yerel olarak saklanır.'
passage_instructions_3: 'Aşağıda indirme ayarlarını yapılandırın, ardından metin alıştırması başlatın.'
code_instructions_1: 'keydr yazma alıştırması için GitHub''dan açık kaynak kodu indirebilir.'
code_instructions_2: 'Kod bir kez indirilir ve yerel olarak saklanır.'
code_instructions_3: 'Aşağıda indirme ayarlarını yapılandırın, ardından kod alıştırması başlatın.'
# Durum mesajları (app.rs dosyasından)
status:
recovery_files: 'Yarıda kalan içe aktarmadan kurtarma dosyaları bulundu. Veriler tutarsız olabilir — yeniden içe aktarmayı düşünün.'
dir_not_exist: 'Klasör mevcut değil: %{path}'
no_data_store: 'Veri deposu mevcut değil'
serialization_error: 'Serileştirme hatası: %{error}'
exported_to: '%{path} konumuna dışa aktarıldı'
export_failed: 'Dışa aktarma başarısız: %{error}'
could_not_read: 'Dosya okunamadı: %{error}'
invalid_export: 'Geçersiz dışa aktarma dosyası: %{error}'
unsupported_version: 'Desteklenmeyen dışa aktarma sürümü: %{got} (beklenen %{expected})'
import_failed: 'İçe aktarma başarısız: %{error}'
imported_theme_fallback: 'Başarıyla içe aktarıldı (''%{theme}'' teması bulunamadı, varsayılan kullanılıyor)'
imported_success: 'Başarıyla içe aktarıldı'
adaptive_unavailable: 'Uyarlanır sıralı mod kullanılamıyor: %{error}'
switched_to: '%{name} moduna geçildi'
layout_changed: 'Düzen %{name} olarak değiştirildi'
# Hatalar (arayüz sınır çevirisi için)
errors:
unknown_language: 'Bilinmeyen dil: %{key}'
unknown_layout: 'Bilinmeyen klavye düzeni: %{key}'
unsupported_pair: 'Desteklenmeyen dil/düzen çifti: %{language} + %{layout}'
language_blocked: 'Dil destek seviyesi nedeniyle engellendi: %{key}'
# Ortak
common:
wpm: 'WPM'
cpm: 'CPM'
back: 'Geri'

View File

@@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""Convert raw.githubusercontent.com URLs in code_syntax.rs from branch refs to commit-SHA permalinks.
Usage:
# Dry-run (prints what would change):
python3 scripts/permalinkify_code_urls.py --dry-run
# Apply in-place:
python3 scripts/permalinkify_code_urls.py
# With a GitHub token for higher rate limits (recommended for 485 URLs):
GITHUB_TOKEN=ghp_xxx python3 scripts/permalinkify_code_urls.py
The script resolves each branch ref (master, main, dev, etc.) to the current
commit SHA via the GitHub API, then rewrites the URLs so they never change when
upstream repos push new commits or restructure files.
Before:
https://raw.githubusercontent.com/tokio-rs/tokio/master/tokio/src/sync/mutex.rs
After:
https://raw.githubusercontent.com/tokio-rs/tokio/a1b2c3d.../tokio/src/sync/mutex.rs
"""
import argparse
import json
import os
import re
import sys
import time
import urllib.error
import urllib.request
CODE_SYNTAX_PATH = os.path.join(
os.path.dirname(__file__), "..", "src", "generator", "code_syntax.rs"
)
# Looks like a full 40-char SHA already
SHA_RE = re.compile(r"^[0-9a-f]{40}$")
def github_headers():
token = os.environ.get("GITHUB_TOKEN")
headers = {"Accept": "application/vnd.github.v3+json"}
if token:
headers["Authorization"] = f"token {token}"
return headers
def _try_resolve_branch(owner: str, repo: str, ref: str) -> str | None:
"""Try to resolve a single branch name to its commit SHA."""
url = f"https://api.github.com/repos/{owner}/{repo}/git/ref/heads/{ref}"
req = urllib.request.Request(url, headers=github_headers())
try:
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
return data["object"]["sha"]
except urllib.error.HTTPError:
return None
def _try_resolve_tag(owner: str, repo: str, ref: str) -> str | None:
"""Try to resolve a tag name to its commit SHA."""
url = f"https://api.github.com/repos/{owner}/{repo}/git/ref/tags/{ref}"
req = urllib.request.Request(url, headers=github_headers())
try:
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
obj = data["object"]
if obj["type"] == "tag":
deref_url = obj["url"]
req2 = urllib.request.Request(deref_url, headers=github_headers())
with urllib.request.urlopen(req2, timeout=15) as resp2:
tag_data = json.loads(resp2.read())
return tag_data["object"]["sha"]
return obj["sha"]
except urllib.error.HTTPError:
return None
def resolve_ref_to_sha(owner: str, repo: str, ref: str) -> str | None:
"""Resolve a branch/tag ref to its commit SHA via the GitHub API.
Tries the ref as a branch first, then as a tag. If neither works and the
ref doesn't contain a slash, also tries common slash-prefixed variants
(e.g. "master" might actually be the first segment of "master/next").
"""
if SHA_RE.match(ref):
return ref
sha = _try_resolve_branch(owner, repo, ref)
if sha:
return sha
sha = _try_resolve_tag(owner, repo, ref)
if sha:
return sha
print(f" WARNING: could not resolve {owner}/{repo} ref={ref}", file=sys.stderr)
return None
def check_rate_limit():
"""Print current GitHub API rate limit status."""
req = urllib.request.Request(
"https://api.github.com/rate_limit", headers=github_headers()
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
core = data["resources"]["core"]
remaining = core["remaining"]
limit = core["limit"]
reset_ts = core["reset"]
reset_in = max(0, reset_ts - int(time.time()))
print(f"GitHub API rate limit: {remaining}/{limit} remaining, resets in {reset_in}s")
if remaining < 50:
print(
"WARNING: Low rate limit. Set GITHUB_TOKEN env var for 5000 req/hr.",
file=sys.stderr,
)
return remaining
except Exception as e:
print(f"Could not check rate limit: {e}", file=sys.stderr)
return None
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print changes without modifying the file",
)
parser.add_argument(
"--file",
default=CODE_SYNTAX_PATH,
help="Path to code_syntax.rs",
)
args = parser.parse_args()
with open(args.file) as f:
content = f.read()
# Collect unique (owner, repo, ref) tuples to minimize API calls.
# Branch names can contain slashes (e.g. "series/3.x"), so we can't simply
# split on "/" to extract the ref. Instead we use the GitHub API to look up
# the repo's default branch and resolve from there.
url_prefix_re = re.compile(
r"https://raw\.githubusercontent\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/(?P<rest>.+)"
)
urls_found = url_prefix_re.findall(content)
# Deduce (owner, repo, ref, path) — if `rest` starts with a 40-char hex SHA
# it's already pinned; otherwise ask the GitHub API for the default branch.
ref_keys: dict[tuple[str, str, str], str | None] = {}
for owner, repo, rest in urls_found:
first_segment = rest.split("/")[0]
if SHA_RE.match(first_segment):
ref_keys[(owner, repo, first_segment)] = first_segment
else:
# We need to figure out which part of `rest` is the ref vs the path.
# We try the first segment, then first two segments (for slash-branches
# like "series/3.x"), etc.
ref_key = (owner, repo, first_segment)
if ref_key not in ref_keys:
ref_keys[ref_key] = None
already_pinned = sum(1 for _, _, ref in ref_keys if SHA_RE.match(ref))
to_resolve = sum(1 for _, _, ref in ref_keys if not SHA_RE.match(ref))
print(f"Found {len(urls_found)} URLs across {len(ref_keys)} unique (owner/repo/ref) combos")
print(f" Already pinned to SHA: {already_pinned}")
print(f" Need resolution: {to_resolve}")
if to_resolve == 0:
print("Nothing to do — all URLs already use commit SHAs.")
return
remaining = check_rate_limit()
if remaining is not None and remaining < to_resolve:
print(
f"ERROR: Need {to_resolve} API calls but only {remaining} remaining. "
"Set GITHUB_TOKEN or wait for reset.",
file=sys.stderr,
)
sys.exit(1)
# Resolve each unique ref
resolved = 0
failed = 0
for (owner, repo, ref) in sorted(ref_keys):
if SHA_RE.match(ref):
ref_keys[(owner, repo, ref)] = ref
continue
sha = resolve_ref_to_sha(owner, repo, ref)
if sha:
ref_keys[(owner, repo, ref)] = sha
resolved += 1
if not args.dry_run:
# Be polite to the API
time.sleep(0.1)
else:
failed += 1
# Progress
done = resolved + failed
if done % 10 == 0 or done == to_resolve:
print(f" Progress: {done}/{to_resolve} ({resolved} resolved, {failed} failed)")
print(f"\nResolved {resolved}/{to_resolve} refs ({failed} failures)")
# Build replacement map
replacements = 0
new_content = content
for (owner, repo, ref), sha in ref_keys.items():
if sha and sha != ref:
old_prefix = f"raw.githubusercontent.com/{owner}/{repo}/{ref}/"
new_prefix = f"raw.githubusercontent.com/{owner}/{repo}/{sha}/"
count = new_content.count(old_prefix)
if count > 0:
if args.dry_run:
print(f" {owner}/{repo}: {ref} -> {sha[:12]}... ({count} URLs)")
new_content = new_content.replace(old_prefix, new_prefix)
replacements += count
print(f"\nTotal URL replacements: {replacements}")
if args.dry_run:
print("\n(dry-run mode — no file modified)")
else:
with open(args.file, "w") as f:
f.write(new_content)
print(f"Wrote {args.file}")
if __name__ == "__main__":
main()

2864
src/app.rs

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,828 @@
use std::collections::HashMap;
use std::fs;
use chrono::{DateTime, TimeZone, Utc};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use keydr::config::Config;
use keydr::engine::key_stats::{KeyStat, KeyStatsStore};
use keydr::engine::skill_tree::{
ALL_BRANCHES, BranchId, BranchProgress, BranchStatus, SkillTreeProgress,
};
use keydr::session::result::{DrillResult, KeyTime};
use keydr::store::schema::{
DrillHistoryData, EXPORT_VERSION, ExportData, KeyStatsData, ProfileData,
};
const SCHEMA_VERSION: u32 = 3;
const TARGET_CPM: f64 = 175.0;
// ── Helpers ──────────────────────────────────────────────────────────────
/// Generate a KeyStat with plausible jitter around a target confidence.
/// Uses seeded RNG for deterministic fixture output.
fn make_key_stat(rng: &mut SmallRng, confidence: f64, sample_count: usize) -> KeyStat {
let target_time_ms = 60000.0 / TARGET_CPM; // ~342.86 ms
let speed_jitter = rng.gen_range(0.92..1.08);
let filtered_time_ms = (target_time_ms / confidence) * speed_jitter;
let best_time_ms = filtered_time_ms * rng.gen_range(0.78..0.9);
// Generate recent_times: up to 30 entries near filtered_time_ms
let recent_count = sample_count.min(30);
let recent_times: Vec<f64> = (0..recent_count)
.map(|i| {
let trend = (i as f64 - recent_count as f64 / 2.0) * rng.gen_range(1.2..2.6);
let noise = rng.gen_range(-8.0..8.0);
(filtered_time_ms + trend + noise).max(best_time_ms)
})
.collect();
// Error rate scales inversely with confidence
let mut error_rate = if confidence >= 1.0 {
rng.gen_range(0.01..0.04)
} else {
(0.08 + (1.0 - confidence) * rng.gen_range(0.22..0.36)).min(0.48)
};
error_rate = error_rate.clamp(0.005, 0.6);
let error_count = (sample_count as f64 * error_rate * 0.5) as usize;
let total_count = sample_count + error_count;
KeyStat {
filtered_time_ms,
best_time_ms,
confidence,
sample_count,
recent_times,
error_count,
total_count,
error_rate_ema: error_rate,
}
}
/// Generate monotonic timestamps: base_date + day_offset days + drill_offset * 2min.
fn drill_timestamp(base: DateTime<Utc>, day: u32, drill_in_day: u32) -> DateTime<Utc> {
base + chrono::Duration::days(day as i64) + chrono::Duration::seconds(drill_in_day as i64 * 120)
}
/// Generate a DrillResult with deterministic per_key_times.
fn make_drill_result(
rng: &mut SmallRng,
wpm: f64,
accuracy: f64,
char_count: usize,
keys: &[char],
timestamp: DateTime<Utc>,
mode: &str,
ranked: bool,
) -> DrillResult {
let cpm = wpm * 5.0;
let target_error_rate = (1.0 - accuracy / 100.0).clamp(0.005, 0.2);
// Generate per_key_times with varied transitions for realistic n-gram data.
let per_key_times: Vec<KeyTime> = (0..char_count)
.map(|i| {
let key = keys[rng.gen_range(0..keys.len())];
let is_correct = !rng.gen_bool(target_error_rate);
let base_transition = 60000.0 / cpm;
let time_ms = if is_correct {
base_transition + rng.gen_range(-14.0..24.0) + (i as f64 % 5.0) * 1.2
} else {
base_transition + rng.gen_range(120.0..290.0) + (i as f64 % 5.0) * 8.0
};
KeyTime {
key,
time_ms: time_ms.max(25.0),
correct: is_correct,
}
})
.collect();
let incorrect = per_key_times.iter().filter(|kt| !kt.correct).count();
let correct = char_count - incorrect;
let elapsed_secs = (char_count as f64 / (cpm / 60.0)).max(1.0);
DrillResult {
wpm,
cpm,
accuracy,
correct,
incorrect,
total_chars: char_count,
elapsed_secs,
timestamp,
per_key_times,
drill_mode: mode.to_string(),
ranked,
partial: false,
completion_percent: 100.0,
}
}
fn make_skill_tree_progress(branches: Vec<(BranchId, BranchStatus, usize)>) -> SkillTreeProgress {
let mut map = HashMap::new();
for (id, status, level) in branches {
map.insert(
id.to_key().to_string(),
BranchProgress {
status,
current_level: level,
},
);
}
// Fill in any missing branches as Locked
for id in BranchId::all() {
map.entry(id.to_key().to_string())
.or_insert(BranchProgress {
status: BranchStatus::Locked,
current_level: 0,
});
}
SkillTreeProgress { branches: map }
}
/// Fixed exported_at timestamp for deterministic output.
fn fixed_export_timestamp() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap()
}
/// Canonical config with fixed paths for deterministic output across environments.
fn canonical_config() -> Config {
Config {
passage_download_dir: "/tmp/keydr/passages".to_string(),
code_download_dir: "/tmp/keydr/code".to_string(),
..Config::default()
}
}
fn make_export(
profile: ProfileData,
key_stats: KeyStatsStore,
ranked_key_stats: KeyStatsStore,
drill_history: Vec<DrillResult>,
) -> ExportData {
ExportData {
keydr_export_version: EXPORT_VERSION,
exported_at: fixed_export_timestamp(),
config: canonical_config(),
profile,
key_stats: KeyStatsData {
schema_version: SCHEMA_VERSION,
stats: key_stats,
},
ranked_key_stats: KeyStatsData {
schema_version: SCHEMA_VERSION,
stats: ranked_key_stats,
},
drill_history: DrillHistoryData {
schema_version: SCHEMA_VERSION,
drills: drill_history,
},
}
}
/// Get all keys for a branch up to (and including) level_index.
fn branch_keys_up_to(branch_id: BranchId, level_index: usize) -> Vec<char> {
let def = ALL_BRANCHES
.iter()
.find(|b| b.id == branch_id)
.expect("branch not found");
let mut keys = Vec::new();
for (i, level) in def.levels.iter().enumerate() {
if i <= level_index {
keys.extend_from_slice(level.keys);
}
}
keys
}
/// Get all keys for all levels of a branch.
fn branch_all_keys(branch_id: BranchId) -> Vec<char> {
let def = ALL_BRANCHES
.iter()
.find(|b| b.id == branch_id)
.expect("branch not found");
let mut keys = Vec::new();
for level in def.levels {
keys.extend_from_slice(level.keys);
}
keys
}
/// Lowercase keys: first `count` from frequency order.
fn lowercase_keys(count: usize) -> Vec<char> {
let def = ALL_BRANCHES
.iter()
.find(|b| b.id == BranchId::Lowercase)
.unwrap();
def.levels[0].keys[..count].to_vec()
}
/// Base date for all profiles.
fn base_date() -> DateTime<Utc> {
// Keep generated fixtures in the recent past so activity heatmaps that show
// only recent weeks still light up when importing test profiles.
let start_day = Utc::now().date_naive() - chrono::Duration::days(100);
Utc.from_utc_datetime(&start_day.and_hms_opt(8, 0, 0).unwrap())
}
/// Generate drill history spread across `streak_days` days.
fn generate_drills(
rng: &mut SmallRng,
total: usize,
streak_days: u32,
keys: &[char],
mode_distribution: &[(&str, bool, usize)], // (mode, ranked, count)
base_wpm: f64,
) -> Vec<DrillResult> {
let base = base_date();
let mut drills = Vec::new();
let mut drill_idx = 0usize;
for &(mode, ranked, count) in mode_distribution {
for i in 0..count {
let day = if streak_days > 0 {
(drill_idx as u32 * streak_days) / total as u32
} else {
0
};
let drill_in_day = drill_idx as u32 % 15; // max 15 drills per day spacing
let ts = drill_timestamp(base, day, drill_in_day);
// Vary WPM slightly by index
let wpm = (base_wpm + (i as f64 % 10.0) - 5.0 + rng.gen_range(-2.0..2.0)).max(12.0);
let accuracy = (91.5 + (i as f64 % 8.0) + rng.gen_range(-1.5..1.5)).clamp(86.0, 99.2);
let char_count = 80 + (i % 40) + rng.gen_range(0..12);
drills.push(make_drill_result(
rng, wpm, accuracy, char_count, keys, ts, mode, ranked,
));
drill_idx += 1;
}
}
// Sort by timestamp to ensure monotonic ordering
drills.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
drills
}
fn last_practice_date_from_drills(drills: &[DrillResult]) -> Option<String> {
drills
.last()
.map(|d| d.timestamp.format("%Y-%m-%d").to_string())
}
fn make_profile_data(
skill_tree: SkillTreeProgress,
total_score: f64,
total_drills: u32,
streak_days: u32,
best_streak: u32,
last_practice_date: Option<String>,
) -> ProfileData {
let mut skill_tree_by_language = HashMap::new();
skill_tree_by_language.insert("en".to_string(), skill_tree.clone());
ProfileData {
schema_version: SCHEMA_VERSION,
skill_tree,
skill_tree_by_language,
total_score,
total_drills,
streak_days,
best_streak,
last_practice_date,
}
}
// ── Profile Builders ─────────────────────────────────────────────────────
fn build_profile_01() -> ExportData {
let skill_tree =
make_skill_tree_progress(vec![(BranchId::Lowercase, BranchStatus::InProgress, 0)]);
make_export(
make_profile_data(skill_tree, 0.0, 0, 0, 0, None),
KeyStatsStore::default(),
KeyStatsStore::default(),
Vec::new(),
)
}
fn build_profile_02() -> ExportData {
// Lowercase InProgress level 4 => 6 + 4 = 10 keys: e,t,a,o,i,n,s,h,r,d
let skill_tree =
make_skill_tree_progress(vec![(BranchId::Lowercase, BranchStatus::InProgress, 4)]);
let all_keys = lowercase_keys(10);
let mastered_keys = &all_keys[..6]; // e,t,a,o,i,n
let partial_keys = &all_keys[6..]; // s,h,r,d
let mut rng = SmallRng::seed_from_u64(2002);
let mut stats = KeyStatsStore::default();
for &k in mastered_keys {
stats.stats.insert(k, make_key_stat(&mut rng, 1.2, 40));
}
let partial_confidences = [0.3, 0.5, 0.6, 0.7];
for (i, &k) in partial_keys.iter().enumerate() {
stats.stats.insert(
k,
make_key_stat(&mut rng, partial_confidences[i], 10 + i * 3),
);
}
let mut ranked_stats = KeyStatsStore::default();
for (&k, base) in &stats.stats {
let conf = if base.confidence >= 1.0 {
(base.confidence - rng.gen_range(0.0..0.18)).max(1.0)
} else {
(base.confidence + rng.gen_range(-0.1..0.08)).clamp(0.15, 0.95)
};
let sample_count =
((base.sample_count as f64) * rng.gen_range(0.5..0.8)).round() as usize + 6;
ranked_stats
.stats
.insert(k, make_key_stat(&mut rng, conf, sample_count));
}
let drills = generate_drills(
&mut rng,
15,
3,
&all_keys,
&[("adaptive", false, 11), ("adaptive", true, 4)],
25.0,
);
// total_score: level_from_score(x) = (x/100).sqrt() => for level 2: score ~400
make_export(
make_profile_data(
skill_tree,
350.0,
15,
3,
3,
last_practice_date_from_drills(&drills),
),
stats,
ranked_stats,
drills,
)
}
fn build_profile_03() -> ExportData {
// Lowercase InProgress level 12 => 6 + 12 = 18 keys through 'y'
let skill_tree =
make_skill_tree_progress(vec![(BranchId::Lowercase, BranchStatus::InProgress, 12)]);
let all_keys = lowercase_keys(18);
let mastered_keys = &all_keys[..14];
let partial_keys = &all_keys[14..]; // w,f,g,y
let mut rng = SmallRng::seed_from_u64(2003);
let mut stats = KeyStatsStore::default();
for &k in mastered_keys {
stats.stats.insert(k, make_key_stat(&mut rng, 1.3, 60));
}
let partial_confidences = [0.4, 0.6, 0.7, 0.8];
for (i, &k) in partial_keys.iter().enumerate() {
stats.stats.insert(
k,
make_key_stat(&mut rng, partial_confidences[i], 15 + i * 5),
);
}
let mut ranked_stats = KeyStatsStore::default();
for (&k, base) in &stats.stats {
let conf = if base.confidence >= 1.0 {
(base.confidence - rng.gen_range(0.0..0.2)).max(1.0)
} else {
(base.confidence + rng.gen_range(-0.12..0.1)).clamp(0.2, 0.95)
};
let sample_count =
((base.sample_count as f64) * rng.gen_range(0.52..0.82)).round() as usize + 8;
ranked_stats
.stats
.insert(k, make_key_stat(&mut rng, conf, sample_count));
}
let drills = generate_drills(
&mut rng,
50,
7,
&all_keys,
&[("adaptive", false, 35), ("adaptive", true, 15)],
30.0,
);
// level ~3: score ~900
make_export(
make_profile_data(
skill_tree,
900.0,
50,
7,
7,
last_practice_date_from_drills(&drills),
),
stats,
ranked_stats,
drills,
)
}
fn build_profile_03_near_lowercase_complete() -> ExportData {
// Lowercase InProgress level 19 => 6 + 19 = 25 keys unlocked.
// One unlocked key is just below mastery so a drill can trigger the final unlock popup.
let skill_tree =
make_skill_tree_progress(vec![(BranchId::Lowercase, BranchStatus::InProgress, 19)]);
let all_keys = lowercase_keys(25);
let mastered_keys = &all_keys[..24];
let near_mastery_key = all_keys[24];
let mut rng = SmallRng::seed_from_u64(2303);
let mut stats = KeyStatsStore::default();
for &k in mastered_keys {
stats.stats.insert(k, make_key_stat(&mut rng, 1.35, 75));
}
// Slightly below mastery, so one good drill can push over 1.0.
stats
.stats
.insert(near_mastery_key, make_key_stat(&mut rng, 0.97, 28));
let mut ranked_stats = KeyStatsStore::default();
for (&k, base) in &stats.stats {
let conf = if base.confidence >= 1.0 {
(base.confidence - rng.gen_range(0.0..0.2)).max(1.0)
} else {
(base.confidence + rng.gen_range(-0.08..0.06)).clamp(0.85, 0.99)
};
let sample_count =
((base.sample_count as f64) * rng.gen_range(0.5..0.8)).round() as usize + 8;
ranked_stats
.stats
.insert(k, make_key_stat(&mut rng, conf, sample_count));
}
let drills = generate_drills(
&mut rng,
90,
10,
&all_keys,
&[("adaptive", false, 62), ("adaptive", true, 28)],
34.0,
);
make_export(
make_profile_data(
skill_tree,
1800.0,
90,
10,
12,
last_practice_date_from_drills(&drills),
),
stats,
ranked_stats,
drills,
)
}
fn build_profile_04() -> ExportData {
// Lowercase Complete (level 20), all others Available
let skill_tree = make_skill_tree_progress(vec![
(BranchId::Lowercase, BranchStatus::Complete, 20),
(BranchId::Capitals, BranchStatus::Available, 0),
(BranchId::Numbers, BranchStatus::Available, 0),
(BranchId::ProsePunctuation, BranchStatus::Available, 0),
(BranchId::Whitespace, BranchStatus::Available, 0),
(BranchId::CodeSymbols, BranchStatus::Available, 0),
]);
let all_keys = lowercase_keys(26);
let mut rng = SmallRng::seed_from_u64(2004);
let mut stats = KeyStatsStore::default();
for &k in &all_keys {
stats.stats.insert(k, make_key_stat(&mut rng, 1.4, 80));
}
let mut ranked_stats = KeyStatsStore::default();
for (&k, base) in &stats.stats {
let conf = (base.confidence - rng.gen_range(0.0..0.2)).max(1.0);
let sample_count =
((base.sample_count as f64) * rng.gen_range(0.55..0.85)).round() as usize + 10;
ranked_stats
.stats
.insert(k, make_key_stat(&mut rng, conf, sample_count));
}
let drills = generate_drills(
&mut rng,
100,
14,
&all_keys,
&[("adaptive", false, 70), ("adaptive", true, 30)],
35.0,
);
// level ~5: score ~2500
make_export(
make_profile_data(
skill_tree,
2500.0,
100,
14,
14,
last_practice_date_from_drills(&drills),
),
stats,
ranked_stats,
drills,
)
}
fn build_profile_05() -> ExportData {
// Multiple branches in progress
let skill_tree = make_skill_tree_progress(vec![
(BranchId::Lowercase, BranchStatus::Complete, 20),
(BranchId::Capitals, BranchStatus::InProgress, 1),
(BranchId::Numbers, BranchStatus::InProgress, 0),
(BranchId::ProsePunctuation, BranchStatus::InProgress, 0),
(BranchId::Whitespace, BranchStatus::Available, 0),
(BranchId::CodeSymbols, BranchStatus::Available, 0),
]);
let mut rng = SmallRng::seed_from_u64(2005);
let mut stats = KeyStatsStore::default();
// All lowercase mastered
for &k in &lowercase_keys(26) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.5, 100));
}
// Capitals L1 mastered: T,I,A,S,W,H,B,M
for &k in &['T', 'I', 'A', 'S', 'W', 'H', 'B', 'M'] {
stats.stats.insert(k, make_key_stat(&mut rng, 1.2, 50));
}
// Capitals L2 partial: J,D,R,C,E
let cap_partial = [('J', 0.4), ('D', 0.5), ('R', 0.6), ('C', 0.3), ('E', 0.7)];
for &(k, conf) in &cap_partial {
stats.stats.insert(k, make_key_stat(&mut rng, conf, 15));
}
// Numbers L1 partial: 1,2,3
let num_partial = [('1', 0.4), ('2', 0.5), ('3', 0.3)];
for &(k, conf) in &num_partial {
stats.stats.insert(k, make_key_stat(&mut rng, conf, 12));
}
// Prose punctuation L1 partial: . , '
let punct_partial = [('.', 0.5), (',', 0.4), ('\'', 0.3)];
for &(k, conf) in &punct_partial {
stats.stats.insert(k, make_key_stat(&mut rng, conf, 10));
}
// Build all unlocked keys for drill history
let mut all_unlocked: Vec<char> = lowercase_keys(26);
all_unlocked.extend(branch_keys_up_to(BranchId::Capitals, 1));
all_unlocked.extend(branch_keys_up_to(BranchId::Numbers, 0));
all_unlocked.extend(branch_keys_up_to(BranchId::ProsePunctuation, 0));
let drills = generate_drills(
&mut rng,
200,
21,
&all_unlocked,
&[
("adaptive", false, 170),
("passage", false, 10),
("adaptive", true, 20),
],
40.0,
);
// Ranked key stats: cover all keys used in ranked drills (all_unlocked)
let mut ranked_stats = KeyStatsStore::default();
for &k in &all_unlocked {
ranked_stats
.stats
.insert(k, make_key_stat(&mut rng, 1.1, 20));
}
// level ~7: score ~5000
make_export(
make_profile_data(
skill_tree,
5000.0,
200,
21,
21,
last_practice_date_from_drills(&drills),
),
stats,
ranked_stats,
drills,
)
}
fn build_profile_06() -> ExportData {
// Most branches complete, Code Symbols InProgress level 2
let skill_tree = make_skill_tree_progress(vec![
(BranchId::Lowercase, BranchStatus::Complete, 20),
(BranchId::Capitals, BranchStatus::Complete, 3),
(BranchId::Numbers, BranchStatus::Complete, 2),
(BranchId::ProsePunctuation, BranchStatus::Complete, 3),
(BranchId::Whitespace, BranchStatus::Complete, 2),
(BranchId::CodeSymbols, BranchStatus::InProgress, 2),
]);
let mut rng = SmallRng::seed_from_u64(2006);
let mut stats = KeyStatsStore::default();
// All lowercase mastered
for &k in &lowercase_keys(26) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.6, 200));
}
// All capitals mastered
for &k in &branch_all_keys(BranchId::Capitals) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.4, 120));
}
// All numbers mastered
for &k in &branch_all_keys(BranchId::Numbers) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.3, 100));
}
// All prose punctuation mastered
for &k in &branch_all_keys(BranchId::ProsePunctuation) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.3, 90));
}
// All whitespace mastered
for &k in &branch_all_keys(BranchId::Whitespace) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.2, 80));
}
// Code Symbols L1 + L2 mastered
for &k in &branch_keys_up_to(BranchId::CodeSymbols, 1) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.2, 60));
}
// Code Symbols L3 partial: &,|,^,~
// Note: '!' is shared with ProsePunctuation L3 (Complete), so it must be mastered
let code_partial = [('&', 0.4), ('|', 0.5), ('^', 0.3), ('~', 0.4)];
for &(k, conf) in &code_partial {
stats.stats.insert(k, make_key_stat(&mut rng, conf, 15));
}
// '!' is mastered (shared with completed ProsePunctuation)
stats.stats.insert('!', make_key_stat(&mut rng, 1.2, 60));
// All unlocked keys for drills
let mut all_unlocked: Vec<char> = lowercase_keys(26);
all_unlocked.extend(branch_all_keys(BranchId::Capitals));
all_unlocked.extend(branch_all_keys(BranchId::Numbers));
all_unlocked.extend(branch_all_keys(BranchId::ProsePunctuation));
all_unlocked.extend(branch_all_keys(BranchId::Whitespace));
all_unlocked.extend(branch_keys_up_to(BranchId::CodeSymbols, 2));
let drills = generate_drills(
&mut rng,
500,
45,
&all_unlocked,
&[
("adaptive", false, 350),
("passage", false, 50),
("code", false, 50),
("adaptive", true, 50),
],
50.0,
);
// Ranked key stats: cover all keys used in ranked drills (all_unlocked)
let mut ranked_stats = KeyStatsStore::default();
for &k in &all_unlocked {
ranked_stats
.stats
.insert(k, make_key_stat(&mut rng, 1.1, 30));
}
// level ~12: score ~15000
make_export(
make_profile_data(
skill_tree,
15000.0,
500,
45,
60,
last_practice_date_from_drills(&drills),
),
stats,
ranked_stats,
drills,
)
}
fn build_profile_07() -> ExportData {
// Everything complete
let skill_tree = make_skill_tree_progress(vec![
(BranchId::Lowercase, BranchStatus::Complete, 20),
(BranchId::Capitals, BranchStatus::Complete, 3),
(BranchId::Numbers, BranchStatus::Complete, 2),
(BranchId::ProsePunctuation, BranchStatus::Complete, 3),
(BranchId::Whitespace, BranchStatus::Complete, 2),
(BranchId::CodeSymbols, BranchStatus::Complete, 4),
]);
let mut rng = SmallRng::seed_from_u64(2007);
let mut stats = KeyStatsStore::default();
// All keys mastered with high sample counts
for &k in &lowercase_keys(26) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.8, 400));
}
for &k in &branch_all_keys(BranchId::Capitals) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.5, 200));
}
for &k in &branch_all_keys(BranchId::Numbers) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.4, 180));
}
for &k in &branch_all_keys(BranchId::ProsePunctuation) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.4, 160));
}
for &k in &branch_all_keys(BranchId::Whitespace) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.3, 140));
}
for &k in &branch_all_keys(BranchId::CodeSymbols) {
stats.stats.insert(k, make_key_stat(&mut rng, 1.3, 120));
}
// All keys for drills
let mut all_keys: Vec<char> = lowercase_keys(26);
all_keys.extend(branch_all_keys(BranchId::Capitals));
all_keys.extend(branch_all_keys(BranchId::Numbers));
all_keys.extend(branch_all_keys(BranchId::ProsePunctuation));
all_keys.extend(branch_all_keys(BranchId::Whitespace));
all_keys.extend(branch_all_keys(BranchId::CodeSymbols));
let drills = generate_drills(
&mut rng,
800,
90,
&all_keys,
&[
("adaptive", false, 400),
("passage", false, 150),
("code", false, 150),
("adaptive", true, 100),
],
60.0,
);
// Full ranked stats
let mut ranked_stats = KeyStatsStore::default();
for &k in &all_keys {
ranked_stats
.stats
.insert(k, make_key_stat(&mut rng, 1.4, 80));
}
// level ~18: score ~35000
make_export(
make_profile_data(
skill_tree,
35000.0,
800,
90,
90,
last_practice_date_from_drills(&drills),
),
stats,
ranked_stats,
drills,
)
}
// ── Main ─────────────────────────────────────────────────────────────────
fn main() {
fs::create_dir_all("test-profiles").unwrap();
let profiles: Vec<(&str, ExportData)> = vec![
("01-brand-new", build_profile_01()),
("02-early-lowercase", build_profile_02()),
("03-mid-lowercase", build_profile_03()),
(
"03-near-lowercase-complete",
build_profile_03_near_lowercase_complete(),
),
("04-lowercase-complete", build_profile_04()),
("05-multi-branch", build_profile_05()),
("06-advanced", build_profile_06()),
("07-fully-complete", build_profile_07()),
];
for (name, data) in &profiles {
let json = serde_json::to_string_pretty(data).unwrap();
let path = format!("test-profiles/{name}.json");
fs::write(&path, &json).unwrap();
println!("Wrote {path} ({} bytes)", json.len());
}
println!("\nGenerated {} test profiles.", profiles.len());
}

View File

@@ -1,6 +1,12 @@
use std::fs;
use std::path::PathBuf;
use crate::i18n;
use crate::keyboard::model::KeyboardModel;
use crate::l10n::language_pack::{
LanguageLayoutValidationError, dictionary_languages_for_layout, supported_dictionary_languages,
validate_language_layout_pair,
};
use anyhow::Result;
use serde::{Deserialize, Serialize};
@@ -16,6 +22,8 @@ pub struct Config {
pub word_count: usize,
#[serde(default = "default_code_language")]
pub code_language: String,
#[serde(default = "default_dictionary_language")]
pub dictionary_language: String,
#[serde(default = "default_passage_book")]
pub passage_book: String,
#[serde(default = "default_passage_downloads_enabled")]
@@ -34,6 +42,8 @@ pub struct Config {
pub code_snippets_per_repo: usize,
#[serde(default = "default_code_onboarding_done")]
pub code_onboarding_done: bool,
#[serde(default = "default_ui_language")]
pub ui_language: String,
}
fn default_target_wpm() -> u32 {
@@ -51,6 +61,9 @@ fn default_word_count() -> usize {
fn default_code_language() -> String {
"rust".to_string()
}
fn default_dictionary_language() -> String {
"en".to_string()
}
fn default_passage_book() -> String {
"all".to_string()
}
@@ -88,6 +101,9 @@ fn default_code_snippets_per_repo() -> usize {
fn default_code_onboarding_done() -> bool {
false
}
fn default_ui_language() -> String {
"en".to_string()
}
impl Default for Config {
fn default() -> Self {
@@ -97,6 +113,7 @@ impl Default for Config {
keyboard_layout: default_keyboard_layout(),
word_count: default_word_count(),
code_language: default_code_language(),
dictionary_language: default_dictionary_language(),
passage_book: default_passage_book(),
passage_downloads_enabled: default_passage_downloads_enabled(),
passage_download_dir: default_passage_download_dir(),
@@ -106,6 +123,7 @@ impl Default for Config {
code_download_dir: default_code_download_dir(),
code_snippets_per_repo: default_code_snippets_per_repo(),
code_onboarding_done: default_code_onboarding_done(),
ui_language: default_ui_language(),
}
}
}
@@ -122,7 +140,6 @@ impl Config {
}
}
#[allow(dead_code)]
pub fn save(&self) -> Result<()> {
let path = Self::config_path();
if let Some(parent) = path.parent() {
@@ -144,9 +161,21 @@ impl Config {
self.target_wpm as f64 * 5.0
}
/// Clamp and normalize all config values to valid ranges.
/// Call after importing config from an external source.
pub fn validate(&mut self, valid_language_keys: &[&str]) {
self.target_wpm = self.target_wpm.clamp(10, 200);
self.word_count = self.word_count.clamp(5, 100);
self.normalize_code_language(valid_language_keys);
self.normalize_keyboard_layout();
self.normalize_dictionary_language();
self.normalize_language_layout_pair();
self.normalize_ui_language();
}
/// Validate `code_language` against known options, resetting to default if invalid.
/// Call after deserialization to handle stale/renamed keys from old configs.
pub fn normalize_code_language(&mut self, valid_keys: &[&str]) {
fn normalize_code_language(&mut self, valid_keys: &[&str]) {
// Backwards compatibility: old "shell" key is now "bash".
if self.code_language == "shell" {
self.code_language = "bash".to_string();
@@ -155,6 +184,55 @@ impl Config {
self.code_language = default_code_language();
}
}
/// Validate `dictionary_language` against supported keys.
fn normalize_dictionary_language(&mut self) {
if !supported_dictionary_languages().contains(&self.dictionary_language.as_str()) {
self.dictionary_language = default_dictionary_language();
}
}
/// Validate `keyboard_layout` against canonical profile keys.
fn normalize_keyboard_layout(&mut self) {
if !KeyboardModel::supported_layout_keys().contains(&self.keyboard_layout.as_str()) {
self.keyboard_layout = default_keyboard_layout();
}
}
/// Ensure the language/layout combination is explicitly supported.
fn normalize_language_layout_pair(&mut self) {
match self.validate_language_layout_pair() {
Ok(()) => {}
Err(LanguageLayoutValidationError::UnknownLanguage(_))
| Err(LanguageLayoutValidationError::LanguageBlockedBySupportLevel(_)) => {
self.dictionary_language = default_dictionary_language();
}
Err(LanguageLayoutValidationError::UnknownLayout(_)) => {
self.keyboard_layout = default_keyboard_layout();
}
Err(LanguageLayoutValidationError::UnsupportedLanguageLayoutPair { .. }) => {
if let Some(first_supported) =
dictionary_languages_for_layout(&self.keyboard_layout).first()
{
self.dictionary_language = (*first_supported).to_string();
} else {
self.keyboard_layout = default_keyboard_layout();
self.dictionary_language = default_dictionary_language();
}
}
}
}
/// Validate `ui_language` against supported UI locales.
fn normalize_ui_language(&mut self) {
if !i18n::SUPPORTED_UI_LOCALES.contains(&self.ui_language.as_str()) {
self.ui_language = default_ui_language();
}
}
pub fn validate_language_layout_pair(&self) -> Result<(), LanguageLayoutValidationError> {
validate_language_layout_pair(&self.dictionary_language, &self.keyboard_layout).map(|_| ())
}
}
#[cfg(test)]
@@ -168,6 +246,7 @@ mod tests {
assert_eq!(config.code_downloads_enabled, false);
assert_eq!(config.code_snippets_per_repo, 200);
assert_eq!(config.code_onboarding_done, false);
assert_eq!(config.dictionary_language, "en");
assert!(!config.code_download_dir.is_empty());
assert!(config.code_download_dir.contains("code"));
}
@@ -184,6 +263,7 @@ code_language = "go"
assert_eq!(config.target_wpm, 60);
assert_eq!(config.theme, "monokai");
assert_eq!(config.code_language, "go");
assert_eq!(config.dictionary_language, "en");
// New fields should have defaults
assert_eq!(config.code_downloads_enabled, false);
assert_eq!(config.code_snippets_per_repo, 200);
@@ -195,10 +275,20 @@ code_language = "go"
let config = Config::default();
let serialized = toml::to_string_pretty(&config).unwrap();
let deserialized: Config = toml::from_str(&serialized).unwrap();
assert_eq!(config.code_downloads_enabled, deserialized.code_downloads_enabled);
assert_eq!(
config.code_downloads_enabled,
deserialized.code_downloads_enabled
);
assert_eq!(config.code_download_dir, deserialized.code_download_dir);
assert_eq!(config.code_snippets_per_repo, deserialized.code_snippets_per_repo);
assert_eq!(config.code_onboarding_done, deserialized.code_onboarding_done);
assert_eq!(
config.code_snippets_per_repo,
deserialized.code_snippets_per_repo
);
assert_eq!(
config.code_onboarding_done,
deserialized.code_onboarding_done
);
assert_eq!(config.dictionary_language, deserialized.dictionary_language);
}
#[test]
@@ -236,4 +326,49 @@ code_language = "go"
config.normalize_code_language(&valid_keys);
assert_eq!(config.code_language, "bash");
}
#[test]
fn test_normalize_dictionary_language_invalid_key_resets() {
let mut config = Config::default();
config.dictionary_language = "zz".to_string();
config.normalize_dictionary_language();
assert_eq!(config.dictionary_language, "en");
}
#[test]
fn test_normalize_keyboard_layout_invalid_key_resets() {
let mut config = Config::default();
config.keyboard_layout = "foo".to_string();
config.normalize_keyboard_layout();
assert_eq!(config.keyboard_layout, "qwerty");
}
#[test]
fn test_normalize_language_layout_pair_keeps_valid_cross_language_pair() {
let mut config = Config::default();
config.dictionary_language = "de".to_string();
config.keyboard_layout = "dvorak".to_string();
config.normalize_language_layout_pair();
// Cross-language/layout pairs are now valid
assert_eq!(config.dictionary_language, "de");
assert_eq!(config.keyboard_layout, "dvorak");
}
#[test]
fn test_validate_language_layout_pair_accepts_cross_language_pair() {
let mut config = Config::default();
config.dictionary_language = "de".to_string();
config.keyboard_layout = "dvorak".to_string();
assert!(config.validate_language_layout_pair().is_ok());
}
#[test]
fn test_normalize_language_layout_pair_unknown_language_resets_language_only() {
let mut config = Config::default();
config.dictionary_language = "zz".to_string();
config.keyboard_layout = "qwerty".to_string();
config.normalize_language_layout_pair();
assert_eq!(config.dictionary_language, "en");
assert_eq!(config.keyboard_layout, "qwerty");
}
}

View File

@@ -11,6 +11,16 @@ pub struct KeyStat {
pub confidence: f64,
pub sample_count: usize,
pub recent_times: Vec<f64>,
#[serde(default)]
pub error_count: usize,
#[serde(default)]
pub total_count: usize,
#[serde(default = "default_error_rate_ema")]
pub error_rate_ema: f64,
}
fn default_error_rate_ema() -> f64 {
0.5
}
impl Default for KeyStat {
@@ -21,6 +31,9 @@ impl Default for KeyStat {
confidence: 0.0,
sample_count: 0,
recent_times: Vec::new(),
error_count: 0,
total_count: 0,
error_rate_ema: 0.5,
}
}
}
@@ -44,6 +57,7 @@ impl KeyStatsStore {
pub fn update_key(&mut self, key: char, time_ms: f64) {
let stat = self.stats.entry(key).or_default();
stat.sample_count += 1;
stat.total_count += 1;
if stat.sample_count == 1 {
stat.filtered_time_ms = time_ms;
@@ -60,6 +74,13 @@ impl KeyStatsStore {
if stat.recent_times.len() > 30 {
stat.recent_times.remove(0);
}
// Update error rate EMA (correct stroke = 0.0 signal)
if stat.total_count == 1 {
stat.error_rate_ema = 0.0;
} else {
stat.error_rate_ema = EMA_ALPHA * 0.0 + (1.0 - EMA_ALPHA) * stat.error_rate_ema;
}
}
pub fn get_confidence(&self, key: char) -> f64 {
@@ -70,6 +91,29 @@ impl KeyStatsStore {
pub fn get_stat(&self, key: char) -> Option<&KeyStat> {
self.stats.get(&key)
}
/// Record an error for a key (increments error_count and total_count).
/// Does NOT update timing/confidence (those are only updated for correct strokes).
pub fn update_key_error(&mut self, key: char) {
let stat = self.stats.entry(key).or_default();
stat.error_count += 1;
stat.total_count += 1;
// Update error rate EMA (error stroke = 1.0 signal)
if stat.total_count == 1 {
stat.error_rate_ema = 1.0;
} else {
stat.error_rate_ema = EMA_ALPHA * 1.0 + (1.0 - EMA_ALPHA) * stat.error_rate_ema;
}
}
/// EMA-based error rate for a key.
pub fn smoothed_error_rate(&self, key: char) -> f64 {
match self.stats.get(&key) {
Some(s) => s.error_rate_ema,
None => 0.5,
}
}
}
#[cfg(test)]
@@ -119,4 +163,50 @@ mod tests {
"confidence should be < 1.0 for slow typing, got {conf}"
);
}
#[test]
fn test_ema_error_rate_correct_strokes() {
let mut store = KeyStatsStore::default();
// All correct strokes → EMA should be 0.0 for first, stay near 0
store.update_key('a', 200.0);
assert!((store.smoothed_error_rate('a') - 0.0).abs() < f64::EPSILON);
for _ in 0..10 {
store.update_key('a', 200.0);
}
assert!(
store.smoothed_error_rate('a') < 0.01,
"All correct → EMA near 0"
);
}
#[test]
fn test_ema_error_rate_error_strokes() {
let mut store = KeyStatsStore::default();
// First stroke is error
store.update_key_error('b');
assert!((store.smoothed_error_rate('b') - 1.0).abs() < f64::EPSILON);
// Follow with correct strokes → EMA decays
for _ in 0..20 {
store.update_key('b', 200.0);
}
let rate = store.smoothed_error_rate('b');
assert!(
rate < 0.15,
"After 20 correct, EMA should be < 0.15, got {rate}"
);
}
#[test]
fn test_ema_error_rate_default_for_missing_key() {
let store = KeyStatsStore::default();
assert!((store.smoothed_error_rate('z') - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_ema_error_rate_serde_default() {
// Verify backward compat: deserializing old data without error_rate_ema gets 0.5
let json = r#"{"filtered_time_ms":200.0,"best_time_ms":200.0,"confidence":1.0,"sample_count":10,"recent_times":[],"error_count":2,"total_count":10}"#;
let stat: KeyStat = serde_json::from_str(json).unwrap();
assert!((stat.error_rate_ema - 0.5).abs() < f64::EPSILON);
}
}

View File

@@ -1,5 +1,8 @@
pub mod filter;
pub mod key_stats;
pub mod learning_rate;
pub mod ngram_stats;
pub mod scoring;
pub mod skill_tree;
pub use ngram_stats::FocusSelection;

1638
src/engine/ngram_stats.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,11 @@ use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use crossterm::event::{self, Event, KeyEvent};
use crossterm::event::{self, Event, KeyEvent, MouseEvent};
pub enum AppEvent {
Key(KeyEvent),
Mouse(MouseEvent),
Tick,
Resize(#[allow(dead_code)] u16, #[allow(dead_code)] u16),
}
@@ -34,6 +35,11 @@ impl EventHandler {
return;
}
}
Ok(Event::Mouse(mouse)) => {
if tx.send(AppEvent::Mouse(mouse)).is_err() {
return;
}
}
_ => {}
}
} else if tx.send(AppEvent::Tick).is_err() {

View File

@@ -1,6 +1,10 @@
use rand::Rng;
use rand::rngs::SmallRng;
fn lowercase_eq(a: char, b: char) -> bool {
a.to_lowercase().eq(b.to_lowercase())
}
/// Post-processing pass that capitalizes words in generated text.
/// Only capitalizes using letters from `unlocked_capitals`.
pub fn apply_capitalization(
@@ -13,58 +17,230 @@ pub fn apply_capitalization(
return text.to_string();
}
// If focused key is an uppercase letter, boost its probability
let focused_upper = focused.filter(|ch| ch.is_ascii_uppercase());
let focused_upper = focused.filter(|ch| ch.is_uppercase());
let mut words: Vec<String> = text.split_whitespace().map(|w| w.to_string()).collect();
if words.is_empty() {
return text.to_string();
}
let mut result = String::with_capacity(text.len());
// Prefer capitals at starts of words (sentence starts always when possible).
let mut at_sentence_start = true;
for (i, ch) in text.chars().enumerate() {
if at_sentence_start && ch.is_ascii_lowercase() {
let upper = ch.to_ascii_uppercase();
for i in 0..words.len() {
if let Some(upper) = word_start_upper(&words[i]) {
if unlocked_capitals.contains(&upper) {
result.push(upper);
at_sentence_start = false;
let should_cap = if at_sentence_start {
true
} else if focused_upper == Some(upper) {
rng.gen_bool(0.55)
} else {
rng.gen_bool(0.22)
};
if should_cap {
capitalize_word_start(&mut words[i]);
}
}
}
at_sentence_start = ends_sentence(&words[i]);
}
// Occasional mid-word capitals are injected as camelCase joins only.
// This keeps internal capitals realistic for code contexts.
let mut i = 0;
while i + 1 < words.len() {
if ends_sentence(&words[i]) {
i += 1;
continue;
}
let next_upper = match word_start_upper(&words[i + 1]) {
Some(upper) if unlocked_capitals.contains(&upper) => upper,
_ => {
i += 1;
continue;
}
// After period/question/exclamation + space, next word starts a sentence
if ch == ' ' && i > 0 {
let prev = text.as_bytes().get(i - 1).map(|&b| b as char);
if matches!(prev, Some('.' | '?' | '!')) {
at_sentence_start = true;
}
}
// Capitalize word starts: boosted for focused key, ~12% for others
if ch.is_ascii_lowercase() && !at_sentence_start {
let is_word_start =
i == 0 || text.as_bytes().get(i - 1).map(|&b| b as char) == Some(' ');
if is_word_start {
let upper = ch.to_ascii_uppercase();
if unlocked_capitals.contains(&upper) {
let prob = if focused_upper == Some(upper) {
0.40
};
let prob = if focused_upper == Some(next_upper) {
0.35
} else {
0.12
0.09
};
if rng.gen_bool(prob) {
result.push(upper);
capitalize_word_start(&mut words[i + 1]);
let next = words.remove(i + 1);
words[i].push_str(&next);
} else {
i += 1;
}
}
// Focused capitals should show up multiple times for focused drills.
if let Some(focused_upper) = focused_upper.filter(|ch| unlocked_capitals.contains(ch)) {
let alpha_words = words
.iter()
.filter(|w| w.chars().any(|ch| ch.is_alphabetic()))
.count();
let min_focused = alpha_words.min(4);
ensure_min_focused_occurrences(&mut words, focused_upper, min_focused);
}
// Keep a baseline capital density so branch/global drills with capitals
// unlocked do not feel too sparse.
let min_total_caps = words.len().clamp(3, 6) / 2; // ~3 for 6+ words
ensure_min_total_capitals(&mut words, unlocked_capitals, min_total_caps, rng);
words.join(" ")
}
fn word_start_upper(word: &str) -> Option<char> {
word.chars()
.find(|ch| ch.is_alphabetic())
.and_then(|ch| ch.to_uppercase().next())
}
fn capitalize_word_start(word: &mut String) -> Option<char> {
let mut chars: Vec<char> = word.chars().collect();
for i in 0..chars.len() {
if chars[i].is_lowercase() {
chars[i] = chars[i].to_uppercase().next().unwrap_or(chars[i]);
let upper = chars[i];
*word = chars.into_iter().collect();
return Some(upper);
}
if chars[i].is_uppercase() {
return Some(chars[i]);
}
}
None
}
fn ends_sentence(word: &str) -> bool {
word.chars()
.rev()
.find(|ch| !ch.is_whitespace())
.is_some_and(|ch| matches!(ch, '.' | '?' | '!'))
}
fn word_starts_with_lower(word: &str, lower: char) -> bool {
word.chars()
.find(|ch| ch.is_alphabetic())
.is_some_and(|ch| lowercase_eq(ch, lower))
}
fn force_word_start_to_upper(word: &mut String, upper: char) -> bool {
let mut chars: Vec<char> = word.chars().collect();
for i in 0..chars.len() {
if chars[i].is_alphabetic() {
if chars[i] == upper {
return false;
}
chars[i] = upper;
*word = chars.into_iter().collect();
return true;
}
}
false
}
fn ensure_min_focused_occurrences(words: &mut Vec<String>, focused_upper: char, min_count: usize) {
let focused_lower = focused_upper.to_lowercase().next().unwrap_or(focused_upper);
let mut count = words
.iter()
.map(|w| w.chars().filter(|&ch| ch == focused_upper).count())
.sum::<usize>();
if count >= min_count {
return;
}
// First, capitalize focused matching word starts.
for word in words.iter_mut() {
if count >= min_count {
break;
}
if !word_starts_with_lower(word, focused_lower) {
continue;
}
if capitalize_word_start(word) == Some(focused_upper) {
count += 1;
}
}
// If still short, create camelCase joins where the second word starts
// with the focused letter.
let mut i = 0;
while i + 1 < words.len() {
if count >= min_count {
break;
}
if ends_sentence(&words[i]) {
i += 1;
continue;
}
let next_starts_focused = words[i + 1]
.chars()
.find(|ch| ch.is_alphabetic())
.is_some_and(|ch| lowercase_eq(ch, focused_lower));
if next_starts_focused {
capitalize_word_start(&mut words[i + 1]);
let next = words.remove(i + 1);
words[i].push_str(&next);
count += 1;
} else {
i += 1;
}
}
// Last resort: force focused uppercase at word starts.
for word in words.iter_mut() {
if count >= min_count {
break;
}
if force_word_start_to_upper(word, focused_upper) {
count += 1;
}
}
}
fn ensure_min_total_capitals(
words: &mut [String],
unlocked_capitals: &[char],
min_count: usize,
rng: &mut SmallRng,
) {
let mut count = words
.iter()
.map(|w| w.chars().filter(|ch| ch.is_uppercase()).count())
.sum::<usize>();
if count >= min_count || unlocked_capitals.is_empty() {
return;
}
// Prefer natural capitalization when the word already starts with an unlocked letter.
for word in words.iter_mut() {
if count >= min_count {
break;
}
let Some(upper) = word_start_upper(word) else {
continue;
};
if unlocked_capitals.contains(&upper)
&& word_starts_with_lower(word, upper.to_lowercase().next().unwrap_or(upper))
{
if capitalize_word_start(word) == Some(upper) {
count += 1;
}
}
}
if ch != '.' && ch != '?' && ch != '!' {
at_sentence_start = false;
// If still short, force additional capitalized starts from unlocked set.
for word in words.iter_mut() {
if count >= min_count {
break;
}
let upper = unlocked_capitals[rng.gen_range(0..unlocked_capitals.len())];
if force_word_start_to_upper(word, upper) {
count += 1;
}
result.push(ch);
}
result
}
#[cfg(test)]
@@ -125,4 +301,40 @@ mod tests {
"Focused W count ({focused_count}) should exceed unfocused ({unfocused_count})"
);
}
#[test]
fn test_focused_capital_has_minimum_presence_when_available() {
let mut rng = SmallRng::seed_from_u64(123);
let text = "we will work with weird words while we wait";
let result = apply_capitalization(text, &['W'], Some('W'), &mut rng);
let focused_count = result.chars().filter(|&ch| ch == 'W').count();
assert!(
focused_count >= 3,
"Expected at least 3 focused capitals, got {focused_count} in: {result}"
);
}
#[test]
fn test_no_interior_focus_caps_without_word_start_or_camel_case_opportunity() {
let mut rng = SmallRng::seed_from_u64(7);
let text = "awful claw draw";
let result = apply_capitalization(text, &['W'], Some('W'), &mut rng);
assert!(result.starts_with('W') || result.contains(" W"));
assert!(
!result.contains("aW"),
"Should avoid interior non-camel W: {result}"
);
}
#[test]
fn test_focused_capital_forced_to_multiple_occurrences() {
let mut rng = SmallRng::seed_from_u64(11);
let text = "alpha beta gamma delta epsilon zeta eta theta iota";
let result = apply_capitalization(text, &['Q'], Some('Q'), &mut rng);
let focused_count = result.chars().filter(|&ch| ch == 'Q').count();
assert!(
focused_count >= 4,
"Expected forced focused Q occurrences, got {focused_count} in: {result}"
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,87 @@
use crate::engine::filter::CharFilter;
use crate::l10n::unicode::normalize_nfc;
const WORDS_EN: &str = include_str!("../../assets/words-en.json");
const WORDS_CS: &str = include_str!("../../assets/dictionaries/words-cs.json");
const WORDS_DA: &str = include_str!("../../assets/dictionaries/words-da.json");
const WORDS_DE: &str = include_str!("../../assets/dictionaries/words-de.json");
const WORDS_EN: &str = include_str!("../../assets/dictionaries/words-en.json");
const WORDS_ES: &str = include_str!("../../assets/dictionaries/words-es.json");
const WORDS_ET: &str = include_str!("../../assets/dictionaries/words-et.json");
const WORDS_FI: &str = include_str!("../../assets/dictionaries/words-fi.json");
const WORDS_FR: &str = include_str!("../../assets/dictionaries/words-fr.json");
const WORDS_HR: &str = include_str!("../../assets/dictionaries/words-hr.json");
const WORDS_HU: &str = include_str!("../../assets/dictionaries/words-hu.json");
const WORDS_IT: &str = include_str!("../../assets/dictionaries/words-it.json");
const WORDS_LT: &str = include_str!("../../assets/dictionaries/words-lt.json");
const WORDS_LV: &str = include_str!("../../assets/dictionaries/words-lv.json");
const WORDS_NB: &str = include_str!("../../assets/dictionaries/words-nb.json");
const WORDS_NL: &str = include_str!("../../assets/dictionaries/words-nl.json");
const WORDS_PL: &str = include_str!("../../assets/dictionaries/words-pl.json");
const WORDS_PT: &str = include_str!("../../assets/dictionaries/words-pt.json");
const WORDS_RO: &str = include_str!("../../assets/dictionaries/words-ro.json");
const WORDS_SL: &str = include_str!("../../assets/dictionaries/words-sl.json");
const WORDS_SV: &str = include_str!("../../assets/dictionaries/words-sv.json");
const WORDS_TR: &str = include_str!("../../assets/dictionaries/words-tr.json");
#[derive(Clone, Debug)]
pub struct Dictionary {
words: Vec<String>,
}
impl Dictionary {
pub fn load() -> Self {
let words: Vec<String> = serde_json::from_str(WORDS_EN).unwrap_or_default();
// Filter to words of length >= 3 (matching keybr)
let words = words
.into_iter()
.filter(|w| w.len() >= 3 && w.chars().all(|c| c.is_ascii_lowercase()))
.collect();
Self { words }
fn raw_for_language(language_key: &str) -> Option<&'static str> {
match language_key {
"cs" => Some(WORDS_CS),
"da" => Some(WORDS_DA),
"de" => Some(WORDS_DE),
"en" => Some(WORDS_EN),
"es" => Some(WORDS_ES),
"et" => Some(WORDS_ET),
"fi" => Some(WORDS_FI),
"fr" => Some(WORDS_FR),
"hr" => Some(WORDS_HR),
"hu" => Some(WORDS_HU),
"it" => Some(WORDS_IT),
"lt" => Some(WORDS_LT),
"lv" => Some(WORDS_LV),
"nb" => Some(WORDS_NB),
"nl" => Some(WORDS_NL),
"pl" => Some(WORDS_PL),
"pt" => Some(WORDS_PT),
"ro" => Some(WORDS_RO),
"sl" => Some(WORDS_SL),
"sv" => Some(WORDS_SV),
"tr" => Some(WORDS_TR),
_ => None,
}
}
pub fn words_list(&self) -> Vec<String> {
self.words.clone()
pub fn supports_language(language_key: &str) -> bool {
Self::raw_for_language(language_key).is_some()
}
pub fn try_load_for_language(language_key: &str) -> Option<Self> {
let raw = Self::raw_for_language(language_key)?;
let words: Vec<String> = serde_json::from_str(raw).unwrap_or_default();
// Filter to words of length >= 3 and normalize to NFC for consistent
// matching across composed/decomposed forms.
let words = words
.into_iter()
.map(|w| normalize_nfc(&w))
.filter(|w| w.chars().count() >= 3)
.filter(|w| !w.chars().any(|c| c.is_whitespace()))
.collect::<Vec<String>>();
Some(Self { words })
}
pub fn load_for_language(language_key: &str) -> Self {
Self::try_load_for_language(language_key)
.unwrap_or_else(|| panic!("unsupported dictionary language: {language_key}"))
}
pub fn words_list(&self) -> &[String] {
&self.words
}
pub fn find_matching(&self, filter: &CharFilter, focused: Option<char>) -> Vec<&str> {
@@ -39,3 +100,63 @@ impl Dictionary {
matching
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::l10n::language_pack::{language_packs, supported_dictionary_languages};
#[test]
#[should_panic(expected = "unsupported dictionary language")]
fn load_for_language_unknown_panics() {
let _ = Dictionary::load_for_language("zz");
}
#[test]
fn find_matching_focused_is_sort_only() {
let dictionary = Dictionary::load_for_language("en");
let filter = CharFilter::new(('a'..='z').collect());
let without_focus = dictionary.find_matching(&filter, None);
let with_focus = dictionary.find_matching(&filter, Some('k'));
// Same membership — focused param only reorders, never filters
let mut sorted_without: Vec<&str> = without_focus.clone();
let mut sorted_with: Vec<&str> = with_focus.clone();
sorted_without.sort();
sorted_with.sort();
assert_eq!(sorted_without, sorted_with);
assert_eq!(without_focus.len(), with_focus.len());
}
#[test]
fn non_english_dictionaries_load_substantial_word_lists() {
for &lang in supported_dictionary_languages() {
if lang == "en" {
continue;
}
let dictionary = Dictionary::load_for_language(lang);
assert!(
dictionary.words_list().len() > 100,
"expected substantial dictionary for language {lang}"
);
}
}
#[test]
fn all_registered_language_packs_have_embedded_dictionary_assets() {
for pack in language_packs() {
assert!(
Dictionary::supports_language(pack.language_key),
"language pack {} is missing an embedded dictionary asset",
pack.language_key
);
assert!(
Dictionary::try_load_for_language(pack.language_key).is_some(),
"dictionary load failed for language pack {}",
pack.language_key
);
}
}
}

Some files were not shown because too many files have changed in this diff Show More