Compare commits
39 Commits
c78a8a90a3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f855fa5606 | |||
| 232f93e054 | |||
| d1fde5c0c1 | |||
| 7e13f73b8c | |||
| eeb48157c5 | |||
| 79021db57f | |||
| 01fc609f8f | |||
| 84f4aabdff | |||
| 6d5de33f55 | |||
| 895e04d6ce | |||
| f20fa6110d | |||
| 5c56a9c3c6 | |||
| 8b8703b9b9 | |||
| 7c1aad84af | |||
| 8e4f9bf064 | |||
| b37dc72b45 | |||
| c67ddf577a | |||
| de236284ea | |||
| ca2a3507f4 | |||
| da907c0f46 | |||
| a088075924 | |||
| 3ef433404e | |||
| 54ddebf054 | |||
| e7f57dd497 | |||
| 0c5a70d5c4 | |||
| f8bcad247b | |||
| 9d59c265dd | |||
| 9deffc3d1d | |||
| 9cc8a214ad | |||
| 9e0411e1f4 | |||
| 4e39e99732 | |||
| d0605f8426 | |||
| 2d63cffb33 | |||
| a61ed77ed6 | |||
| edd2f7e6b5 | |||
| 6d6815af02 | |||
| 13550505c1 | |||
| a51adafeb0 | |||
| a0e8f3cafb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
/clones/
|
/clones/
|
||||||
|
/test-profiles/
|
||||||
|
|||||||
392
Cargo.lock
generated
392
Cargo.lock
generated
@@ -26,6 +26,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anes"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.21"
|
version = "0.6.21"
|
||||||
@@ -82,6 +88,15 @@ version = "1.0.101"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
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]]
|
[[package]]
|
||||||
name = "atomic"
|
name = "atomic"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -103,6 +118,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base62"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -145,6 +166,16 @@ dependencies = [
|
|||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.1"
|
version = "3.19.1"
|
||||||
@@ -163,6 +194,12 @@ version = "1.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cast"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "castaway"
|
name = "castaway"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -208,6 +245,33 @@ dependencies = [
|
|||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.57"
|
version = "4.5.57"
|
||||||
@@ -302,6 +366,67 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
@@ -345,6 +470,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -691,6 +822,36 @@ dependencies = [
|
|||||||
"wasip2",
|
"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]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -710,6 +871,17 @@ dependencies = [
|
|||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@@ -727,6 +899,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -983,6 +1161,22 @@ dependencies = [
|
|||||||
"icu_properties",
|
"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]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -1031,12 +1225,41 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
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]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -1080,14 +1303,20 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"criterion",
|
||||||
"crossterm 0.28.1",
|
"crossterm 0.28.1",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"icu_normalizer",
|
||||||
"rand",
|
"rand",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
|
"rust-i18n",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
@@ -1272,6 +1501,15 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"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]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1319,6 +1557,12 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oorandom"
|
||||||
|
version = "11.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.75"
|
version = "0.10.75"
|
||||||
@@ -1520,6 +1764,34 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
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]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.13.1"
|
version = "1.13.1"
|
||||||
@@ -1628,7 +1900,7 @@ dependencies = [
|
|||||||
"compact_str",
|
"compact_str",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools",
|
"itertools 0.14.0",
|
||||||
"kasuari",
|
"kasuari",
|
||||||
"lru",
|
"lru",
|
||||||
"strum",
|
"strum",
|
||||||
@@ -1681,7 +1953,7 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
"indoc",
|
"indoc",
|
||||||
"instability",
|
"instability",
|
||||||
"itertools",
|
"itertools 0.14.0",
|
||||||
"line-clipping",
|
"line-clipping",
|
||||||
"ratatui-core",
|
"ratatui-core",
|
||||||
"strum",
|
"strum",
|
||||||
@@ -1690,6 +1962,26 @@ dependencies = [
|
|||||||
"unicode-width",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -1829,6 +2121,60 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -2026,6 +2372,19 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -2357,6 +2716,16 @@ dependencies = [
|
|||||||
"zerovec",
|
"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]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.49.0"
|
version = "1.49.0"
|
||||||
@@ -2509,6 +2878,17 @@ dependencies = [
|
|||||||
"once_cell",
|
"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]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -2545,7 +2925,7 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
|
checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itertools",
|
"itertools 0.14.0",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
@@ -2556,6 +2936,12 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
17
Cargo.toml
17
Cargo.toml
@@ -3,6 +3,7 @@ name = "keydr"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Terminal typing tutor with adaptive learning"
|
description = "Terminal typing tutor with adaptive learning"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ratatui = { version = "0.30", features = ["crossterm_0_28"] }
|
ratatui = { version = "0.30", features = ["crossterm_0_28"] }
|
||||||
@@ -18,6 +19,22 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
reqwest = { version = "0.12", features = ["blocking"], optional = true }
|
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]
|
[features]
|
||||||
default = ["network"]
|
default = ["network"]
|
||||||
|
|||||||
661
LICENSE
Normal file
661
LICENSE
Normal 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
9
NOTICE
Normal 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
68
THIRD_PARTY_NOTICES.md
Normal 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.
|
||||||
21
assets/dictionaries/SHA256SUMS
Normal file
21
assets/dictionaries/SHA256SUMS
Normal 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
|
||||||
22
assets/dictionaries/manifest.tsv
Normal file
22
assets/dictionaries/manifest.tsv
Normal 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
|
||||||
|
10002
assets/dictionaries/words-cs.json
Normal file
10002
assets/dictionaries/words-cs.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-cs.json.license
Normal file
7
assets/dictionaries/words-cs.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-da.json
Normal file
10002
assets/dictionaries/words-da.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-da.json.license
Normal file
7
assets/dictionaries/words-da.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-de.json
Normal file
10002
assets/dictionaries/words-de.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-de.json.license
Normal file
7
assets/dictionaries/words-de.json.license
Normal 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).
|
||||||
7
assets/dictionaries/words-en.json.license
Normal file
7
assets/dictionaries/words-en.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-es.json
Normal file
10002
assets/dictionaries/words-es.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-es.json.license
Normal file
7
assets/dictionaries/words-es.json.license
Normal 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).
|
||||||
9682
assets/dictionaries/words-et.json
Normal file
9682
assets/dictionaries/words-et.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-et.json.license
Normal file
7
assets/dictionaries/words-et.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-fi.json
Normal file
10002
assets/dictionaries/words-fi.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-fi.json.license
Normal file
7
assets/dictionaries/words-fi.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-fr.json
Normal file
10002
assets/dictionaries/words-fr.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-fr.json.license
Normal file
7
assets/dictionaries/words-fr.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-hr.json
Normal file
10002
assets/dictionaries/words-hr.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-hr.json.license
Normal file
7
assets/dictionaries/words-hr.json.license
Normal 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).
|
||||||
8649
assets/dictionaries/words-hu.json
Normal file
8649
assets/dictionaries/words-hu.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-hu.json.license
Normal file
7
assets/dictionaries/words-hu.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-it.json
Normal file
10002
assets/dictionaries/words-it.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-it.json.license
Normal file
7
assets/dictionaries/words-it.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-lt.json
Normal file
10002
assets/dictionaries/words-lt.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-lt.json.license
Normal file
7
assets/dictionaries/words-lt.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-lv.json
Normal file
10002
assets/dictionaries/words-lv.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-lv.json.license
Normal file
7
assets/dictionaries/words-lv.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-nb.json
Normal file
10002
assets/dictionaries/words-nb.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-nb.json.license
Normal file
7
assets/dictionaries/words-nb.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-nl.json
Normal file
10002
assets/dictionaries/words-nl.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-nl.json.license
Normal file
7
assets/dictionaries/words-nl.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-pl.json
Normal file
10002
assets/dictionaries/words-pl.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-pl.json.license
Normal file
7
assets/dictionaries/words-pl.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-pt.json
Normal file
10002
assets/dictionaries/words-pt.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-pt.json.license
Normal file
7
assets/dictionaries/words-pt.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-ro.json
Normal file
10002
assets/dictionaries/words-ro.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-ro.json.license
Normal file
7
assets/dictionaries/words-ro.json.license
Normal 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).
|
||||||
2767
assets/dictionaries/words-sl.json
Normal file
2767
assets/dictionaries/words-sl.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-sl.json.license
Normal file
7
assets/dictionaries/words-sl.json.license
Normal 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).
|
||||||
10002
assets/dictionaries/words-sv.json
Normal file
10002
assets/dictionaries/words-sv.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-sv.json.license
Normal file
7
assets/dictionaries/words-sv.json.license
Normal 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).
|
||||||
8888
assets/dictionaries/words-tr.json
Normal file
8888
assets/dictionaries/words-tr.json
Normal file
File diff suppressed because it is too large
Load Diff
7
assets/dictionaries/words-tr.json.license
Normal file
7
assets/dictionaries/words-tr.json.license
Normal 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).
|
||||||
23
assets/themes/farout.toml
Normal file
23
assets/themes/farout.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "farout"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#0f0908"
|
||||||
|
fg = "#E0CCAE"
|
||||||
|
text_correct = "#a4896f"
|
||||||
|
text_incorrect = "#bf472c"
|
||||||
|
text_incorrect_bg = "#392D2B"
|
||||||
|
text_pending = "#A67458"
|
||||||
|
text_cursor_bg = "#0f0908"
|
||||||
|
text_cursor_fg = "#f2a766"
|
||||||
|
focused_key = "#f2a766"
|
||||||
|
accent = "#d47d49"
|
||||||
|
accent_dim = "#392D2B"
|
||||||
|
border = "#392D2B"
|
||||||
|
border_focused = "#d47d49"
|
||||||
|
header_bg = "#392D2B"
|
||||||
|
header_fg = "#E0CCAE"
|
||||||
|
bar_filled = "#a67458"
|
||||||
|
bar_empty = "#392D2B"
|
||||||
|
error = "#bf472c"
|
||||||
|
warning = "#f2a766"
|
||||||
|
success = "#a4896f"
|
||||||
23
assets/themes/gruvbox-darkest.toml
Normal file
23
assets/themes/gruvbox-darkest.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "gruvbox-darkest"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#121212"
|
||||||
|
fg = "#ebdbb2"
|
||||||
|
text_correct = "#b8bb26"
|
||||||
|
text_incorrect = "#fb4934"
|
||||||
|
text_incorrect_bg = "#462726"
|
||||||
|
text_pending = "#a89984"
|
||||||
|
text_cursor_bg = "#fabd2f"
|
||||||
|
text_cursor_fg = "#121212"
|
||||||
|
focused_key = "#fabd2f"
|
||||||
|
accent = "#83a598"
|
||||||
|
accent_dim = "#3c3836"
|
||||||
|
border = "#504945"
|
||||||
|
border_focused = "#83a598"
|
||||||
|
header_bg = "#3c3836"
|
||||||
|
header_fg = "#ebdbb2"
|
||||||
|
bar_filled = "#83a598"
|
||||||
|
bar_empty = "#3c3836"
|
||||||
|
error = "#fb4934"
|
||||||
|
warning = "#fabd2f"
|
||||||
|
success = "#b8bb26"
|
||||||
23
assets/themes/kanagawa-dragon.toml
Normal file
23
assets/themes/kanagawa-dragon.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "kanagawa-dragon"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#181616"
|
||||||
|
fg = "#c5c9c5"
|
||||||
|
text_correct = "#8a9a7b"
|
||||||
|
text_incorrect = "#c4746e"
|
||||||
|
text_incorrect_bg = "#43242B"
|
||||||
|
text_pending = "#a6a69c"
|
||||||
|
text_cursor_bg = "#2d4f67"
|
||||||
|
text_cursor_fg = "#c8c093"
|
||||||
|
focused_key = "#c4b28a"
|
||||||
|
accent = "#8ba4b0"
|
||||||
|
accent_dim = "#282727"
|
||||||
|
border = "#625e5a"
|
||||||
|
border_focused = "#8ba4b0"
|
||||||
|
header_bg = "#282727"
|
||||||
|
header_fg = "#c5c9c5"
|
||||||
|
bar_filled = "#8ea4a2"
|
||||||
|
bar_empty = "#282727"
|
||||||
|
error = "#c4746e"
|
||||||
|
warning = "#c4b28a"
|
||||||
|
success = "#8a9a7b"
|
||||||
23
assets/themes/kanagawa-lotus.toml
Normal file
23
assets/themes/kanagawa-lotus.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "kanagawa-lotus"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#f2ecbc"
|
||||||
|
fg = "#545464"
|
||||||
|
text_correct = "#6f894e"
|
||||||
|
text_incorrect = "#c84053"
|
||||||
|
text_incorrect_bg = "#d9a594"
|
||||||
|
text_pending = "#8a8980"
|
||||||
|
text_cursor_bg = "#5d57a3"
|
||||||
|
text_cursor_fg = "#f2ecbc"
|
||||||
|
focused_key = "#77713f"
|
||||||
|
accent = "#4d699b"
|
||||||
|
accent_dim = "#e7dba0"
|
||||||
|
border = "#a5a37d"
|
||||||
|
border_focused = "#4d699b"
|
||||||
|
header_bg = "#e7dba0"
|
||||||
|
header_fg = "#545464"
|
||||||
|
bar_filled = "#597b75"
|
||||||
|
bar_empty = "#d9d0a3"
|
||||||
|
error = "#c84053"
|
||||||
|
warning = "#77713f"
|
||||||
|
success = "#6f894e"
|
||||||
23
assets/themes/kanagawa-wave.toml
Normal file
23
assets/themes/kanagawa-wave.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "kanagawa-wave"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#1f1f28"
|
||||||
|
fg = "#dcd7ba"
|
||||||
|
text_correct = "#76946a"
|
||||||
|
text_incorrect = "#c34043"
|
||||||
|
text_incorrect_bg = "#43242B"
|
||||||
|
text_pending = "#727169"
|
||||||
|
text_cursor_bg = "#2d4f67"
|
||||||
|
text_cursor_fg = "#c8c093"
|
||||||
|
focused_key = "#c0a36e"
|
||||||
|
accent = "#7e9cd8"
|
||||||
|
accent_dim = "#2A2A37"
|
||||||
|
border = "#54546D"
|
||||||
|
border_focused = "#7e9cd8"
|
||||||
|
header_bg = "#2A2A37"
|
||||||
|
header_fg = "#dcd7ba"
|
||||||
|
bar_filled = "#7e9cd8"
|
||||||
|
bar_empty = "#2A2A37"
|
||||||
|
error = "#c34043"
|
||||||
|
warning = "#c0a36e"
|
||||||
|
success = "#76946a"
|
||||||
23
assets/themes/terminal-default.toml
Normal file
23
assets/themes/terminal-default.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "terminal-default"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "reset"
|
||||||
|
fg = "reset"
|
||||||
|
text_correct = "green"
|
||||||
|
text_incorrect = "red"
|
||||||
|
text_incorrect_bg = "reset"
|
||||||
|
text_pending = "darkgray"
|
||||||
|
text_cursor_bg = "reset"
|
||||||
|
text_cursor_fg = "reset"
|
||||||
|
focused_key = "yellow"
|
||||||
|
accent = "blue"
|
||||||
|
accent_dim = "darkgray"
|
||||||
|
border = "darkgray"
|
||||||
|
border_focused = "blue"
|
||||||
|
header_bg = "reset"
|
||||||
|
header_fg = "reset"
|
||||||
|
bar_filled = "blue"
|
||||||
|
bar_empty = "darkgray"
|
||||||
|
error = "red"
|
||||||
|
warning = "yellow"
|
||||||
|
success = "green"
|
||||||
133
benches/ngram_benchmarks.rs
Normal file
133
benches/ngram_benchmarks.rs
Normal 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);
|
||||||
42
docs/license-compliance.md
Normal file
42
docs/license-compliance.md
Normal 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.
|
||||||
357
docs/plans/2026-02-14-improvement-2.md
Normal file
357
docs/plans/2026-02-14-improvement-2.md
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
# Keydr Improvement Plan
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The keydr typing tutor app needs six improvements to bring it closer to the quality of keybr.com and typr. Currently the app starts at a menu screen, doesn't properly count corrected errors, has a confusing keyboard visualization, lacks responsive layout, can't delete sessions, and has basic statistics views.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Start in Adaptive Drill by Default
|
||||||
|
|
||||||
|
**Files:** `src/app.rs`
|
||||||
|
|
||||||
|
**Implementation:** Change `App::new()` to use a `let mut app = Self { ... }; app.start_lesson(); app` pattern. The struct literal currently at `src/app.rs:99-120` sets `screen: AppScreen::Menu` — change this to construct `Self`, then call `start_lesson()` which sets `screen = AppScreen::Lesson` and generates text. The menu remains accessible via ESC from lesson/result screens (unchanged).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Fix Error Tracking for Backspaced Corrections
|
||||||
|
|
||||||
|
**Files:** `src/session/lesson.rs`, `src/session/input.rs`, `src/session/result.rs`, `src/ui/components/stats_sidebar.rs`
|
||||||
|
|
||||||
|
**Problem:** When a user types wrong, backspaces, then types correctly, keydr pops `CharStatus::Incorrect` from the input vector and replaces with `CharStatus::Correct`. Final accuracy shows 0 errors. keybr.com counts this as an error (see `packages/keybr-textinput/lib/textinput.ts` — `typo` flag persists through backspace corrections, and `stats.ts:42-49` counts all steps with `typo: true`).
|
||||||
|
|
||||||
|
**Implementation — keybr-style step-based tracking:**
|
||||||
|
|
||||||
|
### Two separate tracking systems:
|
||||||
|
|
||||||
|
**A. Live display counters (existing, unchanged):**
|
||||||
|
- `input: Vec<CharStatus>` continues to track current visible state (grows on type, shrinks on backspace)
|
||||||
|
- `incorrect_count()` and `correct_count()` show current snapshot for the sidebar display
|
||||||
|
- `accuracy()` on `LessonState` continues using `input.len()` as denominator — only reflects currently-visible chars
|
||||||
|
|
||||||
|
**B. Persistent typo tracking (new, for final results):**
|
||||||
|
- Add `typo_flags: HashSet<usize>` to `LessonState` — tracks positions where ANY incorrect key was ever pressed
|
||||||
|
|
||||||
|
**Process flow:**
|
||||||
|
1. `process_char()` — when `!correct`: insert `lesson.cursor` into `typo_flags`. Push to `input` as before.
|
||||||
|
2. `process_backspace()` — pop from `input`, decrement cursor. Do NOT remove from `typo_flags`.
|
||||||
|
3. When the lesson completes (all positions filled with correct/incorrect chars), `LessonResult::from_lesson()` builds the final result using `typo_flags` to determine error count:
|
||||||
|
- `incorrect = typo_flags.len()` (positions where any error ever occurred)
|
||||||
|
- `accuracy = (total_chars - typo_flags.len()) / total_chars * 100`
|
||||||
|
- This avoids the denominator mismatch since we always use `target.len()` as the denominator
|
||||||
|
|
||||||
|
**Sidebar display during lesson:**
|
||||||
|
- Show "Errors: X" using `typo_flags.len()` (accumulated errors, never decreases)
|
||||||
|
- Live accuracy: count `typo_flags` entries that are `< cursor` (i.e., only count typos at positions already typed past), then: `((cursor - typos_before_cursor).max(0) as f64 / cursor as f64 * 100.0).clamp(0.0, 100.0)` where cursor > 0. This handles the backspace case correctly — if cursor retreats behind a typo'd position, that typo doesn't count in the live denominator.
|
||||||
|
|
||||||
|
### Unit tests:
|
||||||
|
- Type "abc" correctly → typo_flags empty, accuracy 100%
|
||||||
|
- Type wrong char at pos 0, backspace, type correct → typo_flags = {0}, accuracy < 100%
|
||||||
|
- Type wrong char, continue without backspace → typo_flags = {pos}, also in input as Incorrect
|
||||||
|
- Multiple errors at same position (wrong, backspace, wrong again, backspace, correct) → typo_flags = {pos}, counts as 1 error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Fix Keyboard Visualization
|
||||||
|
|
||||||
|
**Files:** `src/ui/components/keyboard_diagram.rs`, `src/main.rs`, `src/app.rs`, `src/event.rs`
|
||||||
|
|
||||||
|
**Problem:** All key colors shift constantly with no meaning. User expects pressed keys to light up.
|
||||||
|
|
||||||
|
**How keybr.com does it:**
|
||||||
|
- Uses physical key codes (W3C `event.code` like `KeyA`, `KeyQ`) for tracking depressed keys
|
||||||
|
- `Controller.tsx:99-107`: `onKeyDown` adds to `depressedKeys`, `onKeyUp` removes
|
||||||
|
- `KeyboardPresenter.tsx:36-39`: passes `depressedKeys` array and `suffixKeys` (next expected) to keyboard UI
|
||||||
|
- `KeyLayer.tsx`: pre-computes 8 states per key (depressed × toggled × showColors), selects based on current state
|
||||||
|
|
||||||
|
**Implementation — crossterm supports key Press/Release events:**
|
||||||
|
|
||||||
|
**Scope decision:** We track depressed state for **printable character keys only** (`KeyCode::Char(ch)`). This is intentional non-parity with keybr.com's physical-key-ID model — keybr runs in a browser with W3C key codes, but keydr's keyboard diagram only shows letter keys. Modifier keys (Shift, Ctrl, Alt) are not shown on the diagram and don't need depressed tracking. Characters are lowercased for matching against the diagram.
|
||||||
|
|
||||||
|
crossterm 0.28 provides `KeyEventKind::Press`, `KeyEventKind::Release`, and `KeyEventKind::Repeat` via `KeyEvent.kind`. However, terminal key-release support is inconsistent across terminals. We use a **hybrid approach**: track via Release events when available, with a 150ms timed fallback.
|
||||||
|
|
||||||
|
1. **Enable enhanced key events** (`src/main.rs`):
|
||||||
|
- Call `crossterm::event::PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::REPORT_EVENT_TYPES)` on startup (enables Release events on supported terminals)
|
||||||
|
- Pop the flags on cleanup
|
||||||
|
|
||||||
|
2. **Track depressed keys** (`src/app.rs`):
|
||||||
|
- Add `depressed_keys: HashSet<char>` field (stores lowercase chars)
|
||||||
|
- Add `last_key_time: Option<Instant>` for fallback clearing
|
||||||
|
- On `KeyEventKind::Press` with `KeyCode::Char(ch)`: insert `ch.to_ascii_lowercase()` into `depressed_keys`, set `last_key_time`
|
||||||
|
- On `KeyEventKind::Release` with `KeyCode::Char(ch)`: remove `ch.to_ascii_lowercase()` from `depressed_keys`
|
||||||
|
- On tick: if `last_key_time` > 150ms ago and no Release was received, clear `depressed_keys` (fallback for terminals without Release support)
|
||||||
|
|
||||||
|
3. **Update event handling** (`src/main.rs` `handle_key`):
|
||||||
|
- Check `key.kind` — only process typing logic on `KeyEventKind::Press`
|
||||||
|
- On `KeyEventKind::Release`: call `app.depressed_keys.remove(&ch.to_ascii_lowercase())`
|
||||||
|
- Filter out `KeyEventKind::Repeat` to avoid double-counting (or treat same as Press for depressed tracking)
|
||||||
|
|
||||||
|
4. **Update KeyboardDiagram** (`src/ui/components/keyboard_diagram.rs`):
|
||||||
|
- Accept `depressed_keys: &HashSet<char>` (all lowercase)
|
||||||
|
- Rendering priority order: **depressed** (bright/inverted style) > **next_expected** (accent bg) > **focused** (yellow bg) > **unlocked** (finger zone color) > **locked** (dim)
|
||||||
|
- Depressed style: bold white text on brighter version of the finger color
|
||||||
|
|
||||||
|
5. **Investigate "constantly shifting colors" bug:**
|
||||||
|
- Current code at `main.rs:356-359` passes `lesson.target.get(lesson.cursor)` as `next_char` — this correctly changes on each keystroke
|
||||||
|
- Verify the finger_color mapping is stable (it uses static match arms — should be fine)
|
||||||
|
- Most likely the "shifting" perception is the `next_key` highlight moving to adjacent keys as user types — this is correct behavior. The depressed-key highlight will make the interaction much clearer.
|
||||||
|
|
||||||
|
### Unit tests:
|
||||||
|
- Verify `depressed_keys` set grows on Press and shrinks on Release
|
||||||
|
- Verify fallback clearing works after 150ms timeout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Responsive UI for Small Terminals
|
||||||
|
|
||||||
|
**Files:** `src/ui/layout.rs`, `src/main.rs`, `src/ui/components/keyboard_diagram.rs`, `src/ui/components/stats_sidebar.rs`, `src/ui/components/typing_area.rs`
|
||||||
|
|
||||||
|
**How typr handles it (from `clones/typr/lua/typr/stats/init.lua:15-17`):**
|
||||||
|
- Base width: `state.w = 80` columns
|
||||||
|
- Responsive threshold: `vim.o.columns > ((2 * state.w) + 10)` = `> 170` cols → horizontal stats layout
|
||||||
|
- Below 170 cols → vertical tabbed stats layout
|
||||||
|
- Drill view: fixed 80-col centered window, doesn't have a sidebar concept
|
||||||
|
- Window height adapts: `large_screen and state.h or vim.o.lines - 7`
|
||||||
|
|
||||||
|
**Implementation — tiered layout for keydr:**
|
||||||
|
|
||||||
|
### Drill View Layout Tiers (based on `area` from `AppLayout::new()`):
|
||||||
|
|
||||||
|
**Wide (≥100 cols):** Current layout — typing area (70%) + sidebar (30%) side-by-side, keyboard + progress bar below typing area
|
||||||
|
|
||||||
|
**Medium (60-99 cols):**
|
||||||
|
- Typing area takes full width (no sidebar)
|
||||||
|
- Compact stats in header bar: `WPM: XX | Acc: XX% | Errors: X`
|
||||||
|
- Keyboard diagram below typing area (compressed 3-char keys `[x]` instead of `[ x ]`)
|
||||||
|
- Progress bar below keyboard
|
||||||
|
|
||||||
|
**Narrow (<60 cols):**
|
||||||
|
- Typing area full width
|
||||||
|
- Stats in header bar only
|
||||||
|
- No keyboard diagram
|
||||||
|
- No progress bar
|
||||||
|
|
||||||
|
**Short (<20 rows):**
|
||||||
|
- No keyboard diagram (regardless of width)
|
||||||
|
- No progress bar
|
||||||
|
- Typing area + single-line header + single-line footer
|
||||||
|
|
||||||
|
### Stats View Layout Tiers:
|
||||||
|
|
||||||
|
**Wide (>170 cols):** Side-by-side panels (matching typr threshold: `(2 * 80) + 10`)
|
||||||
|
**Normal (≤170 cols):** Tabbed view (current behavior, improved styling per item 6)
|
||||||
|
|
||||||
|
### Implementation:
|
||||||
|
1. Modify `AppLayout::new()` to accept area and return different constraint sets based on dimensions
|
||||||
|
2. Add `LayoutTier` enum: `{ Wide, Medium, Narrow }` computed from `area.width` and `area.height`
|
||||||
|
3. `render_lesson()` checks tier to decide which components to render
|
||||||
|
4. `KeyboardDiagram` gets a `compact: bool` flag for 3-char key mode
|
||||||
|
5. Verify `TypingArea` wraps properly at narrow widths (current implementation should handle this via Ratatui's `Paragraph` wrapping)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Delete Sessions from History
|
||||||
|
|
||||||
|
**Files:** `src/app.rs`, `src/main.rs`, `src/ui/components/stats_dashboard.rs`
|
||||||
|
|
||||||
|
**Implementation — complete recalculation scope:**
|
||||||
|
|
||||||
|
### State machine for history tab interaction:
|
||||||
|
|
||||||
|
```
|
||||||
|
Normal browsing → [j/k/Up/Down] → Move selection cursor
|
||||||
|
Normal browsing → [x/Delete] → Show confirmation dialog
|
||||||
|
Confirmation dialog → [y] → Delete session, recalculate, return to Normal
|
||||||
|
Confirmation dialog → [n/ESC] → Cancel, return to Normal
|
||||||
|
Normal browsing → [Tab/d/h/k/1/2/3] → Switch tabs (existing behavior)
|
||||||
|
```
|
||||||
|
|
||||||
|
### App state additions (`src/app.rs`):
|
||||||
|
- `history_selected: usize` — selected row index in history view (0 = most recent)
|
||||||
|
- `history_confirm_delete: bool` — whether confirmation dialog is showing
|
||||||
|
|
||||||
|
### Key bindings — full precedence table for `handle_stats_key`:
|
||||||
|
|
||||||
|
**When `history_confirm_delete == true` (confirmation dialog active):**
|
||||||
|
- `y` → call `delete_session()`, set `history_confirm_delete = false`
|
||||||
|
- `n` / `ESC` → set `history_confirm_delete = false` (cancel)
|
||||||
|
- All other keys ignored
|
||||||
|
|
||||||
|
**When `stats_tab == 1` (history tab, no dialog):**
|
||||||
|
- `j` / `Down` → increment `history_selected` (clamp to history length)
|
||||||
|
- `k` / `Up` → decrement `history_selected` (clamp to 0)
|
||||||
|
- `x` / `Delete` → set `history_confirm_delete = true`
|
||||||
|
- `d` / `1` → switch to Dashboard tab (`stats_tab = 0`)
|
||||||
|
- `h` / `2` → switch to History tab (no-op, already there)
|
||||||
|
- `3` → switch to Keystrokes tab (`stats_tab = 2`). Note: `k` is NOT a Keystrokes tab shortcut when on history tab — it navigates rows instead.
|
||||||
|
- `Tab` / `BackTab` → cycle tabs
|
||||||
|
- `ESC` / `q` → back to menu
|
||||||
|
|
||||||
|
**When on other tabs (stats_tab == 0 or 2):**
|
||||||
|
- Existing behavior unchanged: `d`/`1`, `h`/`2`, `k`/`3` switch tabs, `Tab`/`BackTab` cycle, `ESC`/`q` back to menu
|
||||||
|
|
||||||
|
### Delete logic (`src/app.rs` `delete_session()`):
|
||||||
|
|
||||||
|
Full recalculation via **chronological replay** to make it "as if the session never happened":
|
||||||
|
|
||||||
|
1. **Remove the lesson** from `self.lesson_history` at the correct index (history tab shows reverse order, so actual index = `len - 1 - history_selected`)
|
||||||
|
|
||||||
|
2. **Chronological state replay** — reset and rebuild from scratch, oldest→newest:
|
||||||
|
```
|
||||||
|
// Reset all derived state
|
||||||
|
self.key_stats = KeyStatsStore::default();
|
||||||
|
self.key_stats.target_cpm = self.config.target_cpm();
|
||||||
|
self.letter_unlock = LetterUnlock::new();
|
||||||
|
self.profile.total_score = 0.0;
|
||||||
|
self.profile.total_lessons = 0;
|
||||||
|
self.profile.streak_days = 0;
|
||||||
|
self.profile.best_streak = 0;
|
||||||
|
self.profile.last_practice_date = None;
|
||||||
|
|
||||||
|
// Replay each remaining session oldest→newest
|
||||||
|
for result in &self.lesson_history {
|
||||||
|
// Update key stats (same as finish_lesson does)
|
||||||
|
for kt in &result.per_key_times {
|
||||||
|
if kt.correct {
|
||||||
|
self.key_stats.update_key(kt.key, kt.time_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update letter unlock
|
||||||
|
self.letter_unlock.update(&self.key_stats);
|
||||||
|
|
||||||
|
// Compute score using current unlock state (matches runtime)
|
||||||
|
let complexity = compute_complexity(self.letter_unlock.unlocked_count());
|
||||||
|
let score = compute_score(result, complexity);
|
||||||
|
self.profile.total_score += score;
|
||||||
|
self.profile.total_lessons += 1;
|
||||||
|
|
||||||
|
// Rebuild streak tracking (same logic as finish_lesson)
|
||||||
|
let day = result.timestamp.format("%Y-%m-%d").to_string();
|
||||||
|
// ... streak logic identical to App::finish_lesson
|
||||||
|
}
|
||||||
|
|
||||||
|
self.profile.unlocked_letters = self.letter_unlock.included.clone();
|
||||||
|
```
|
||||||
|
This exactly reproduces the runtime scoring path (`src/app.rs:186-218`, `src/engine/scoring.rs:3-7`), including complexity that depends on unlock state at each point in progression.
|
||||||
|
|
||||||
|
3. **Persist:** Call `self.save_data()` to write all three files (profile, key_stats, lesson_history)
|
||||||
|
|
||||||
|
4. **Adjust selection:** Clamp `history_selected` to new valid range
|
||||||
|
|
||||||
|
**Implementation note:** Extract the replay logic into a reusable `rebuild_from_history(&mut self)` method on `App`, since it could also be useful for data recovery.
|
||||||
|
|
||||||
|
### Rendering (`stats_dashboard.rs`):
|
||||||
|
- Selected row gets `bg(colors.accent_dim())` highlight background (existing theme color `accent_dim` = `#45475a`, a subtle dark surface color)
|
||||||
|
- Confirmation dialog: centered overlay box with border: `"Delete session #X? (y/n)"`
|
||||||
|
|
||||||
|
### Unit tests:
|
||||||
|
- Delete last session → history shrinks by 1, total_lessons decremented
|
||||||
|
- Delete session → key_stats rebuilt without that session's key times
|
||||||
|
- Delete all sessions → profile reset to defaults, key_stats empty
|
||||||
|
- Delete session with only practice day → streak recalculated correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Improved Statistics Display (Full Typr-Style Overhaul)
|
||||||
|
|
||||||
|
**Files:** `src/ui/components/stats_dashboard.rs`, new `src/ui/components/activity_heatmap.rs`
|
||||||
|
|
||||||
|
**Data sources (all derivable from existing persisted data):**
|
||||||
|
- `lesson_history: Vec<LessonResult>` — has `wpm`, `cpm`, `accuracy`, `correct`, `incorrect`, `total_chars`, `elapsed_secs`, `timestamp`, `per_key_times`
|
||||||
|
- `key_stats: KeyStatsStore` — has per-key `filtered_time_ms`, `best_time_ms`, `confidence`, `sample_count`, `recent_times`
|
||||||
|
- No schema migration needed — all new visualizations derive from existing fields
|
||||||
|
|
||||||
|
### Dashboard Tab Improvements:
|
||||||
|
|
||||||
|
**Summary stats as bordered table:**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Lessons: 42 Avg WPM: 65 Best WPM: 82 │
|
||||||
|
│ Accuracy: 94.2% Total time: 2h 15m │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Progress bars** using `┃` filled / dim `┃` empty:
|
||||||
|
- WPM progress: `avg_wpm / target_wpm` (green ≥ goal, accent < goal)
|
||||||
|
- Accuracy progress: (green ≥ 95%, yellow ≥ 85%, red < 85%)
|
||||||
|
- Level progress to next level
|
||||||
|
|
||||||
|
**WPM bar graph** (last 20 sessions) using `▁▂▃▄▅▆▇█` block characters, replacing the Braille line chart. Color-coded: green above goal, red below.
|
||||||
|
|
||||||
|
**Keep accuracy trend chart** (Braille line chart works well for this).
|
||||||
|
|
||||||
|
### History Tab Improvements:
|
||||||
|
|
||||||
|
**Bordered table:**
|
||||||
|
```
|
||||||
|
┌────┬──────┬──────┬───────┬───────┬────────────┐
|
||||||
|
│ # │ WPM │ Raw │ Acc% │ Time │ Date │
|
||||||
|
├────┼──────┼──────┼───────┼───────┼────────────┤
|
||||||
|
│ 42 │ 68 │ 72 │ 96.2% │ 45.2s │ 02/14 10:30│
|
||||||
|
│ 41 │ 63 │ 67 │ 93.1% │ 52.1s │ 02/14 09:15│
|
||||||
|
└────┴──────┴──────┴───────┴───────┴────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Selected row highlighted with distinct background
|
||||||
|
- WPM goal indicator per row: small inline bar or color indicator
|
||||||
|
|
||||||
|
**Character speed distribution** (below table): dot/bar graph of all 26 letters (from typr's history view), using per-key `filtered_time_ms` data already available in `key_stats`.
|
||||||
|
|
||||||
|
### Keystrokes Tab Improvements:
|
||||||
|
|
||||||
|
**Activity heatmap** (new widget in `src/ui/components/activity_heatmap.rs`):
|
||||||
|
- 7-month calendar grid grouped by week
|
||||||
|
- Each day cell: `▪` or `█` colored by session count (0 = dim, 1-5 = light green, 6-15 = medium, 16+ = bright)
|
||||||
|
- Data source: group `lesson_history` by `timestamp.date()`, count per day
|
||||||
|
- Month labels along top, day-of-week labels on left (M/W/F or all 7)
|
||||||
|
- Toggle between first/last 6 months (optional, if space allows)
|
||||||
|
|
||||||
|
**Key accuracy heatmap:** show accuracy percentage text on each key, not just color. E.g., `[a 97%]` or use color intensity.
|
||||||
|
|
||||||
|
**Top 3 worst keys:** highlighted badges showing the keys with lowest accuracy, matching typr's approach.
|
||||||
|
|
||||||
|
**Char times analysis:** Slowest 5 / Fastest 5 keys with times (already exists, clean up formatting with box borders).
|
||||||
|
|
||||||
|
### Shared visual improvements:
|
||||||
|
- Unicode box-drawing borders (`┌─┬─┐`, `│`, `└─┴─┘`) via Ratatui's `Block::bordered()` with custom border set
|
||||||
|
- Bar graphs using `▁▂▃▄▅▆▇█` block characters
|
||||||
|
- Consistent 2-char padding inside bordered sections
|
||||||
|
- Color gradients for intensity (heatmap, speed distribution)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Summary
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `src/app.rs` | Start in lesson mode, add `depressed_keys: HashSet<char>`, `last_key_time`, `history_selected`, `history_confirm_delete`, `delete_session()` with chronological replay via `rebuild_from_history()` |
|
||||||
|
| `src/main.rs` | Enable keyboard enhancement flags, handle Press/Release events, update `render_lesson` for responsive tiers, update `handle_stats_key` for history selection/deletion state machine |
|
||||||
|
| `src/event.rs` | Filter key events by kind (pass all events, let main.rs handle kind) |
|
||||||
|
| `src/session/input.rs` | Add `typo_flags` tracking — insert on incorrect, preserve through backspace |
|
||||||
|
| `src/session/lesson.rs` | Add `typo_flags: HashSet<usize>`, `typo_count()` method. Keep `accuracy()`/`incorrect_count()` for live display. |
|
||||||
|
| `src/session/result.rs` | Use `typo_flags.len()` for final `incorrect` count and accuracy |
|
||||||
|
| `src/ui/layout.rs` | Add `LayoutTier` enum, compute from area dimensions, return different constraint sets |
|
||||||
|
| `src/ui/components/keyboard_diagram.rs` | Accept `depressed_keys: &HashSet<char>`, render depressed state, add compact mode |
|
||||||
|
| `src/ui/components/stats_dashboard.rs` | Full overhaul: bordered tables, bar graphs, progress bars, row selection, delete confirmation overlay, character speed distribution |
|
||||||
|
| `src/ui/components/activity_heatmap.rs` | New: 7-month activity calendar heatmap widget |
|
||||||
|
| `src/ui/components/stats_sidebar.rs` | Compact single-line mode for medium terminals |
|
||||||
|
| `src/ui/components/typing_area.rs` | Verify wrapping at narrow widths |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Manual Testing:
|
||||||
|
1. **Start in drill:** Launch app → immediately in Adaptive typing lesson, no menu
|
||||||
|
2. **Error tracking:** Type wrong char, backspace, type correct char → accuracy < 100%, error count ≥ 1. Type wrong at same pos twice, backspace twice, type correct → still only 1 error for that position.
|
||||||
|
3. **Keyboard:** Type characters → pressed key visually highlights. Next expected key highlighted. Releasing key clears highlight (or after 150ms fallback).
|
||||||
|
4. **Responsive:** Resize terminal to 50×15, 80×25, 120×40, 200×50 → layout adapts, no panics, no overlapping text
|
||||||
|
5. **Delete sessions:** Stats → History → select row → press `x` → confirm dialog → press `y` → session gone, all stats recalculated. Verify key_stats and letter_unlock are consistent.
|
||||||
|
6. **Statistics:** Visual inspection of bordered tables, bar graphs, activity heatmap, progress bars
|
||||||
|
|
||||||
|
### Automated Tests:
|
||||||
|
- `session/lesson.rs`: typo_flags behavior (wrong→backspace→correct counts as error, multiple errors at same pos = 1 typo)
|
||||||
|
- `session/input.rs`: process_char sets typo_flags, process_backspace preserves them
|
||||||
|
- `app.rs`: delete_session recalculates total_lessons, total_score, key_stats, letter_unlock, streak fields
|
||||||
|
- `engine/key_stats.rs`: verify rebuild from scratch produces same results as incremental updates (within EMA tolerance)
|
||||||
507
docs/plans/2026-02-15-skill-tree-progression-system.md
Normal file
507
docs/plans/2026-02-15-skill-tree-progression-system.md
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
# Skill Tree Progression System & Whitespace Support
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
keydr currently tracks only a-z lowercase letters in its adaptive unlock system. Since keydr aims to be a coding-focused typing tutor, it must also train capitals, numbers, punctuation, whitespace (tabs/newlines), and code-specific symbols. The current flat a-z progression needs to be replaced with a branching skill tree that lets players choose their training path after mastering lowercase letters. Additionally, code drills currently strip newlines into spaces, making them unrealistic for real-world code practice.
|
||||||
|
|
||||||
|
## Skill Tree Structure
|
||||||
|
|
||||||
|
The tree is flat: a-z is the root, and all other branches are direct siblings at the same level. Once a-z is complete, all branches unlock simultaneously and the user can choose any order.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ a-z Lowercase │ (ROOT - everyone starts here)
|
||||||
|
│ 26 keys, freq │
|
||||||
|
│ order unlock │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌─────────┬──────────┼──────────┬──────────┐
|
||||||
|
▼ ▼ ▼ ▼ ▼
|
||||||
|
┌─────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐
|
||||||
|
│Capitals │ │Numbers │ │ Prose │ │White- │ │ Code │
|
||||||
|
│ A-Z │ │ 0-9 │ │ Punct. │ │ space │ │ Symbols │
|
||||||
|
│ 3 lvls │ │ 2 lvls │ │ 3 lvls │ │ 2 lvls │ │ 4 lvls │
|
||||||
|
└─────────┘ └────────┘ └────────┘ └────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **a-z Lowercase** (root): Always available from start
|
||||||
|
- **All other branches**: Require a-z complete (all 26 lowercase letters confident). Once a-z is done, all 5 branches unlock simultaneously. User freely chooses which to pursue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branch Status State Machine
|
||||||
|
|
||||||
|
Each branch has an explicit status:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum BranchStatus {
|
||||||
|
Locked, // Prerequisites not met
|
||||||
|
Available, // Prerequisites met, user hasn't started
|
||||||
|
InProgress, // User has begun drilling this branch
|
||||||
|
Complete, // All levels in branch are done
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transitions:**
|
||||||
|
- `Locked → Available`: When a-z branch reaches `Complete`
|
||||||
|
- `Available → InProgress`: **Only** when user explicitly launches a branch drill from the skill tree (start-on-select model). The global adaptive drill does NOT auto-start branches.
|
||||||
|
- `InProgress → Complete`: When all keys in all levels of the branch reach confidence >= 1.0
|
||||||
|
|
||||||
|
**Multiple branches active**: Yes. The user can have multiple branches `InProgress` simultaneously. Each tracks its own current level independently.
|
||||||
|
|
||||||
|
**Global adaptive scope**: Only includes keys from `InProgress` and `Complete` branches. `Available` branches are not included — the user must visit the skill tree to start them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Level Breakdown
|
||||||
|
|
||||||
|
### Branch: a-z Lowercase (Root)
|
||||||
|
|
||||||
|
Uses existing frequency-order system. Starts with 6 keys, unlocks one at a time when all current keys reach confidence >= 1.0. Branch is "complete" when all 26 letters are confident.
|
||||||
|
|
||||||
|
Order: `e t a o i n s h r d l c u m w f g y p b v k j x q z`
|
||||||
|
|
||||||
|
Total keys: **26**
|
||||||
|
|
||||||
|
### Branch: Capital Letters (3 levels)
|
||||||
|
|
||||||
|
- **Level 1 — Common Sentence Capitals** (8 keys): `T I A S W H B M`
|
||||||
|
- **Level 2 — Name Capitals** (10 keys): `J D R C E N P L F G`
|
||||||
|
- **Level 3 — Remaining Capitals** (8 keys): `O U K V Y X Q Z`
|
||||||
|
|
||||||
|
Total keys: **26**
|
||||||
|
|
||||||
|
Text generation rules:
|
||||||
|
- First word of each "sentence" (after `.` `?` `!` or at drill start) gets capitalized
|
||||||
|
- ~10-15% of words get capitalized as proper-noun-like words
|
||||||
|
- Focused capital letter is boosted (40% chance to appear in word starts)
|
||||||
|
|
||||||
|
### Branch: Numbers (2 levels)
|
||||||
|
|
||||||
|
- **Level 1 — Common Digits** (5 keys): `1 2 3 4 5`
|
||||||
|
- **Level 2 — All Digits** (5 keys): `0 6 7 8 9`
|
||||||
|
|
||||||
|
Total keys: **10**
|
||||||
|
|
||||||
|
Text generation rules:
|
||||||
|
- ~15% of words replaced with number expressions using only unlocked digits
|
||||||
|
- Patterns: counts ("3 items"), years ("2024"), IDs ("room 42"), measurements ("7 miles")
|
||||||
|
|
||||||
|
### Branch: Prose Punctuation (3 levels)
|
||||||
|
|
||||||
|
- **Level 1 — Essential** (3 keys): `. , '`
|
||||||
|
- **Level 2 — Common** (4 keys): `; : " -`
|
||||||
|
- **Level 3 — Expressive** (4 keys): `? ! ( )`
|
||||||
|
|
||||||
|
Total keys: **11**
|
||||||
|
|
||||||
|
Text generation rules follow natural prose patterns:
|
||||||
|
- `.` ends sentences (every 5-15 words), `,` separates clauses
|
||||||
|
- `'` in contractions (don't, it's, we'll)
|
||||||
|
- `"` wrapping quoted phrases, `;` between clauses, `:` before lists
|
||||||
|
- `-` in compound words (well-known), `?` for questions, `!` for exclamations
|
||||||
|
- `( )` for parenthetical asides
|
||||||
|
|
||||||
|
### Branch: Whitespace (2 levels)
|
||||||
|
|
||||||
|
- **Level 1 — Enter/Return** (1 key): `\n`
|
||||||
|
- **Level 2 — Tab/Indent** (1 key): `\t`
|
||||||
|
|
||||||
|
Total keys: **2**
|
||||||
|
|
||||||
|
Text generation rules:
|
||||||
|
- Line breaks at sentence boundaries (every ~60-80 chars)
|
||||||
|
- Tabs for indentation in code-like structures
|
||||||
|
- Once unlocked, **default adaptive drills automatically become multi-line**
|
||||||
|
|
||||||
|
### Branch: Code Symbols (4 levels)
|
||||||
|
|
||||||
|
- **Level 1 — Arithmetic & Assignment** (5 keys): `= + * /` and `-` (shared with Prose Punct L2)
|
||||||
|
- **Level 2 — Grouping** (6 keys): `{ } [ ] < >`
|
||||||
|
- **Level 3 — Logic & Reference** (5 keys): `& | ^ ~` and `!` (shared with Prose Punct L3)
|
||||||
|
- **Level 4 — Special** (7 keys): `` @ # $ % _ \ ` ``
|
||||||
|
|
||||||
|
Total keys: **23** (21 unique + 2 shared with Prose Punctuation)
|
||||||
|
|
||||||
|
Text generation rules:
|
||||||
|
- L1: Prose with simple expressions (`x = a + b`, `total = price * qty`)
|
||||||
|
- L2: Code-pattern templates (`if (x) { return y; }`, `arr[0]`)
|
||||||
|
- L3: Bitwise/logical patterns (`a & b`, `!flag`, `*ptr`)
|
||||||
|
- L4: Language-specific patterns (`@decorator`, `#include`, `snake_case`)
|
||||||
|
|
||||||
|
**Grand total**: 98 keys across branches, **96 unique keys** (after deducting 2 shared: `-` and `!`). `TOTAL_UNIQUE_KEYS` is derived at startup by collecting all keys from all branch definitions into a `HashSet` and taking `len()`. Stored as a field on `SkillTree` for use in scoring and UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shared Keys Between Branches
|
||||||
|
|
||||||
|
Two keys appear in multiple branches:
|
||||||
|
- `-` appears in Prose Punctuation L2 and Code Symbols L1
|
||||||
|
- `!` appears in Prose Punctuation L3 and Code Symbols L3
|
||||||
|
|
||||||
|
**Rule**: Confidence is tracked once per character in `KeyStatsStore` (keyed by `char`). If a user masters `-` in Prose Punctuation, it is automatically confident in Code Symbols too. When checking level completion, the branch reads the single confidence value for that char. This is idempotent — no special handling needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Focused Key Policy
|
||||||
|
|
||||||
|
### Global Adaptive Drill (from menu)
|
||||||
|
|
||||||
|
1. Collect all keys from all `InProgress` branches (current level's keys only) plus all `Complete` branch keys
|
||||||
|
2. Find the key with the **lowest confidence < 1.0** across this entire set
|
||||||
|
3. If all keys are confident, no focused key (maintenance mode)
|
||||||
|
4. Boost the focused key in text generation (40% probability)
|
||||||
|
|
||||||
|
### Branch-Specific Drill (from skill tree)
|
||||||
|
|
||||||
|
1. Collect keys from the selected branch including **all prior completed levels** (as background reinforcement) plus the **current level's keys**, plus all a-z keys
|
||||||
|
2. Find the key with the **lowest confidence < 1.0** within the **current level keys only** (prior level keys are reinforcement, not focus targets)
|
||||||
|
3. If all current level keys are confident, advance the level and focus on the weakest new key
|
||||||
|
4. Boost the focused key in text generation (40% probability)
|
||||||
|
5. Prior-level keys always appear in generated text for reinforcement but are never the focused key
|
||||||
|
|
||||||
|
### Branches with Zero Progress
|
||||||
|
|
||||||
|
When a branch is `Available` but user hasn't started it yet:
|
||||||
|
- Launching a drill from that branch transitions it to `InProgress` at level 1
|
||||||
|
- The focused key is the weakest among level 1's keys (likely all at 0.0 confidence, so pick the first in definition order)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scoring
|
||||||
|
|
||||||
|
Current formula: `complexity = unlocked_count / 26`
|
||||||
|
|
||||||
|
**New formula**: `complexity = total_unlocked_keys / TOTAL_UNIQUE_KEYS`
|
||||||
|
|
||||||
|
Where `TOTAL_UNIQUE_KEYS = 96` is computed from branch definitions (deduplicated across shared keys). This scales naturally — the more branches the user has unlocked, the higher the complexity multiplier.
|
||||||
|
|
||||||
|
Level formula remains: `level = floor(sqrt(total_score / 100))`.
|
||||||
|
|
||||||
|
Menu header changes from `"X/26 letters"` to `"X/96 keys"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Skill Tree UI
|
||||||
|
|
||||||
|
### New Screen: `AppScreen::SkillTree`
|
||||||
|
|
||||||
|
Accessible from menu via `[t] Skill Tree`. Renders **vertically** as a scrollable list.
|
||||||
|
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
║ SKILL TREE ║
|
||||||
|
╠══════════════════════════════════════════════════════════════════╣
|
||||||
|
║ ║
|
||||||
|
║ ★ Lowercase a-z COMPLETE 26/26 ║
|
||||||
|
║ ████████████████████████████████████████ Level 26/26 ║
|
||||||
|
║ ║
|
||||||
|
║ ── Branches (unlocked after a-z) ────────────────────────── ║
|
||||||
|
║ ║
|
||||||
|
║ ► Capitals A-Z Lvl 2/3 18/26 keys ║
|
||||||
|
║ ████████████████████░░░░░░░░░░░░ 69% ║
|
||||||
|
║ ║
|
||||||
|
║ Numbers 0-9 Lvl 0/2 0/10 keys ║
|
||||||
|
║ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% ║
|
||||||
|
║ ║
|
||||||
|
║ Prose Punctuation Lvl 1/3 3/11 keys ║
|
||||||
|
║ ██████████░░░░░░░░░░░░░░░░░░░░░ 27% ║
|
||||||
|
║ ║
|
||||||
|
║ Whitespace Lvl 0/2 0/2 keys ║
|
||||||
|
║ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% ║
|
||||||
|
║ ║
|
||||||
|
║ Code Symbols Lvl 0/4 0/23 keys ║
|
||||||
|
║ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% ║
|
||||||
|
║ ║
|
||||||
|
╠══════════════════════════════════════════════════════════════════╣
|
||||||
|
║ ► Capitals A-Z Level 2/3 ║
|
||||||
|
║ L1: T I A S W H B M (complete) ║
|
||||||
|
║ L2: J [D] R C E N P L F G (in progress, focused: D) ║
|
||||||
|
║ L3: O U K V Y X Q Z (locked) ║
|
||||||
|
║ Avg Confidence: ████████░░ 82% ║
|
||||||
|
║ ║
|
||||||
|
║ [Enter] Start Drill [↑↓/jk] Navigate [q] Back ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
- **Top section**: Vertical list of all branches with status prefix, level, key count, progress bar
|
||||||
|
- **Bottom section**: Detail panel showing per-level key breakdown, confidence bars, focused key
|
||||||
|
- **Footer**: Controls
|
||||||
|
|
||||||
|
**Node states (prefix):**
|
||||||
|
- Locked: grayed out, no prefix, not selectable
|
||||||
|
- Available: normal color, no prefix
|
||||||
|
- In Progress `►`: accent color
|
||||||
|
- Complete `★`: gold/green
|
||||||
|
|
||||||
|
**Navigation:** `↑↓` / `j/k` move selection. `Enter` launches branch drill. `q` returns to menu.
|
||||||
|
|
||||||
|
**Keyboard diagram**: For non-printable keys (`Enter`, `Tab`), show them as labeled keys on the keyboard diagram in their standard positions. No special handling needed — they're physical keys with fixed positions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code & Passage Drill Changes (Unranked Modes)
|
||||||
|
|
||||||
|
Code and Passage drills remain as separate menu options.
|
||||||
|
|
||||||
|
1. **Unranked tagging**: Add `ranked: bool` to `DrillResult` with `#[serde(default = "default_true")]` for backward compat
|
||||||
|
2. **Derive ranked from DrillContext**: At drill start, set `ranked = (drill_mode == Adaptive)`. Code/Passage → `ranked = false`.
|
||||||
|
3. **No progression**: `finish_drill()` gates skill tree updates on `result.ranked`
|
||||||
|
4. **History replay**: `rebuild_from_history()` uses `result.ranked` as the gate. No legacy fallback — since we reset on schema change (WIP policy), old history without `ranked` field won't exist.
|
||||||
|
5. **Visual indicators**:
|
||||||
|
- Drill header: "Code Drill (Unranked)" / "Passage Drill (Unranked)" in dimmed/muted color
|
||||||
|
- Result screen: "Unranked — does not count toward skill tree"
|
||||||
|
- Stats dashboard history: unranked rows shown with muted styling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Whitespace Handling
|
||||||
|
|
||||||
|
### Tokenized Render Model (`typing_area.rs`)
|
||||||
|
|
||||||
|
Replace direct char→span rendering with a `RenderToken` approach to handle one-to-many char-to-cell mapping:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct RenderToken {
|
||||||
|
target_idx: usize, // Index into DrillState.target
|
||||||
|
display: String, // What to show (e.g., "↵", "→···", "a")
|
||||||
|
style: Style, // Computed style (correct/incorrect/cursor/pending)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Display mapper:**
|
||||||
|
- `\n` → visible `↵` marker token + hard line break (new `Line` in paragraph)
|
||||||
|
- `\t` → visible `→` marker + padding `·` tokens to next 4-char tab stop
|
||||||
|
- All other chars → single token with char as display
|
||||||
|
|
||||||
|
**Cursor/style mapping:** Maintain a `Vec<(usize, usize)>` mapping from `target_idx` to first display cell position. When highlighting cursor or errors, look up the target index to find which display tokens to style.
|
||||||
|
|
||||||
|
**Multi-line rendering:** Change from single `Line` to `Vec<Line>`. Split on newline tokens. Each line is a separate `Line` in the `Paragraph`.
|
||||||
|
|
||||||
|
### Input Pipeline (`main.rs` + `session/input.rs`)
|
||||||
|
|
||||||
|
Current flow: `main.rs` matches `KeyCode::Char(ch)` → `app.type_char(ch)`. Enter/Tab are currently consumed by other handlers (menu nav, etc.).
|
||||||
|
|
||||||
|
**Changes in `main.rs`:**
|
||||||
|
- When `screen == Drill` and drill is active:
|
||||||
|
- `KeyCode::Enter` → `app.type_char('\n')` **unconditionally** (correctness decided by `process_char()`)
|
||||||
|
- `KeyCode::Tab` → `app.type_char('\t')` **unconditionally** (correctness decided by `process_char()`)
|
||||||
|
- `KeyCode::BackTab` (Shift+Tab) → ignore (no action)
|
||||||
|
- These must be checked **before** the existing Esc/Enter handlers for drill screen
|
||||||
|
- If Enter/Tab is typed when not expected, it registers as an error on the current char — same as typing any wrong key
|
||||||
|
|
||||||
|
**No changes to `session/input.rs`**: `process_char()` already compares `ch == expected` generically. It will work with `'\n'` and `'\t'` as-is.
|
||||||
|
|
||||||
|
### Code Drill Updates (`generator/code_syntax.rs`)
|
||||||
|
|
||||||
|
- Embedded snippets change from single-line `&str` to multi-line string literals with preserved indentation
|
||||||
|
- `extract_code_snippets()`: preserve original newlines and leading whitespace instead of `split_whitespace().join(" ")`
|
||||||
|
- `generate()`: join snippets with `\n\n` instead of `" "`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model Changes
|
||||||
|
|
||||||
|
### Persistence Policy (WIP stage)
|
||||||
|
|
||||||
|
**No backward compatibility migration.** On schema mismatch, reset persisted files to defaults. Bump schema version to 2. Add a note in changelog that local progress is intentionally reset for this version. This avoids over-engineering migration logic during early development.
|
||||||
|
|
||||||
|
### `ProfileData` (schema v2)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ProfileData {
|
||||||
|
pub schema_version: u32, // 2
|
||||||
|
pub skill_tree: SkillTreeProgress, // Replaces unlocked_letters
|
||||||
|
pub total_score: f64,
|
||||||
|
pub total_drills: u32,
|
||||||
|
pub streak_days: u32,
|
||||||
|
pub best_streak: u32,
|
||||||
|
pub last_practice_date: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SkillTreeProgress {
|
||||||
|
pub branches: HashMap<String, BranchProgress>, // String keys for stable JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct BranchProgress {
|
||||||
|
pub status: BranchStatus,
|
||||||
|
pub current_level: usize, // 0-indexed into branch's levels array
|
||||||
|
// current_level = 0 means working on first level (plan's "Level 1")
|
||||||
|
// current_level = levels.len() only when status == Complete
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Indexing invariant**: `current_level` is always 0-indexed into `BranchDefinition.levels`. When the plan says "Level 1", "Level 2", etc. in human-readable text, that maps to `current_level = 0`, `current_level = 1`, etc. in code. A branch with `current_level = 0` and `status = InProgress` is actively working on its first level.
|
||||||
|
|
||||||
|
**HashMap uses `String` keys** (e.g., `"lowercase"`, `"capitals"`, `"numbers"`, etc.) for stable JSON serialization. `BranchId` enum has `to_key() -> &'static str` and `from_key()` methods.
|
||||||
|
|
||||||
|
### `DrillResult` Addition
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub ranked: bool,
|
||||||
|
```
|
||||||
|
|
||||||
|
### `KeyStatsStore`
|
||||||
|
|
||||||
|
No structural change. Already `HashMap<char, KeyStat>` — works for any char.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Skill Tree Definition (Source of Truth)
|
||||||
|
|
||||||
|
Hard-coded static definition in `src/engine/skill_tree.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct BranchDefinition {
|
||||||
|
pub id: BranchId,
|
||||||
|
pub name: &'static str,
|
||||||
|
pub levels: Vec<LevelDefinition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LevelDefinition {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub keys: Vec<char>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All branch/level/key definitions are `const`/`static` arrays. No data-driven manifest needed at this stage. The `SkillTree` struct holds:
|
||||||
|
- The static definition (reference)
|
||||||
|
- The persisted `SkillTreeProgress` (mutable state)
|
||||||
|
- Methods: `unlocked_keys(scope)`, `focused_key(scope, &KeyStatsStore)`, `update(&KeyStatsStore)`, `branch_status(id)`, `all_branches()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Skill Tree Core & Data Model
|
||||||
|
|
||||||
|
**Goal**: Replace `LetterUnlock` with `SkillTree`, update persistence.
|
||||||
|
|
||||||
|
1. Create `src/engine/skill_tree.rs`:
|
||||||
|
- `BranchId` enum (`Lowercase, Capitals, Numbers, ProsePunctuation, Whitespace, CodeSymbols`)
|
||||||
|
- `BranchStatus` enum (`Locked, Available, InProgress, Complete`)
|
||||||
|
- `BranchDefinition`, `LevelDefinition` structs
|
||||||
|
- Static branch definitions with all keys per level
|
||||||
|
- `SkillTree` struct with `update()`, `unlocked_keys()`, `focused_key()`, `branch_status()`
|
||||||
|
2. Update `src/store/schema.rs`: new `ProfileData` with `SkillTreeProgress`, schema v2, reset on mismatch
|
||||||
|
3. Add `ranked: bool` to `DrillResult` in `src/session/result.rs`
|
||||||
|
4. Update `src/app.rs`: replace `letter_unlock: LetterUnlock` with `skill_tree: SkillTree`, update `finish_drill()` to gate on `ranked`, update `rebuild_from_history()`, update scoring complexity formula
|
||||||
|
5. Delete/replace `src/engine/letter_unlock.rs`
|
||||||
|
|
||||||
|
**Key files**: `src/engine/skill_tree.rs` (new), `src/engine/letter_unlock.rs` (delete), `src/store/schema.rs`, `src/session/result.rs`, `src/app.rs`
|
||||||
|
|
||||||
|
**Tests**:
|
||||||
|
- Skill tree status transitions (Locked → Available → InProgress → Complete)
|
||||||
|
- Shared key confidence propagation
|
||||||
|
- Focused key selection (global vs branch scope)
|
||||||
|
- Level completion and advancement
|
||||||
|
- Schema reset on version mismatch
|
||||||
|
|
||||||
|
**Acceptance criteria**: `cargo build` passes, `cargo test` passes, existing adaptive drills work with skill tree (a-z only), scoring uses new formula.
|
||||||
|
|
||||||
|
### Phase 2: Whitespace Input & Rendering
|
||||||
|
|
||||||
|
**Goal**: Support Enter/Tab in typing drills with proper display.
|
||||||
|
|
||||||
|
1. Update `src/ui/components/typing_area.rs`: tokenized render model with `RenderToken`, multi-line support, visible `↵` and `→` markers
|
||||||
|
2. Update `src/main.rs`: route `KeyCode::Enter` → `'\n'` and `KeyCode::Tab` → `'\t'` when in drill mode, ignore `BackTab`
|
||||||
|
3. Update `src/generator/code_syntax.rs`: preserve newlines/indentation in snippets, change embedded snippets to multi-line, fix `extract_code_snippets()` to preserve whitespace
|
||||||
|
4. Optionally update `src/generator/passage.rs` with multi-line passage variants
|
||||||
|
|
||||||
|
**Key files**: `src/ui/components/typing_area.rs`, `src/main.rs`, `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
**Tests**:
|
||||||
|
- RenderToken generation for strings with `\n` and `\t`
|
||||||
|
- Cursor position mapping with expanded tokens
|
||||||
|
- Enter/Tab input processing (reuse existing `process_char()` — just verify `'\n'` and `'\t'` work)
|
||||||
|
|
||||||
|
**Acceptance criteria**: Code drills display multi-line with visible whitespace markers, Enter/Tab advance the cursor correctly, backspace works across line boundaries.
|
||||||
|
|
||||||
|
### Phase 3: Text Generation for Capitals & Punctuation
|
||||||
|
|
||||||
|
**Goal**: Generate drill text that naturally incorporates capitals and punctuation.
|
||||||
|
|
||||||
|
1. Create `src/generator/capitalize.rs`: post-processing pass that capitalizes sentence starts and occasional words, using only unlocked capital letters
|
||||||
|
2. Create `src/generator/punctuate.rs`: post-processing pass that inserts periods, commas, apostrophes, etc. at natural positions, using only unlocked punctuation
|
||||||
|
3. Update `src/generator/phonetic.rs` or `src/app.rs` `generate_text()`: apply capitalize/punctuate passes when those branches are active
|
||||||
|
4. Update `src/engine/filter.rs` `CharFilter`: add awareness of which char types are allowed (lowercase, uppercase, punctuation, etc.)
|
||||||
|
|
||||||
|
**Key files**: `src/generator/capitalize.rs` (new), `src/generator/punctuate.rs` (new), `src/generator/phonetic.rs`, `src/app.rs`, `src/engine/filter.rs`
|
||||||
|
|
||||||
|
**Acceptance criteria**: Adaptive drills with Capitals branch active produce properly capitalized text. Drills with Prose Punctuation active have natural punctuation placement.
|
||||||
|
|
||||||
|
### Phase 4: Text Generation for Numbers & Code Symbols
|
||||||
|
|
||||||
|
**Goal**: Generate drill text with numbers and code symbol patterns.
|
||||||
|
|
||||||
|
1. Create `src/generator/numbers.rs`: injects number expressions into prose using only unlocked digits
|
||||||
|
2. Create `src/generator/code_patterns.rs`: code-pattern templates for Code Symbols branch drills (expressions, brackets, operators)
|
||||||
|
3. Update `src/app.rs` `generate_text()`: apply number/code passes based on active branches
|
||||||
|
4. For whitespace branch: when active, insert `\n` at sentence boundaries in generated text
|
||||||
|
|
||||||
|
**Key files**: `src/generator/numbers.rs` (new), `src/generator/code_patterns.rs` (new), `src/app.rs`
|
||||||
|
|
||||||
|
**Acceptance criteria**: Number expressions use only unlocked digits. Code symbol drills produce recognizable code-like patterns. Whitespace branch generates multi-line output.
|
||||||
|
|
||||||
|
### Phase 5: Skill Tree UI
|
||||||
|
|
||||||
|
**Goal**: Navigable skill tree screen with branch detail and drill launch.
|
||||||
|
|
||||||
|
1. Add `AppScreen::SkillTree` to `src/app.rs`
|
||||||
|
2. Create `src/ui/components/skill_tree.rs`: vertical branch list + detail panel widget
|
||||||
|
3. Update `src/main.rs`: handle key events for skill tree screen (navigation, drill launch)
|
||||||
|
4. Update `src/ui/components/menu.rs`: add `[t] Skill Tree` option
|
||||||
|
5. Update menu header: show `"X/96 keys"` instead of `"X/26 letters"`
|
||||||
|
6. Add `DrillMode::BranchDrill(BranchId)` or similar to track drill origin for branch-specific focus
|
||||||
|
|
||||||
|
**Key files**: `src/ui/components/skill_tree.rs` (new), `src/app.rs`, `src/main.rs`, `src/ui/components/menu.rs`
|
||||||
|
|
||||||
|
**Acceptance criteria**: Can navigate to skill tree from menu, see all branches with correct status, launch a branch-specific drill, return to menu.
|
||||||
|
|
||||||
|
### Phase 6: Unranked Mode Polish
|
||||||
|
|
||||||
|
**Goal**: Clearly distinguish ranked vs unranked drills in UI.
|
||||||
|
|
||||||
|
1. Update drill header in `src/main.rs`: show "(Unranked)" for Code/Passage modes
|
||||||
|
2. Update `src/ui/components/dashboard.rs` result screen: note "does not count toward skill tree"
|
||||||
|
3. Update `src/ui/components/stats_dashboard.rs`: muted styling for unranked history rows
|
||||||
|
4. Verify `rebuild_from_history()` correctly uses `ranked` field to gate skill tree updates
|
||||||
|
|
||||||
|
**Key files**: `src/main.rs`, `src/ui/components/dashboard.rs`, `src/ui/components/stats_dashboard.rs`, `src/app.rs`
|
||||||
|
|
||||||
|
**Acceptance criteria**: Code/Passage drills clearly marked unranked. Stats history shows visual distinction. Ranked drills advance skill tree, unranked don't.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Automated Tests
|
||||||
|
|
||||||
|
- **Skill tree transitions**: `Locked → Available → InProgress → Complete` for each branch
|
||||||
|
- **Shared keys**: Mastering `!` in Prose Punct → confident in Code Symbols too
|
||||||
|
- **Focused key**: Global scope selects weakest across all active branches; branch scope selects within branch
|
||||||
|
- **Level advancement**: Completing all keys in a level auto-advances to next
|
||||||
|
- **Ranked/unranked**: Only ranked drills update skill tree in `rebuild_from_history()`
|
||||||
|
- **Whitespace tokens**: RenderToken expansion for `\n` and `\t` produces correct display strings and index mapping
|
||||||
|
- **Input routing**: `'\n'` and `'\t'` correctly processed as typed characters
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
|
||||||
|
1. Launch app → a-z trunk works as before
|
||||||
|
2. Complete a-z (or edit profile to simulate) → all 5 branches show as Available
|
||||||
|
3. Navigate skill tree → select Capitals → launch drill → see capitalized text
|
||||||
|
4. Complete Capitals L1 → L2 keys appear in drills
|
||||||
|
5. Launch default adaptive with multiple branches active → text mixes all unlocked keys
|
||||||
|
6. Launch Code/Passage drill → header shows "(Unranked)", no skill tree progress
|
||||||
|
7. Start Whitespace branch → default adaptive becomes multi-line
|
||||||
|
8. Type Enter/Tab in code drills → cursor advances correctly, errors tracked
|
||||||
|
9. Quit and relaunch → progress preserved
|
||||||
|
10. Delete `~/.local/share/keydr/` → app resets cleanly to fresh state
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# Skill Tree Integration Fixes & UI Improvements
|
||||||
|
|
||||||
|
## Context
|
||||||
|
After adding a skill tree progression system, several parts of the app weren't fully integrated. This plan addresses 7 issues: progress bar confusion, broken skill tree bars, missing selectability, duplicate displays, incomplete keyboard visualization, code drill formatting issues, and a missing menu shortcut.
|
||||||
|
|
||||||
|
## Architecture Foundations
|
||||||
|
|
||||||
|
### A. Layout-Driven Keyboard Model
|
||||||
|
**Files:** `src/keyboard/layout.rs`, new `src/keyboard/model.rs`
|
||||||
|
|
||||||
|
The existing `KeyboardLayout` in `layout.rs` only stores `Vec<Vec<char>>` (base layer). We need a shared model used by both drill and stats keyboards.
|
||||||
|
|
||||||
|
Create `src/keyboard/model.rs`:
|
||||||
|
- `PhysicalKey { base: char, shifted: char }` - represents one physical key with both layers
|
||||||
|
- `KeyboardModel { rows: Vec<Vec<PhysicalKey>> }` - full keyboard definition
|
||||||
|
- Factory methods: `KeyboardModel::qwerty()`, `::dvorak()`, `::colemak()` - each returns the full layout
|
||||||
|
- Helper: `base_to_shifted(ch) -> Option<char>` and `shifted_to_base(ch) -> Option<char>` derived from the model
|
||||||
|
- Helper: `physical_key_for(ch) -> Option<&PhysicalKey>` - lookup by either base or shifted char
|
||||||
|
|
||||||
|
The QWERTY model:
|
||||||
|
```
|
||||||
|
Row 0 (number): (`~) (1!) (2@) (3#) (4$) (5%) (6^) (7&) (8*) (9() (0)) (-_) (=+)
|
||||||
|
Row 1 (top): (qQ) (wW) (eE) (rR) (tT) (yY) (uU) (iI) (oO) (pP) ([{) (]}) (\|)
|
||||||
|
Row 2 (home): (aA) (sS) (dD) (fF) (gG) (hH) (jJ) (kK) (lL) (;:) ('")
|
||||||
|
Row 3 (bottom): (zZ) (xX) (cC) (vV) (bB) (nN) (mM) (,<) (.>) (/?)
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `KeyboardLayout` to use `KeyboardModel` internally (or replace it).
|
||||||
|
|
||||||
|
Replace `qwerty_finger(ch)` with a layout-aware API:
|
||||||
|
- `KeyboardModel::finger_for(&self, key: &PhysicalKey) -> FingerAssignment` - each layout defines finger assignments per physical key position (row, col)
|
||||||
|
- For shifted chars, callers first resolve to physical key via `physical_key_for(ch)`, then look up finger
|
||||||
|
- This eliminates the QWERTY-only char match and works for Dvorak/Colemak
|
||||||
|
|
||||||
|
Load the active layout from `config.keyboard_layout` and pass it through to all keyboard rendering.
|
||||||
|
|
||||||
|
### B. Dual Progress Metrics
|
||||||
|
**File:** `src/engine/skill_tree.rs`
|
||||||
|
|
||||||
|
Add `branch_unlocked_count(id: BranchId) -> usize` method:
|
||||||
|
- Lowercase: delegates to `lowercase_unlocked_count()`
|
||||||
|
- Others: sums `keys.len()` for levels `0..=current_level` when InProgress; all keys when Complete; 0 otherwise
|
||||||
|
|
||||||
|
All UI uses two metrics per branch:
|
||||||
|
- **Unlocked**: `branch_unlocked_count(id)` / `branch_total_keys(id)` - how far through the branch
|
||||||
|
- **Mastered**: `branch_confident_keys(id, stats)` / `branch_total_keys(id)` - how many keys at confidence >= 1.0
|
||||||
|
|
||||||
|
### C. Code Language Config
|
||||||
|
**File:** `src/config.rs`
|
||||||
|
|
||||||
|
Replace the implicit `code_languages: Vec<String>` usage with a clearer model:
|
||||||
|
- Add `code_language: String` field (single language: "rust", "python", "javascript", "go", "all")
|
||||||
|
- Keep `code_languages` for backwards compat but derive from `code_language`
|
||||||
|
- Settings cycling and code generation both read `code_language`
|
||||||
|
- "all" picks a random language per drill in `generate_text()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Changes (in order)
|
||||||
|
|
||||||
|
### 1. Fix missing `[c] Settings` shortcut in menu footer
|
||||||
|
**File:** `src/main.rs` (`render_menu` function)
|
||||||
|
- Change footer string to: `" [1-3] Start [t] Skill Tree [s] Stats [c] Settings [q] Quit "`
|
||||||
|
- Verify no other footers are missing hints by checking all `render_*` functions
|
||||||
|
|
||||||
|
### 2. Fix duplicate fraction display on Lowercase branch
|
||||||
|
**File:** `src/ui/components/skill_tree.rs` (`render_branch_list`)
|
||||||
|
- Currently shows `"6/26 0/26 keys"` because status_text and confident/total are concatenated
|
||||||
|
- Change to single display: `"6/26 unlocked"` when no mastered keys, or `"6/26 unlocked (3 mastered)"` when some exist
|
||||||
|
- Apply same pattern to all branches: `"Lvl 1/3 5/10 unlocked (2 mastered)"`
|
||||||
|
|
||||||
|
### 3. Make Lowercase a-z selectable in skill tree
|
||||||
|
**Files:** `src/ui/components/skill_tree.rs`, `src/main.rs` (`handle_skill_tree_key`)
|
||||||
|
|
||||||
|
- Add `BranchId::Lowercase` to `selectable_branches()` at index 0
|
||||||
|
- Merge the separate root Lowercase rendering (currently in `render_branch_list` lines 113-170) into the main branch loop
|
||||||
|
- Apply selection highlighting to Lowercase using same `is_selected` logic as other branches
|
||||||
|
- Keep "Branches (unlocked after a-z)" separator after Lowercase (index 0) and before Capitals (index 1)
|
||||||
|
- Detail panel for Lowercase: show progressive unlock state `"Unlocked 6/26 letters"` instead of `"Level 1/1"`. Show each unlocked key with its confidence, locked keys dimmed
|
||||||
|
- Enter on InProgress Lowercase starts branch drill (existing `start_branch_drill` handles this)
|
||||||
|
- Update `branch_list_height` calculation to account for the merged layout
|
||||||
|
|
||||||
|
### 4. Fix skill tree progress bars - combined unlocked/mastered bar
|
||||||
|
**Files:** `src/engine/skill_tree.rs`, `src/ui/components/skill_tree.rs`
|
||||||
|
|
||||||
|
- Add `branch_unlocked_count()` method (see Architecture B above)
|
||||||
|
- Change progress bars to a **combined dual-metric bar**: the bar is divided into three segments:
|
||||||
|
- Filled (accent color): mastered keys (confidence >= 1.0)
|
||||||
|
- Filled (dimmer color): unlocked but not yet mastered
|
||||||
|
- Empty (background): locked keys
|
||||||
|
- This works because mastered <= unlocked <= total always holds
|
||||||
|
- Update `progress_bar_str` to accept two ratios and render with two fill colors
|
||||||
|
- **Rounding rule**: compute cell counts from raw counts (not ratios) to avoid rounding violations:
|
||||||
|
- `mastered_cells = (mastered * width / total)` (floor)
|
||||||
|
- `unlocked_cells = (unlocked * width / total).max(mastered_cells)` (floor, clamped)
|
||||||
|
- `empty_cells = width - unlocked_cells`
|
||||||
|
- This guarantees `mastered_cells <= unlocked_cells <= width` with no overlap
|
||||||
|
- Text label shows: `"6/26 unlocked, 3 mastered"`
|
||||||
|
|
||||||
|
### 5. Add per-key mastery display in skill tree detail panel (phase 2 if time allows)
|
||||||
|
**File:** `src/ui/components/skill_tree.rs` (`render_detail_panel`)
|
||||||
|
|
||||||
|
- In the detail view for the selected branch, show a mini progress bar per key
|
||||||
|
- Each key shows: `char [====----] 75%` where the bar represents confidence (0-100%)
|
||||||
|
- Keys already at confidence >= 1.0 show as fully filled with success color
|
||||||
|
- Keys not yet unlocked show dimmed with "locked" label
|
||||||
|
- Focused key is highlighted (existing logic already identifies it)
|
||||||
|
- Layout: keys in their level groups, each on its own line with the mini bar
|
||||||
|
- Note: This adds UI complexity. Implement after core issues (1-4, 6-8) are stable.
|
||||||
|
|
||||||
|
### 6. Replace drill screen progress bar with per-branch progress
|
||||||
|
**Files:** `src/main.rs` (`render_drill`), new `src/ui/components/branch_progress_list.rs`
|
||||||
|
|
||||||
|
Create a new `BranchProgressList` widget (not stretching the existing `ProgressBar`):
|
||||||
|
- Shows one compact line per active branch (InProgress or Complete), plus an overall line
|
||||||
|
- Each line: `" ▶ Lowercase [████░░░░] 6/26"`
|
||||||
|
- Uses the combined dual-metric bar from Issue 4 (mastered vs unlocked segments)
|
||||||
|
- Active drill branch (from `app.drill_scope`) is highlighted with accent color and `▶` prefix
|
||||||
|
- Other branches use dimmer color and `·` prefix
|
||||||
|
|
||||||
|
Layout budgeting by `LayoutTier` (unbordered, plain lines to maximize density):
|
||||||
|
- **Wide** (height >= 25): show all active branches (InProgress/Complete). `Constraint::Length(active_count.min(6) as u16 + 1)` (+1 for "Overall" line)
|
||||||
|
- **Wide** (height 20-24): show active drill branch + overall only. `Constraint::Length(2)`
|
||||||
|
- **Medium**: show active drill branch only. `Constraint::Length(1)`
|
||||||
|
- **Narrow**: hide progress (current behavior)
|
||||||
|
|
||||||
|
### 7. Full keyboard visualization
|
||||||
|
**Files:** `src/keyboard/model.rs` (new), `src/keyboard/layout.rs` (update), `src/ui/components/keyboard_diagram.rs`, `src/ui/components/stats_dashboard.rs`, `src/main.rs`, `src/app.rs`
|
||||||
|
|
||||||
|
#### 7a. Build KeyboardModel (Architecture A above)
|
||||||
|
|
||||||
|
#### 7b. Drill keyboard
|
||||||
|
- `KeyboardDiagram` takes `&KeyboardModel` instead of hardcoded `ROWS`
|
||||||
|
- Add `shift_held: bool` field
|
||||||
|
- **Shift state handling**: Primary source is `key.modifiers.contains(KeyModifiers::SHIFT)` checked on every Press event. Set `app.shift_held = true` when modifier present, `false` when absent. Additionally, on tick (100ms), if `shift_held` is true and no key event has been received in 200ms, clear it as a fallback. This means: shifted display appears when a shifted key is pressed, and naturally clears on the next unshifted keypress or after timeout. Acceptance: brief flicker (1-2 frames) on quick shift+key combos is acceptable; sustained wrong state is not.
|
||||||
|
- When `shift_held`, display `physical_key.shifted` for each key; otherwise `physical_key.base`
|
||||||
|
- Full mode: 4 rows (number, top, home, bottom) + visual-only labels for Tab/Backspace/Shift/Enter at row edges
|
||||||
|
- Compact mode: 3 rows letters only (current behavior, but driven from `KeyboardModel`)
|
||||||
|
- Height: `Constraint::Length(7)` for full (4 rows + 2 border + label), `Constraint::Length(5)` for compact
|
||||||
|
- Replace `finger_color(ch)` with layout-aware `finger_for(model, physical_key) -> FingerAssignment` that works for any layout (see 7a)
|
||||||
|
- `is_unlocked` check: map the displayed char against `unlocked_keys` list
|
||||||
|
|
||||||
|
#### 7c. Stats keyboard heatmap
|
||||||
|
- Two sub-rows per physical row: top = shifted layer (dimmer styling), bottom = base layer
|
||||||
|
- Each cell shows char + accuracy % (existing format)
|
||||||
|
- Height: `Constraint::Length(12)` (4 physical rows x 2 sub-rows + 2 borders + header)
|
||||||
|
- Load from `KeyboardModel` based on `config.keyboard_layout`
|
||||||
|
- Accuracy lookup: use existing `get_key_accuracy(char)` for each layer independently
|
||||||
|
- **Width fallback**: if terminal width < 70, collapse to base layer only (hide shifted sub-rows). Existing min-width guard pattern from `render_keyboard_heatmap` (width < 50 => skip) is preserved.
|
||||||
|
|
||||||
|
### 8. Code drill improvements
|
||||||
|
**Files:** `src/generator/code_syntax.rs`, `src/app.rs`, `src/main.rs`, `src/config.rs`
|
||||||
|
|
||||||
|
#### 8a. Multi-line embedded snippets
|
||||||
|
- Reformat all snippets in `rust_snippets()`, `python_snippets()`, `javascript_snippets()`, `go_snippets()` to be multi-line with realistic formatting
|
||||||
|
- Go: use `\t` for indentation (gofmt convention)
|
||||||
|
- Rust/Python/JavaScript: use 4 spaces
|
||||||
|
- Keep Tab key input as literal `\t` (do NOT convert to spaces) - this is needed for whitespace branch progression and the typing area already renders tabs properly
|
||||||
|
- Add basic validation for fetched snippets: require at least one newline and reject snippets that are all on one line (filter in `extract_code_snippets`)
|
||||||
|
|
||||||
|
#### 8b. Language selection screen
|
||||||
|
- Add `AppScreen::CodeLanguageSelect` to `AppScreen` enum
|
||||||
|
- Add `code_language_selected: usize` to `App`
|
||||||
|
- Screen flow: Menu `'2'` or Enter on "Code Drill" -> `CodeLanguageSelect` -> select language -> start drill
|
||||||
|
- ESC from language select returns to Menu
|
||||||
|
- Direct hotkeys in language select: `1`=Rust, `2`=Python, `3`=JavaScript, `4`=Go, `5`=All
|
||||||
|
- Enter confirms selection
|
||||||
|
- Arrow keys / j/k navigate
|
||||||
|
- Default selection: whichever language matches current `config.code_language`
|
||||||
|
- On confirm: update `config.code_language`, save config, set `drill_mode = Code`, start drill
|
||||||
|
- Render: centered bordered box with language list, highlighting selected item, showing `(current)` next to the default
|
||||||
|
|
||||||
|
#### 8c. Config changes
|
||||||
|
- Add `code_language: String` field to Config with default "rust"
|
||||||
|
- Settings screen language cycling updates `code_language`
|
||||||
|
- `generate_text` for Code mode reads `code_language` (if "all", picks random)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- `cargo build` -- no compilation errors
|
||||||
|
- `cargo test` -- existing tests pass; add tests for:
|
||||||
|
- `branch_unlocked_count` returns correct values for each branch state
|
||||||
|
- `KeyboardModel::qwerty()` covers all skill tree chars
|
||||||
|
- Selection bounds don't panic with Lowercase in `selectable_branches`
|
||||||
|
- Manual testing checklist:
|
||||||
|
- Menu footer shows `[c] Settings`
|
||||||
|
- Skill tree: Lowercase is selectable with arrow keys, Enter starts drill
|
||||||
|
- Skill tree: single fraction display, no duplicate numbers
|
||||||
|
- Skill tree: progress bars show dual unlocked/mastered segments
|
||||||
|
- Skill tree detail: per-key mastery bars shown
|
||||||
|
- Drill: branch progress bars visible, active branch highlighted
|
||||||
|
- Drill keyboard: full layout visible, keys shift on Shift press
|
||||||
|
- Stats keyboard: both layers shown
|
||||||
|
- Code drill: language selection appears, snippets have proper newlines/indentation
|
||||||
|
- Non-adaptive drills: ESC still shows partial result correctly
|
||||||
|
- Dvorak/Colemak: keyboard renders correctly when layout config changed
|
||||||
757
docs/plans/2026-02-17-code-drill-feature-parity-plan.md
Normal file
757
docs/plans/2026-02-17-code-drill-feature-parity-plan.md
Normal file
@@ -0,0 +1,757 @@
|
|||||||
|
# Code Drill Feature Parity Plan
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The code drill feature is significantly less developed than the passage drill. The passage drill has a full onboarding flow, lazy downloads with progress bars, configurable network/cache settings, and rich content from Project Gutenberg. The code drill only has 4 hardcoded languages with ~20-30 built-in snippets each, a basic language selection screen, and a partially-implemented synchronous GitHub fetch that blocks the UI thread. There's also a completely dead `github_code.rs` file that's never used.
|
||||||
|
|
||||||
|
This plan is split into three delivery phases:
|
||||||
|
1. **Phase 1**: Feature parity with passage drill (onboarding, downloads, progress bar, config)
|
||||||
|
2. **Phase 2**: Language expansion and extraction improvements
|
||||||
|
3. **Phase 3**: Custom repo support
|
||||||
|
|
||||||
|
## Current Code Drill Analysis
|
||||||
|
|
||||||
|
### What exists:
|
||||||
|
- **`generator/code_syntax.rs`**: `CodeSyntaxGenerator` with built-in snippets for 4 languages (rust, python, javascript, go), a `try_fetch_code()` that synchronously fetches from hardcoded GitHub URLs (blocking UI), `extract_code_snippets()` for parsing functions from source
|
||||||
|
- **`generator/code_patterns.rs`**: Post-processor that inserts code-like expressions into adaptive drill text (unrelated to code drill mode)
|
||||||
|
- **`generator/github_code.rs`**: **Dead code** - `GitHubCodeGenerator` struct with `#[allow(dead_code)]`, never referenced outside its own file
|
||||||
|
- **Config**: Only `code_language: String` - no download/network/onboarding settings
|
||||||
|
- **Screens**: `CodeLanguageSelect` only - no intro, no download progress
|
||||||
|
- **Languages**: rust, python, javascript, go, "all"
|
||||||
|
|
||||||
|
### What passage drill has that code drill doesn't:
|
||||||
|
- Onboarding intro screen (`PassageIntro`) with config for downloads/dir/limits
|
||||||
|
- `passage_onboarding_done` flag (shows intro only on first use)
|
||||||
|
- `passage_downloads_enabled` toggle
|
||||||
|
- `passage_download_dir` configurable path
|
||||||
|
- `passage_paragraphs_per_book` content limit
|
||||||
|
- Lazy download: on drill start, downloads one book if not cached
|
||||||
|
- Background download thread with atomic progress reporting
|
||||||
|
- Download progress screen (`PassageDownloadProgress`) with byte-level progress bar
|
||||||
|
- Fallback to built-in content when downloads off
|
||||||
|
|
||||||
|
### Built-in snippet whitespace review:
|
||||||
|
- **Rust**: 4-space indent - idiomatic
|
||||||
|
- **Python**: 4-space indent - idiomatic
|
||||||
|
- **JavaScript**: 4-space indent - idiomatic
|
||||||
|
- **Go**: `\t` tab indent - idiomatic
|
||||||
|
|
||||||
|
All whitespace is correct. The escaped string format (`\n`, `\t`, `\"`) is hard to read. Converting to raw strings (`r#"..."#`) improves maintainability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Feature Parity with Passage Drill
|
||||||
|
|
||||||
|
Goal: Give code drill the same onboarding, download, caching, and config infrastructure as passage drill. Keep the existing 4 languages. No language expansion yet.
|
||||||
|
|
||||||
|
### Step 1.1: Delete dead code
|
||||||
|
|
||||||
|
- Delete `src/generator/github_code.rs` entirely
|
||||||
|
- Remove `pub mod github_code;` from `src/generator/mod.rs`
|
||||||
|
|
||||||
|
### Step 1.2: Convert built-in snippets to raw strings
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
Convert all 4 language snippet arrays from escaped strings to `r#"..."#` raw strings. Example:
|
||||||
|
|
||||||
|
Before: `"fn main() {\n println!(\"hello\");\n}"`
|
||||||
|
After:
|
||||||
|
```rust
|
||||||
|
r#"fn main() {
|
||||||
|
println!("hello");
|
||||||
|
}"#
|
||||||
|
```
|
||||||
|
|
||||||
|
Go snippets: `\t` becomes actual tab characters inside raw strings (correct for Go).
|
||||||
|
|
||||||
|
Keep all existing snippets at their current count (~20-30 per language). Do NOT reduce them -- since downloads default to off, these are the primary content source for new users.
|
||||||
|
|
||||||
|
Validation: run `cargo test` after conversion. Add a focused test that asserts a sample snippet's char content matches expectations (catches any accidental whitespace changes).
|
||||||
|
|
||||||
|
### Step 1.3: Add config fields for code drill
|
||||||
|
|
||||||
|
**File**: `src/config.rs`
|
||||||
|
|
||||||
|
Add fields mirroring passage drill config:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[serde(default = "default_code_downloads_enabled")]
|
||||||
|
pub code_downloads_enabled: bool, // default: false
|
||||||
|
#[serde(default = "default_code_download_dir")]
|
||||||
|
pub code_download_dir: String, // default: dirs::data_dir()/keydr/code/
|
||||||
|
#[serde(default = "default_code_snippets_per_repo")]
|
||||||
|
pub code_snippets_per_repo: usize, // default: 50
|
||||||
|
#[serde(default = "default_code_onboarding_done")]
|
||||||
|
pub code_onboarding_done: bool, // default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
`code_download_dir` default uses `dirs::data_dir()` (same pattern as `default_passage_download_dir`) for cross-platform portability.
|
||||||
|
|
||||||
|
`code_snippets_per_repo` is a **download-time extraction cap**: when fetching from a repo, extract at most this many snippets and write them to cache. The generator reads whatever is in the cache without re-filtering.
|
||||||
|
|
||||||
|
Update `Default` impl. Add `default_*` functions.
|
||||||
|
|
||||||
|
**Config normalization**: After deserialization in `App::new()` (not `Config::load()`, to avoid coupling config to generator internals), validate `code_language` against `code_language_options()`. If invalid (e.g., old/renamed key), reset to `"rust"`.
|
||||||
|
|
||||||
|
**Old cache migration**: The old `DiskCache("code_cache")` entries (in `~/.local/share/keydr/code_cache/`) are simply ignored. They used a different key format (`{lang}_snippets`) and location. No migration or cleanup needed -- they'll be naturally superseded by the new cache in `code_download_dir`.
|
||||||
|
|
||||||
|
### Step 1.4: Define language data structures
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
Add structures for the language registry. Phase 1 only populates the 4 existing languages + "all":
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct CodeLanguage {
|
||||||
|
pub key: &'static str, // filesystem-safe identifier (e.g. "rust", "bash")
|
||||||
|
pub display_name: &'static str, // UI label (e.g. "Rust", "Shell/Bash")
|
||||||
|
pub extensions: &'static [&'static str], // e.g. &[".rs"], &[".py", ".pyi"]
|
||||||
|
pub repos: &'static [CodeRepo],
|
||||||
|
pub has_builtin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CodeRepo {
|
||||||
|
pub key: &'static str, // filesystem-safe identifier for cache naming
|
||||||
|
pub urls: &'static [&'static str], // raw.githubusercontent.com file URLs to fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CODE_LANGUAGES: &[CodeLanguage] = &[
|
||||||
|
CodeLanguage {
|
||||||
|
key: "rust",
|
||||||
|
display_name: "Rust",
|
||||||
|
extensions: &[".rs"],
|
||||||
|
repos: &[
|
||||||
|
CodeRepo {
|
||||||
|
key: "tokio",
|
||||||
|
urls: &[
|
||||||
|
"https://raw.githubusercontent.com/tokio-rs/tokio/master/tokio/src/sync/mutex.rs",
|
||||||
|
"https://raw.githubusercontent.com/tokio-rs/tokio/master/tokio/src/net/tcp/stream.rs",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
CodeRepo {
|
||||||
|
key: "serde",
|
||||||
|
urls: &[
|
||||||
|
"https://raw.githubusercontent.com/serde-rs/serde/master/serde/src/ser/mod.rs",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
has_builtin: true,
|
||||||
|
},
|
||||||
|
// ... python, javascript, go with similar structure
|
||||||
|
// Move existing hardcoded URLs from try_fetch_code() into these repo definitions
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Helper functions:
|
||||||
|
```rust
|
||||||
|
pub fn code_language_options() -> Vec<(&'static str, String)>
|
||||||
|
// Returns [("rust", "Rust"), ("python", "Python"), ..., ("all", "All (random)")]
|
||||||
|
|
||||||
|
pub fn language_by_key(key: &str) -> Option<&'static CodeLanguage>
|
||||||
|
|
||||||
|
pub fn is_language_cached(cache_dir: &str, key: &str) -> bool
|
||||||
|
// Checks if any {key}_*.txt files exist in cache_dir AND have non-empty content (>0 bytes)
|
||||||
|
// Uses direct filesystem scanning (NOT DiskCache -- DiskCache has no list/glob API)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1.5: Generalize download job struct
|
||||||
|
|
||||||
|
**File**: `src/app.rs`
|
||||||
|
|
||||||
|
Rename `PassageDownloadJob` to `DownloadJob`. It's already generic (just `Arc<AtomicU64>`, `Arc<AtomicBool>`, and a thread handle). Update all passage references to use the renamed type. No behavior change.
|
||||||
|
|
||||||
|
### Step 1.6: Add code drill app state
|
||||||
|
|
||||||
|
**File**: `src/app.rs`
|
||||||
|
|
||||||
|
Add `CodeDownloadCompleteAction` enum (parallels `PassageDownloadCompleteAction`):
|
||||||
|
```rust
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum CodeDownloadCompleteAction {
|
||||||
|
StartCodeDrill,
|
||||||
|
ReturnToSettings,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add screen variants:
|
||||||
|
```rust
|
||||||
|
CodeIntro, // Onboarding screen for code drill
|
||||||
|
CodeDownloadProgress, // Download progress for code files
|
||||||
|
```
|
||||||
|
|
||||||
|
Add app fields:
|
||||||
|
```rust
|
||||||
|
pub code_intro_selected: usize,
|
||||||
|
pub code_intro_downloads_enabled: bool,
|
||||||
|
pub code_intro_download_dir: String,
|
||||||
|
pub code_intro_snippets_per_repo: usize,
|
||||||
|
pub code_intro_downloading: bool,
|
||||||
|
pub code_intro_download_total: usize,
|
||||||
|
pub code_intro_downloaded: usize,
|
||||||
|
pub code_intro_current_repo: String,
|
||||||
|
pub code_intro_download_bytes: u64,
|
||||||
|
pub code_intro_download_bytes_total: u64,
|
||||||
|
pub code_download_queue: Vec<usize>, // repo indices within current language's repos array
|
||||||
|
pub code_drill_language_override: Option<String>,
|
||||||
|
pub code_download_action: CodeDownloadCompleteAction,
|
||||||
|
code_download_job: Option<DownloadJob>,
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1.7: Remove blocking fetch from generator
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
Remove `try_fetch_code()` from `CodeSyntaxGenerator`. All network I/O moves to the app layer with background threads.
|
||||||
|
|
||||||
|
Update constructor:
|
||||||
|
```rust
|
||||||
|
pub fn new(rng: SmallRng, language: &str, cache_dir: &str) -> Self
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `load_cached_snippets()`: scan `cache_dir` for files matching `{language}_*.txt`, read each, split on `---SNIPPET---` delimiter. This replaces the `DiskCache("code_cache")` approach with direct filesystem reads (since `DiskCache` has no listing/glob API and the cache dir is now user-configurable).
|
||||||
|
|
||||||
|
### Step 1.8: Add download function
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn download_code_repo_to_cache_with_progress<F>(
|
||||||
|
cache_dir: &str,
|
||||||
|
language_key: &str,
|
||||||
|
repo: &CodeRepo,
|
||||||
|
snippets_limit: usize,
|
||||||
|
on_progress: F,
|
||||||
|
) -> bool
|
||||||
|
where
|
||||||
|
F: FnMut(u64, Option<u64>),
|
||||||
|
```
|
||||||
|
|
||||||
|
This function:
|
||||||
|
1. Creates `cache_dir` if needed (`fs::create_dir_all`)
|
||||||
|
2. Fetches each URL in `repo.urls` using `fetch_url_bytes_with_progress` (already exists in `cache.rs`)
|
||||||
|
3. Runs `extract_code_snippets()` on each fetched file
|
||||||
|
4. Combines all snippets, truncates to `snippets_limit`
|
||||||
|
5. Writes to `{cache_dir}/{language_key}_{repo.key}.txt` with `---SNIPPET---` delimiter
|
||||||
|
6. Returns `true` on success
|
||||||
|
|
||||||
|
**Error handling**: If any individual URL fails (404, timeout, network error), skip it and continue with others. If zero snippets extracted from all URLs, return `false`. The app layer treats `false` as "skip this repo, continue queue" (same as passage drill's failure behavior).
|
||||||
|
|
||||||
|
### Step 1.9: Implement code drill flow methods
|
||||||
|
|
||||||
|
**File**: `src/app.rs`
|
||||||
|
|
||||||
|
**`go_to_code_intro()`**: Initialize intro screen state (downloads toggle, dir, snippets limit from config). Set `code_download_action = CodeDownloadCompleteAction::StartCodeDrill`. Set screen to `CodeIntro`.
|
||||||
|
|
||||||
|
**`start_code_drill()`**: Lazy download logic with explicit language resolution:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn start_code_drill(&mut self) {
|
||||||
|
// Step 1: Resolve concrete language (never download with "all" selected)
|
||||||
|
if self.code_drill_language_override.is_none() {
|
||||||
|
let chosen = if self.config.code_language == "all" {
|
||||||
|
// Pick from languages with built-in OR cached content only
|
||||||
|
// Never pick a network-only language that isn't cached
|
||||||
|
let available = languages_with_content(&self.config.code_download_dir);
|
||||||
|
if available.is_empty() {
|
||||||
|
"rust".to_string() // ultimate fallback
|
||||||
|
} else {
|
||||||
|
let idx = self.rng.gen_range(0..available.len());
|
||||||
|
available[idx].to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.config.code_language.clone()
|
||||||
|
};
|
||||||
|
self.code_drill_language_override = Some(chosen);
|
||||||
|
}
|
||||||
|
|
||||||
|
let chosen = self.code_drill_language_override.clone().unwrap();
|
||||||
|
|
||||||
|
// Step 2: Check if we need to download
|
||||||
|
if self.config.code_downloads_enabled
|
||||||
|
&& !is_language_cached(&self.config.code_download_dir, &chosen)
|
||||||
|
{
|
||||||
|
if let Some(lang) = language_by_key(&chosen) {
|
||||||
|
if !lang.repos.is_empty() {
|
||||||
|
// Pick one random repo to download
|
||||||
|
let repo_idx = self.rng.gen_range(0..lang.repos.len());
|
||||||
|
self.code_download_queue = vec![repo_idx];
|
||||||
|
self.code_intro_download_total = 1;
|
||||||
|
self.code_intro_downloaded = 0;
|
||||||
|
self.code_intro_downloading = true;
|
||||||
|
self.code_intro_current_repo = format!("{}", lang.repos[repo_idx].key);
|
||||||
|
self.code_download_action = CodeDownloadCompleteAction::StartCodeDrill;
|
||||||
|
self.code_download_job = None;
|
||||||
|
self.screen = AppScreen::CodeDownloadProgress;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Language has no repos or unknown: fall through to built-in
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: If language has no built-in AND no cache AND downloads off → fallback
|
||||||
|
if !is_language_cached(&self.config.code_download_dir, &chosen) {
|
||||||
|
if let Some(lang) = language_by_key(&chosen) {
|
||||||
|
if !lang.has_builtin {
|
||||||
|
// Network-only language with no cache: fall back to "rust"
|
||||||
|
self.code_drill_language_override = Some("rust".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Start the drill
|
||||||
|
self.drill_mode = DrillMode::Code;
|
||||||
|
self.drill_scope = DrillScope::Global;
|
||||||
|
self.start_drill();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key behavior: `"all"` only selects from `languages_with_content()` (built-in OR cached). This prevents the dead-end loop of repeatedly picking uncached network-only languages and forcing download screens. In Phase 2, once network-only languages get cached via manual download, they are automatically included in `"all"` selection.
|
||||||
|
|
||||||
|
**`languages_with_content(cache_dir: &str) -> Vec<&'static str>`**: Returns language keys that have either `has_builtin: true` or non-empty cache files in `cache_dir`.
|
||||||
|
|
||||||
|
**`process_code_download_tick()`**, **`spawn_code_download_job()`**: Same pattern as passage equivalents, using `download_code_repo_to_cache_with_progress` and `DownloadJob`.
|
||||||
|
|
||||||
|
**`start_code_downloads_from_settings()`**: Mirror `start_passage_downloads_from_settings()` with `CodeDownloadCompleteAction::ReturnToSettings`.
|
||||||
|
|
||||||
|
### Step 1.10: Update code language select flow
|
||||||
|
|
||||||
|
**File**: `src/main.rs`
|
||||||
|
|
||||||
|
Update `handle_code_language_key()` and `render_code_language_select()`:
|
||||||
|
- Still shows the same 4+1 languages for now (Phase 2 expands this)
|
||||||
|
- Wire Enter to `confirm_code_language_and_continue()`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn confirm_code_language_and_continue(app: &mut App, langs: &[&str]) {
|
||||||
|
if app.code_language_selected >= langs.len() { return; }
|
||||||
|
app.config.code_language = langs[app.code_language_selected].to_string();
|
||||||
|
let _ = app.config.save();
|
||||||
|
if app.config.code_onboarding_done {
|
||||||
|
app.start_code_drill();
|
||||||
|
} else {
|
||||||
|
app.go_to_code_intro();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1.11: Add event handlers and renderers
|
||||||
|
|
||||||
|
**File**: `src/main.rs`
|
||||||
|
|
||||||
|
Add to screen dispatch in `handle_key()` and `render()`:
|
||||||
|
|
||||||
|
**`handle_code_intro_key()`**: Same field navigation as `handle_passage_intro_key()` but operates on `code_intro_*` fields. 4 fields:
|
||||||
|
1. Enable network downloads (toggle)
|
||||||
|
2. Download directory (editable text)
|
||||||
|
3. Snippets per repo (numeric, adjustable)
|
||||||
|
4. Start code drill (confirm button)
|
||||||
|
|
||||||
|
On confirm: save config fields, set `code_onboarding_done = true`, call `start_code_drill()`.
|
||||||
|
|
||||||
|
**`handle_code_download_progress_key()`**: Esc/q to cancel. On cancel:
|
||||||
|
1. Clear `code_download_queue`
|
||||||
|
2. Set `code_intro_downloading = false`
|
||||||
|
3. If a `code_download_job` is in-flight, detach it (set to `None` without joining -- the thread will finish and write to cache, which is harmless; the `Arc` atomics keep the thread safe)
|
||||||
|
4. Reset `code_drill_language_override` to `None`
|
||||||
|
5. Go to menu
|
||||||
|
|
||||||
|
This matches the existing passage download cancel behavior (passage also does not join/abort in-flight threads on Esc).
|
||||||
|
|
||||||
|
**`render_code_intro()`**: Mirror `render_passage_intro()` layout. Title: "Code Downloads Setup". Explanatory text: "Configure code source settings before your first code drill." / "Downloads are lazy: code is fetched only when first needed."
|
||||||
|
|
||||||
|
**`render_code_download_progress()`**: Mirror `render_passage_download_progress()`. Title: "Downloading Code Source". Show repo name, byte progress bar.
|
||||||
|
|
||||||
|
Update tick handler:
|
||||||
|
```rust
|
||||||
|
if (app.screen == AppScreen::CodeIntro
|
||||||
|
|| app.screen == AppScreen::CodeDownloadProgress)
|
||||||
|
&& app.code_intro_downloading
|
||||||
|
{
|
||||||
|
app.process_code_download_tick();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1.12: Update generate_text for Code mode
|
||||||
|
|
||||||
|
**File**: `src/app.rs`
|
||||||
|
|
||||||
|
Update `DrillMode::Code` in `generate_text()`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
DrillMode::Code => {
|
||||||
|
let filter = CharFilter::new(('a'..='z').collect());
|
||||||
|
let lang = self.code_drill_language_override
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| self.config.code_language.clone());
|
||||||
|
let rng = SmallRng::from_rng(&mut self.rng).unwrap();
|
||||||
|
let mut generator = CodeSyntaxGenerator::new(
|
||||||
|
rng, &lang, &self.config.code_download_dir,
|
||||||
|
);
|
||||||
|
self.code_drill_language_override = None;
|
||||||
|
let text = generator.generate(&filter, None, word_count);
|
||||||
|
(text, Some(generator.last_source().to_string()))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1.13: Settings integration
|
||||||
|
|
||||||
|
**Files**: `src/main.rs`, `src/app.rs`
|
||||||
|
|
||||||
|
Add settings rows after existing code language field (index 3):
|
||||||
|
- Index 4: Code Downloads: On/Off
|
||||||
|
- Index 5: Code Download Dir: editable path
|
||||||
|
- Index 6: Code Snippets per Repo: numeric
|
||||||
|
- Index 7: Download Code Now: action button
|
||||||
|
|
||||||
|
Shift existing passage settings indices up by 4. Update `settings_cycle_forward`/`settings_cycle_backward` and max `settings_selected` bound.
|
||||||
|
|
||||||
|
**"Download Code Now" behavior**: Downloads all uncached curated repos for the currently selected `code_language` only. If `code_language == "all"`, downloads all uncached repos for all curated languages. Does NOT include custom repos. Mirrors passage behavior where "Download Passages Now" downloads all uncached books.
|
||||||
|
|
||||||
|
**`start_code_downloads()`**: Queues all uncached repos for the currently selected language. Used by intro screen "confirm" flow when downloads are enabled.
|
||||||
|
|
||||||
|
### Phase 1 Verification
|
||||||
|
|
||||||
|
1. `cargo build` -- compiles
|
||||||
|
2. `cargo test` -- all existing tests pass, plus new tests:
|
||||||
|
- `test_languages_with_content_includes_builtin` -- verifies built-in languages appear in `languages_with_content()` even with empty cache dir
|
||||||
|
- `test_languages_with_content_excludes_uncached_network_only` -- verifies network-only languages without cache are not returned
|
||||||
|
- `test_config_serde_defaults` -- verifies new config fields deserialize with correct defaults from empty/old configs
|
||||||
|
- `test_raw_string_snippets_preserved` -- spot-check that raw string conversion didn't alter snippet content
|
||||||
|
3. `cargo build --no-default-features` -- compiles, network features gated
|
||||||
|
4. Manual tests:
|
||||||
|
- Menu → Code Drill → language select → first time shows CodeIntro
|
||||||
|
- CodeIntro with downloads off → confirms → starts drill with built-in snippets
|
||||||
|
- CodeIntro with downloads on → confirms → shows CodeDownloadProgress → downloads repo → starts drill with downloaded content
|
||||||
|
- Subsequent code drills skip onboarding
|
||||||
|
- "all" language mode only picks from languages with content (never triggers download)
|
||||||
|
- Settings shows code drill fields, values persist on restart
|
||||||
|
- Passage drill flow completely unchanged
|
||||||
|
- Esc during download progress → returns to menu, no crash
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Language Expansion and Extraction Improvements
|
||||||
|
|
||||||
|
Goal: Add 8 more built-in languages and ~18 network-only languages, improve snippet extraction.
|
||||||
|
|
||||||
|
### Step 2.1: Add 8 built-in language snippet sets
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
Add ~10-15 raw-string snippets each for: **typescript, java, c, cpp, ruby, swift, bash, lua**
|
||||||
|
|
||||||
|
Language keys: `typescript`/`ts`, `java`, `c`, `cpp`, `ruby`, `swift`, `bash` (display: "Shell/Bash"), `lua`
|
||||||
|
|
||||||
|
All with idiomatic whitespace:
|
||||||
|
- TypeScript: 4-space indent
|
||||||
|
- Java: 4-space indent
|
||||||
|
- C: 4-space indent
|
||||||
|
- C++: 4-space indent
|
||||||
|
- Ruby: 2-space indent
|
||||||
|
- Swift: 4-space indent
|
||||||
|
- Bash: 2-space indent (common convention)
|
||||||
|
- Lua: 2-space indent
|
||||||
|
|
||||||
|
Update `get_snippets()` match to include all 12 languages.
|
||||||
|
|
||||||
|
### Step 2.2: Expand language registry to ~30 languages
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
Add ~18 network-only entries to `CODE_LANGUAGES` with curated repos:
|
||||||
|
|
||||||
|
kotlin, scala, haskell, elixir, clojure, perl, php, r, dart, zig, nim, ocaml, erlang, julia, objective-c, groovy, csharp, fsharp
|
||||||
|
|
||||||
|
Each gets 2-3 repos with specific raw.githubusercontent.com file URLs. **Exclude SQL and CSS** -- their syntax is too different from procedural code for function-level extraction to work well.
|
||||||
|
|
||||||
|
This is a significant data curation subtask: for each language, identify 2-3 well-known repos with permissive licenses (MIT/Apache/BSD), select 2-5 representative source files per repo with functions/methods to extract.
|
||||||
|
|
||||||
|
**Acceptance threshold**: Each language must yield at least 10 extractable snippets from its curated repos (verified by running `extract_code_snippets` against fetched files). Languages that fall below this threshold should be dropped from the registry rather than shipped with poor content.
|
||||||
|
|
||||||
|
### Step 2.3: Improve snippet extraction
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
Add a `func_start_patterns` field to `CodeLanguage`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct CodeLanguage {
|
||||||
|
// ... existing fields ...
|
||||||
|
pub block_style: BlockStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BlockStyle {
|
||||||
|
Braces(&'static [&'static str]), // fn/def/func patterns, brace-delimited (C, Java, Go, etc.)
|
||||||
|
Indentation(&'static [&'static str]), // def/class patterns, indentation-delimited (Python)
|
||||||
|
EndDelimited(&'static [&'static str]), // def/class patterns, closed by `end` keyword (Ruby, Lua, Elixir)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `extract_code_snippets()` to accept `BlockStyle`:
|
||||||
|
- `Braces`: current behavior with configurable start patterns (C, Java, Go, JS, etc.)
|
||||||
|
- `Indentation`: track indent level changes to find block boundaries (Python only)
|
||||||
|
- `EndDelimited`: scan for matching `end` keyword at same indent level to close blocks (Ruby, Lua, Elixir)
|
||||||
|
|
||||||
|
Language-specific patterns:
|
||||||
|
- Java: `["public ", "private ", "protected ", "static ", "class ", "interface "]`
|
||||||
|
- Ruby: `["def ", "class ", "module "]` (EndDelimited style -- uses `end` keyword to close blocks)
|
||||||
|
- C/C++: `["int ", "void ", "char ", "float ", "double ", "struct ", "class ", "template"]`
|
||||||
|
- Swift: `["func ", "class ", "struct ", "enum ", "protocol "]`
|
||||||
|
- Bash: `["function ", "() {"]` (Braces style, simple)
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
### Step 2.4: Make language select scrollable
|
||||||
|
|
||||||
|
**File**: `src/main.rs`
|
||||||
|
|
||||||
|
With 30+ languages, the selection screen needs scrolling. Add `code_language_scroll: usize` to `App`. Show a viewport of ~15 items. Add keybindings:
|
||||||
|
- Up/Down: navigate
|
||||||
|
- PageUp/PageDown: jump 10 items
|
||||||
|
- Home/End or `g`/`G`: jump to top/bottom
|
||||||
|
- `/`: type-to-filter (optional, nice-to-have)
|
||||||
|
|
||||||
|
Mark each language as "(built-in)" or "(download required)" in the list.
|
||||||
|
|
||||||
|
### Phase 2 Verification
|
||||||
|
|
||||||
|
1. `cargo build && cargo test`
|
||||||
|
2. Manual: verify all 12 built-in languages produce readable snippets with correct indentation
|
||||||
|
3. Manual: select a network-only language → triggers download → produces good snippets
|
||||||
|
4. Manual: scrollable language list works, indicators are accurate
|
||||||
|
5. Verify each built-in language's snippet whitespace is idiomatic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Custom Repo Support
|
||||||
|
|
||||||
|
Goal: Let users specify their own GitHub repos to train on.
|
||||||
|
|
||||||
|
### Step 3.1: Design custom repo fetch strategy
|
||||||
|
|
||||||
|
Custom repos require solving problems that curated repos don't have:
|
||||||
|
- **Branch discovery**: Use GitHub API `GET /repos/{owner}/{repo}` to find `default_branch`. Requires `User-Agent` header (GitHub rejects requests without it; use `"keydr/{version}"`). Optionally support a `GITHUB_TOKEN` env var for authenticated requests (raises rate limit from 60 to 5000 req/hour).
|
||||||
|
- **File discovery**: Use GitHub API `GET /repos/{owner}/{repo}/git/trees/{branch}?recursive=1` to list all files, filter by language extensions. Same `User-Agent` and optional auth headers. If the response has `"truncated": true` (repos with >100k files), reject with a user-facing error: "Repository is too large for automatic file discovery. Please use a smaller repo or fork with fewer files."
|
||||||
|
- **Rate limiting**: Cache the tree response to disk. On 403/429 responses, show error: "GitHub API rate limit reached. Try again later or set GITHUB_TOKEN env var for higher limits."
|
||||||
|
- **File selection**: From matching files, randomly select 3-5 files to download via raw.githubusercontent.com (no API needed for file content)
|
||||||
|
- **Language detection**: Match file extensions against `CodeLanguage.extensions` field. If ambiguous or no match, prompt user.
|
||||||
|
- **All API requests**: Set `Accept: application/vnd.github.v3+json` header, timeout 10s.
|
||||||
|
|
||||||
|
### Step 3.2: Add config field and validation
|
||||||
|
|
||||||
|
**File**: `src/config.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[serde(default)]
|
||||||
|
pub code_custom_repos: Vec<String>, // Format: "owner/repo" or "owner/repo@language"
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse function:
|
||||||
|
```rust
|
||||||
|
pub fn parse_custom_repo(input: &str) -> Option<CustomRepo> {
|
||||||
|
// Accepts: "owner/repo", "owner/repo@language", "https://github.com/owner/repo"
|
||||||
|
// Validates: owner and repo contain only valid GitHub chars
|
||||||
|
// Returns None on invalid input
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3.3: Settings UI for custom repos
|
||||||
|
|
||||||
|
Add a settings section showing current custom repos as a scrollable list. Keybindings:
|
||||||
|
- `a`: add new repo (enters text input mode)
|
||||||
|
- `d`/`x`: delete selected repo
|
||||||
|
- Up/Down: navigate list
|
||||||
|
|
||||||
|
### Step 3.4: Code language select "Add custom repo" option
|
||||||
|
|
||||||
|
At the bottom of the language select list, add an "[ + Add custom repo ]" option. Selecting it enters a text input mode for `owner/repo`. On confirm:
|
||||||
|
1. Validate format
|
||||||
|
2. Add to `code_custom_repos` config
|
||||||
|
3. Auto-detect language from repo (via API tree listing file extensions)
|
||||||
|
4. If language ambiguous, show a small picker
|
||||||
|
5. Queue download of that repo
|
||||||
|
|
||||||
|
### Step 3.5: Integrate custom repos into download flow
|
||||||
|
|
||||||
|
When `start_code_drill()` runs for a language, include matching custom repos in the download candidates alongside curated repos.
|
||||||
|
|
||||||
|
### Phase 3 Verification
|
||||||
|
|
||||||
|
1. Add a custom repo → appears in settings list
|
||||||
|
2. Start drill → custom repo snippets appear
|
||||||
|
3. Invalid repo format → shows error, doesn't save
|
||||||
|
4. GitHub rate limit → shows informative error
|
||||||
|
5. Remove custom repo → removed from config and future drills
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Files Summary
|
||||||
|
|
||||||
|
| File | Phase | Changes |
|
||||||
|
|------|-------|---------|
|
||||||
|
| `src/generator/github_code.rs` | 1 | Delete |
|
||||||
|
| `src/generator/mod.rs` | 1 | Remove github_code module |
|
||||||
|
| `src/generator/code_syntax.rs` | 1, 2 | Raw strings, new constructor, remove blocking fetch, language registry, download fn, new snippet sets, improved extraction |
|
||||||
|
| `src/config.rs` | 1, 3 | New code drill config fields, validation |
|
||||||
|
| `src/app.rs` | 1 | DownloadJob rename, new screens/state/flow methods, CodeDownloadCompleteAction |
|
||||||
|
| `src/main.rs` | 1, 2 | New handlers/renderers, updated settings, scrollable language list |
|
||||||
|
| `src/generator/cache.rs` | 1 | No changes (reuse existing `fetch_url_bytes_with_progress`) |
|
||||||
|
|
||||||
|
## Existing Code to Reuse
|
||||||
|
|
||||||
|
- `generator::cache::fetch_url_bytes_with_progress` -- already handles progress callbacks, used for passage downloads
|
||||||
|
- `generator::cache::DiskCache` -- NOT reused for code cache (no listing API); use direct `fs::read_dir` + `fs::read_to_string` instead
|
||||||
|
- `PassageDownloadJob` pattern (atomics + thread) -- generalized into `DownloadJob`
|
||||||
|
- `passage::extract_paragraphs` pattern -- referenced for extraction design but not directly reused
|
||||||
|
- `passage::download_book_to_cache_with_progress` -- structural template for `download_code_repo_to_cache_with_progress`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2.5: Improve Snippet Extraction Quality
|
||||||
|
|
||||||
|
### Context
|
||||||
|
|
||||||
|
After Phase 2, the verification test (`test_verify_repo_urls`) shows many languages producing far fewer than 100 snippets. Root causes:
|
||||||
|
1. **Per-file cap of 50** in `extract_code_snippets()` (line 1869) limits output even from large source files
|
||||||
|
2. **Keyword-only matching** — extraction only starts when a line begins with a recognized keyword (e.g. `fn `, `def `, `class `). Many valid code blocks (anonymous functions, method chains, match arms, closures, etc.) are missed.
|
||||||
|
3. **Narrow keyword lists** — some languages are missing patterns for common constructs (e.g. `macro_rules!` in Rust, `@interface` in Objective-C)
|
||||||
|
4. **`code_snippets_per_repo` default of 50** caps total output per download
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Get every language to produce 100+ snippets from its curated repos, without sacrificing snippet quality. Do this by:
|
||||||
|
1. Widening keyword patterns to capture more language constructs
|
||||||
|
2. Adding a structural fallback that extracts well-formed code blocks by structure when keywords alone don't find enough
|
||||||
|
3. Raising the per-file and per-repo snippet caps
|
||||||
|
|
||||||
|
### Step 2.5.1: Raise snippet caps
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
Change `snippets.truncate(50)` → `snippets.truncate(200)` in `extract_code_snippets()`.
|
||||||
|
|
||||||
|
**File**: `src/config.rs`
|
||||||
|
|
||||||
|
Change `default_code_snippets_per_repo()` → `200`.
|
||||||
|
|
||||||
|
### Step 2.5.2: Widen keyword patterns
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
Add missing start patterns to existing languages. These are patterns that should have been there from the start — they represent common, well-defined constructs that produce good typing drill snippets:
|
||||||
|
|
||||||
|
| Language | Add patterns |
|
||||||
|
|----------|-------------|
|
||||||
|
| Rust | `"macro_rules! "`, `"mod "`, `"const "`, `"static "`, `"type "` |
|
||||||
|
| Python | `"async def "` is already there. Add `"@"` (decorators start blocks) |
|
||||||
|
| JavaScript | `"class "`, `"const "`, `"let "`, `"export "` |
|
||||||
|
| Go | No changes needed (already has `"func "`, `"type "`) |
|
||||||
|
| TypeScript | `"class "`, `"const "`, `"let "`, `"export "`, `"interface "` |
|
||||||
|
| Java | `"abstract "`, `"final "`, `"@"` (annotations start blocks) |
|
||||||
|
| C | `"typedef "`, `"#define "`, `"enum "` |
|
||||||
|
| C++ | `"namespace "`, `"typedef "`, `"#define "`, `"enum "`, `"constexpr "`, `"auto "` |
|
||||||
|
| Ruby | Add `"attr_"`, `"scope "`, `"describe "`, `"it "` |
|
||||||
|
| Swift | `"var "`, `"let "`, `"init("`, `"deinit "`, `"extension "`, `"typealias "` |
|
||||||
|
| Bash | `"if "`, `"for "`, `"while "`, `"case "` |
|
||||||
|
| Kotlin | `"override fun "` already there. Add `"val "`, `"var "`, `"enum "`, `"annotation "`, `"typealias "` |
|
||||||
|
| Scala | `"val "`, `"var "`, `"type "`, `"implicit "`, `"given "`, `"extension "` |
|
||||||
|
| PHP | `"class "`, `"interface "`, `"trait "`, `"enum "` |
|
||||||
|
| Dart | Add `"Widget "`, `"get "`, `"set "`, `"enum "`, `"typedef "`, `"extension "` |
|
||||||
|
| Elixir | `"defmacro "`, `"defstruct"`, `"defprotocol "`, `"defimpl "` |
|
||||||
|
| Zig | `"test "`, `"var "` |
|
||||||
|
| Haskell | Already broad. No changes. |
|
||||||
|
| Objective-C | `"@interface "`, `"@implementation "`, `"@protocol "`, `"typedef "` |
|
||||||
|
| Others | Review on a case-by-case basis during implementation |
|
||||||
|
|
||||||
|
### Step 2.5.3: Add structural fallback extraction
|
||||||
|
|
||||||
|
**File**: `src/generator/code_syntax.rs`
|
||||||
|
|
||||||
|
When keyword-based extraction yields fewer than 20 snippets from a file, run a second pass that extracts code blocks purely by structure. This captures anonymous functions, nested blocks, and other constructs that don't start with recognized keywords.
|
||||||
|
|
||||||
|
#### Design
|
||||||
|
|
||||||
|
Add a `structural_fallback: bool` field to each `BlockStyle` variant:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum BlockStyle {
|
||||||
|
Braces {
|
||||||
|
patterns: &'static [&'static str],
|
||||||
|
structural_fallback: bool,
|
||||||
|
},
|
||||||
|
Indentation {
|
||||||
|
patterns: &'static [&'static str],
|
||||||
|
structural_fallback: bool,
|
||||||
|
},
|
||||||
|
EndDelimited {
|
||||||
|
patterns: &'static [&'static str],
|
||||||
|
structural_fallback: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `structural_fallback: true` for all languages. This can be disabled per-language if it produces poor results.
|
||||||
|
|
||||||
|
Update `extract_code_snippets()`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn extract_code_snippets(source: &str, block_style: &BlockStyle) -> Vec<String> {
|
||||||
|
let mut snippets = keyword_extract(source, block_style);
|
||||||
|
|
||||||
|
if snippets.len() < 20 && has_structural_fallback(block_style) {
|
||||||
|
let structural = structural_extract(source, block_style);
|
||||||
|
// Add structural snippets that don't overlap with keyword ones
|
||||||
|
for s in structural {
|
||||||
|
if !snippets.contains(&s) {
|
||||||
|
snippets.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snippets.truncate(200);
|
||||||
|
snippets
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Structural extraction for Braces languages
|
||||||
|
|
||||||
|
`structural_extract_braces(source)`:
|
||||||
|
1. Scan for lines containing `{` where brace depth transitions from 0→1 or 1→2
|
||||||
|
2. Capture from that line until depth returns to its starting level
|
||||||
|
3. Apply the same quality filters: 3-30 lines, 20+ non-whitespace chars, ≤800 bytes
|
||||||
|
4. Skip noise blocks: reject snippets where first non-blank line is only `{`, or where the block is just imports/use statements
|
||||||
|
|
||||||
|
#### Structural extraction for Indentation languages
|
||||||
|
|
||||||
|
`structural_extract_indent(source)`:
|
||||||
|
1. Scan for non-blank lines at indentation level 0 (top-level) that are followed by indented lines
|
||||||
|
2. Capture the top-level line + all subsequent lines with greater indentation
|
||||||
|
3. Apply same quality filters
|
||||||
|
4. Skip noise: reject if all body lines are `import`/`from`/`use`/`#include` statements
|
||||||
|
|
||||||
|
#### Structural extraction for EndDelimited languages
|
||||||
|
|
||||||
|
`structural_extract_end(source)`:
|
||||||
|
1. Scan for lines at top-level indentation followed by indented body ending with `end`
|
||||||
|
2. Same quality filters and noise rejection
|
||||||
|
|
||||||
|
#### Noise filtering
|
||||||
|
|
||||||
|
A snippet is "noise" and should be rejected if:
|
||||||
|
- First meaningful line (after stripping comments) is just `{` or `}`
|
||||||
|
- Body consists entirely of `import`, `use`, `from`, `require`, `include`, or blank lines
|
||||||
|
- It's a single-statement block (only 1 non-blank body line after the opening)
|
||||||
|
|
||||||
|
### Step 2.5.4: Add more source URLs for low-count languages
|
||||||
|
|
||||||
|
After implementing the extraction improvements, re-run `test_verify_repo_urls` to identify languages still under 100 snippets. For those, add 1-2 more source file URLs from the same or new repos to increase raw material.
|
||||||
|
|
||||||
|
This step is intentionally deferred until after extraction improvements, since better extraction may push many languages over the 100 threshold without needing more URLs.
|
||||||
|
|
||||||
|
### Phase 2.5 Verification
|
||||||
|
|
||||||
|
1. `cargo test` — all existing tests pass
|
||||||
|
2. Run `cargo test test_verify_repo_urls -- --ignored --nocapture` — verify all 30 languages produce 50+ snippets (ideally 100+)
|
||||||
|
3. Spot-check structural fallback snippets for 3-4 languages — verify they contain real code, not just import blocks or noise
|
||||||
|
4. `cargo build --no-default-features` — compiles without network features
|
||||||
|
5. Verify no change to built-in snippet behavior (built-in snippets don't go through extraction)
|
||||||
188
docs/plans/2026-02-20-import-export-feature-plan.md
Normal file
188
docs/plans/2026-02-20-import-export-feature-plan.md
Normal 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
|
||||||
@@ -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")
|
||||||
@@ -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
|
||||||
221
docs/plans/2026-02-24-n-grams-statistics-tab.md
Normal file
221
docs/plans/2026-02-24-n-grams-statistics-tab.md
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
90
docs/plans/2026-02-26-adaptive-drill-word-diversity.md
Normal file
90
docs/plans/2026-02-26-adaptive-drill-word-diversity.md
Normal 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
|
||||||
@@ -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
|
||||||
150
docs/plans/2026-02-27-enhanced-setting-path-input.md
Normal file
150
docs/plans/2026-02-27-enhanced-setting-path-input.md
Normal 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
|
||||||
203
docs/plans/2026-02-27-generated-user-profile-data-for-testing.md
Normal file
203
docs/plans/2026-02-27-generated-user-profile-data-for-testing.md
Normal 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.
|
||||||
@@ -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.
|
||||||
154
docs/plans/2026-02-28-skill-tree-milestone-popups.md
Normal file
154
docs/plans/2026-02-28-skill-tree-milestone-popups.md
Normal 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
|
||||||
217
docs/plans/2026-03-08-internationalize-ui-text.md
Normal file
217
docs/plans/2026-03-08-internationalize-ui-text.md
Normal 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
|
||||||
117
docs/plans/2026-03-17-fix-remaining-untranslated-strings.md
Normal file
117
docs/plans/2026-03-17-fix-remaining-untranslated-strings.md
Normal 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
454
locales/cs.yml
Normal 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
454
locales/da.yml
Normal 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
454
locales/de.yml
Normal 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
454
locales/en.yml
Normal 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
454
locales/es.yml
Normal 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
454
locales/et.yml
Normal 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
454
locales/fi.yml
Normal 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
454
locales/fr.yml
Normal 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
454
locales/hr.yml
Normal 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
454
locales/hu.yml
Normal 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
454
locales/it.yml
Normal 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
454
locales/lt.yml
Normal 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
454
locales/lv.yml
Normal 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
454
locales/nb.yml
Normal 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
454
locales/nl.yml
Normal 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
454
locales/pl.yml
Normal 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
454
locales/pt.yml
Normal 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
454
locales/ro.yml
Normal 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
454
locales/sl.yml
Normal 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
454
locales/sv.yml
Normal 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
454
locales/tr.yml
Normal 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'
|
||||||
237
scripts/permalinkify_code_urls.py
Normal file
237
scripts/permalinkify_code_urls.py
Normal 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()
|
||||||
3873
src/app.rs
3873
src/app.rs
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user