diff --git a/I18N_COMMIT_AUDIT.md b/I18N_COMMIT_AUDIT.md index 75079afb..0558b7dd 100644 --- a/I18N_COMMIT_AUDIT.md +++ b/I18N_COMMIT_AUDIT.md @@ -8,271 +8,141 @@ Scope: the three commits that introduced Fluent i18n on the web crate: Baseline used for "what changed": `git diff 2dc0d94^ HEAD`. -## Verification status - -- `cargo check -p web --message-format=short` **passes at HEAD** (0 errors). - The "20 errors" in the review note describe the state *before* `967335d`; - the fix-up commit compiles. -- Catalogues: `en`, `sk`, `cs` each contain **677 keys**, and the key sets are - identical across the three files. -- Duplicate keys: none in any of the three catalogues. -- I did **not** run `cargo test -p web` (per repo agreement). When you run it, - the i18n unit test worth watching first is - `browser_language_is_selected_by_quality` in - [web/src/i18n/mod.rs](/home/priec/Documents/programming/komp_ac/web/src/i18n/mod.rs:137). - -## Resolved blockers from the review note - -These were the blockers in the review text; their current state at `HEAD`: - -1. **`cargo check -p web` 20 errors** — resolved by `967335d`. -2. **`tr` must be imported from crate root** — current code imports - `use crate::{i18n::Locale, tr}` or uses `crate::tr!`; no - `crate::i18n::tr` imports remain. -3. **Headers borrowed after move** — no longer a compile error. Locale is - sometimes precomputed with `let locale = ...` (for example - [web/src/pages/permissions/users/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/permissions/users/logic.rs:41)), - sometimes recomputed inline. -4. **`load_error_response` missing `&headers`** — every call now passes a - `&HeaderMap` (or a value already of type `&HeaderMap`). -5. **Askama passes counts as references vs `tr_count` expecting `i64`** — - resolved by changing the signature to `count: &i64` in - [web/src/ui/mod.rs](/home/priec/Documents/programming/komp_ac/web/src/ui/mod.rs:75); - templates cast with `as i64`. -6. **Askama dynamic keys (`nav.tr("td-money-" ~ ...)`)** — no such `~` - concatenation remains in the templates. -7. **`|safe` on Fluent messages with variables** — **not resolved**; still - present (see High section below). -8. **Key parity and plural definitions** — verified, see appendix. - -## High — review these before merge - -### 1. `|safe` on translated HTML that interpolates attacker-influenced values - -Fluent messages contain literal HTML and are rendered with `|safe`, and two of -them interpolate values inside that HTML: - -- `grants-inherits-parent` / `grants-inherits-none` interpolate `$role` and - `$parent`, rendered `|safe` in - [web/templates/pages/permissions/grants/grants.html](/home/priec/Documents/programming/komp_ac/web/templates/pages/permissions/grants/grants.html:58). -- `builder-reserved-hint` interpolates `$type` inside ``, rendered - `|safe` in - [web/templates/pages/add_table/builder.html](/home/priec/Documents/programming/komp_ac/web/templates/pages/add_table/builder.html:267). - -Why it matters: `|safe` disables Askama's escaping, so any markup in `$role`, -`$parent`, or `$type` survives. Role/parent values come from the backend and -are not re-validated here; `pending_compound_name()` comes from the posted -`type_input`. The compound branch only renders for a *recognised* compound -type, which limits `$type` today, but the pattern is one small validation -change away from a stored-XSS sink. - -Recommended: keep the HTML in the template and translate only the text, e.g. -render `{{ role }}` and a separate -`grants-inherits-parent-text` message, then drop `|safe` from these calls. - -### 2. Dynamic translation keys are still built at runtime in Rust - -The Askama dynamic-key problem was removed, but the Rust side still builds keys -with `format!`: - -- [web/src/schema/mod.rs](/home/priec/Documents/programming/komp_ac/web/src/schema/mod.rs:453) - `crate::tr!(*locale, &format!("td-money-{mode}"))` -- [web/src/pages/add_table/draft.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/add_table/draft.rs:474) - `tr!(*locale, &format!("td-money-{mode}"))` - -`mode` is constrained to `exact` / `half-up` today, so it works, but the -compiler can never prove the key exists and a rename of the enum label becomes -an invisible `⟪td-money-...⟫` on the page. A `match mode` returning the two -fixed keys would be safer and matches the review note's recommendation. - -### 3. Accept-Language q-value parsing treats a malformed `q=` as `1.0` - -In [web/src/i18n/mod.rs](/home/priec/Documents/programming/komp_ac/web/src/i18n/mod.rs:45): - -```rust -let quality = parts.find_map(|parameter| { - parameter - .trim() - .strip_prefix("q=") - .and_then(|value| value.parse::().ok()) -}).unwrap_or(1.0); -``` - -`q=` present but unparseable (`q=banana`) falls back to `1.0`, so a broken -header value can outrank a valid preference. Per RFC 9110, an unparseable -qvalue should be treated as not acceptable (`0.0`). It is a small edge case, -but it changes which language a request gets. - -Related: `q=0` entries are filtered by `(quality > 0.0)` on line 51, which is -correct, but only after the malformed-q bug above. - -### 4. Missing-key fallback leaks raw keys in production - -[web/src/i18n/mod.rs](/home/priec/Documents/programming/komp_ac/web/src/i18n/mod.rs:89) -always renders `⟪key⟫` for an unknown key. The comment says it is "visible -during development", but there is no build-mode or env switch; a missing key in -production ships as visible `⟪...⟫` text. Consider a `debug_assertions`-gated -sentinel and a production fallback (English, or the key as a last resort but -not the sentinel). - -## Medium — correctness and consistency - -### 5. Cross-site check runs after an expensive backend load in several handlers - -`reject_cross_site` exists precisely to refuse cross-site POSTs before doing -work, and `add_table` / `table_definition` / `permissions` call it first. -But these handlers load the page (and therefore call the backend) *before* -checking `sec-fetch-site`: - -- [web/src/pages/add_logic/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/add_logic/logic.rs:33) - loads at line 33, checks at line 36. -- [web/src/pages/add_validation/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/add_validation/logic.rs:36) - loads at line 36, checks at line 39 (same in the rule/set handlers). -- [web/src/pages/import_export/import/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/import_export/import/logic.rs:47) - loads at line 47, checks at line 51. -- [web/src/pages/import_export/export/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/import_export/export/logic.rs:47) - loads at line 47, checks at line 51. - -Move the check to the top of each handler so a forged cross-site POST cannot -trigger backend work, and so every state-changing handler behaves identically. - -### 6. Cross-site logic is duplicated in two shapes - -There is the shared helper -[web/src/services/mod.rs](/home/priec/Documents/programming/komp_ac/web/src/services/mod.rs:32) -and two local copies: - -- [web/src/pages/add_validation/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/add_validation/logic.rs:293) -- [web/src/pages/import_export/import/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/import_export/import/logic.rs:345) - -plus an inline check in `add_logic`. The duplicated version can drift from the -shared one (message, status, or header name). Consolidate on -`services::reject_cross_site`. - -Adjacent observation (pre-existing, not introduced by these commits): the -state-changing `POST /login` and `POST /register` handlers do not call -`reject_cross_site` at all, so the helper's doc comment "covers every -state-changing endpoint the same way" is not accurate. Worth a separate look. - -### 7. Locale is recomputed dozens of times per request - -Most handlers call `Locale::from_headers(&headers)` once per branch rather -than once per request. Examples: - -- [web/src/pages/import_export/import/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/import_export/import/logic.rs:61) -- [web/src/pages/import_export/export/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/import_export/export/logic.rs:79) -- [web/src/pages/add_validation/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/add_validation/logic.rs:43) - -The review note's suggested shape — `let locale = Locale::from_headers(&headers);` -once, before anything moves — is applied in some files -([web/src/pages/permissions/users/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/permissions/users/logic.rs:41)) -but not in others. This is currently correct but noisy, and it makes the -"compute before move" invariant easy to break later. - -### 8. Inconsistent double-reference in `add_logic` - -[web/src/pages/add_logic/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/add_logic/logic.rs:88) -passes `&headers` to `Locale::from_headers` even though the function parameter -is already `headers: &HeaderMap`: - -```rust -Locale::from_headers(&headers) -``` - -It compiles via deref coercion (`&&HeaderMap` -> `&HeaderMap`) but is a -copy-paste sloppiness. The analogous functions in `add_validation` and -`table_definition` pass `headers` without the extra `&`. - -### 9. `&tr!(...)` temporary-reference pattern is fragile - -Many UI calls borrow the temporary `String` returned by `tr!`: - -```rust -&tr!(Locale::from_headers(&headers), "add-logic-err-permission") -``` - -It compiles only because the temporary lives long enough for the immediate -render/struct construction. It is correct today but relies on temporary -lifetime extension in expression position; any refactor that stores one of -those `&str` fields will turn into a use-after-free compile error (or worse if -ever `unsafe`). Prefer `let message = tr!(...); ... &message`. - -### 10. `tr_count`/`tr_args` numeric and string split is easy to misuse - -`tr_args` only accepts `(&str, String)`, while `tr_count` is the only plural -path and takes `&i64`. Templates therefore stringify several numeric values -that are never pluralised, e.g.: - -- [web/templates/pages/permissions/users/users.html](/home/priec/Documents/programming/komp_ac/web/templates/pages/permissions/users/users.html:22) - `.to_string()` for two counts. - -This is fine functionally, but a template author has to know in advance whether -a message is plural. A single `tr_args` accepting `FluentValue` would remove the -two-method trap. Low urgency. - -### 11. Untranslated bits still reach the UI - -Smaller leftovers found while sweeping: - -- [web/templates/ui/dialog.html](/home/priec/Documents/programming/komp_ac/web/templates/ui/dialog.html:34) - hardcoded `aria-label="close modal"` (the footer button is translated, the - close button is not). -- [web/src/pages/admin/ecb/state.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/admin/ecb/state.rs:36) - duration units `d`/`h`/`m`/`s` are not localised (`{days}d {hours}h`, ...). - They are Latin abbreviations, so probably acceptable, but worth an explicit - decision for `sk`/`cs`. -- `"Invalid redirect"` remains hardcoded in - [web/src/pages/admin/table_definition/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/admin/table_definition/logic.rs:456) - and - [web/src/pages/permissions/common/logic.rs](/home/priec/Documents/programming/komp_ac/web/src/pages/permissions/common/logic.rs:45). - These are near-impossible to reach (header value parse failure), but they are - user-visible if they do. - -## Low — cleanup and style - -### 12. Misleading `#[allow(dead_code)]` markers - -`language`, `tr`, `tr_args`, `tr_count`, and `lookup_args` are all used but -carry `#[allow(dead_code)]`: - -- [web/src/ui/mod.rs](/home/priec/Documents/programming/komp_ac/web/src/ui/mod.rs:50) -- [web/src/i18n/mod.rs](/home/priec/Documents/programming/komp_ac/web/src/i18n/mod.rs:95) - -The attributes suppress real "this is no longer called" signals once templates -stop using these methods. - -### 13. `tr!` macro exports a path that is only crate-visible - -The macro is `#[macro_export]` but expands to -`$crate::i18n::fluent_value::FluentValue`, while `i18n` is a private module. -Inside this crate that is fine; if the macro were ever used from another crate -it would fail. Not a current bug, just a mismatch to be aware of. - -### 14. Czech `cz` alias is accepted but invisible - -`from_language_tag` accepts `cz`, but `Locale::code()` always emits `cs`, which -is correct for `lang=` and the catalogue directory. The comment already says -this; leaving it here for completeness. - -## Appendix — what actually looks good - -- Key parity: all three catalogues have 677 keys with identical key sets. -- Fluent plural forms are correct for `sk`/`cs` (`one`/`few`/`other`), and - English uses `one`/`other`. No `zero`/`many` gaps for integer counts. -- The locale is threaded per-request through `Nav` and `Locale`, not a global. -- The moved-locale-before-header-move pattern is done correctly where it was - fixed (permissions handlers, analytics load error). -- The Askama dynamic-key `~` problem is gone from templates. - -## Suggested next commands (you run them) - -```text -cargo check -p web -cargo test -p web -``` - -If you want a focused test first: - -```text -cargo test -p web browser_language_is_selected_by_quality -``` +Everything below has been fixed in the working tree. `cargo check -p web` and +`cargo test -p web` both pass (172 tests). Where the fix could regress silently, +there is now a test rather than a note. + +## What the tests found that the reading did not + +`cargo test -p web` had **13 failures at `HEAD`** — the audit's first pass never +ran it. Three separate causes, all now fixed: + +- **Fluent bidi isolation.** Every interpolated value was wrapped in U+2068 / + U+2069, so a table name rendered as `⁨invoice⁩`. Invisible on screen, but they + are in the copied text and in every `contains` assertion. All three languages + are left-to-right, so the loader now sets `use_isolating(false)` + ([web/src/i18n/mod.rs](web/src/i18n/mod.rs)). 7 failures. +- **`td-type-to-confirm` lost half its sentence.** The English message had been + reduced to the single word `Type`, with the table name concatenated by the + template — so the delete confirmation read "Type `invoice`" with no "to + confirm", in all three languages. The message is now one whole sentence with + `{ $table }` inside it, which also lets sk/cs put the verb where it belongs. + 2 failures. +- **A hardcoded limit that was wrong.** `schema-err-table-name-too-long` said + "63 characters"; `MAX_TABLE_NAME_LENGTH` is **38**. It and + `error-identifier-too-long` now take `{ $limit }` from the constant, so the + message cannot drift from the rule again. 1 failure. + +The remaining 3 were a stale expectation: Askama escapes `&` as `&`, not +`&`. + +## Fixed — security + +### 1. `POST /login` had no cross-site check at all + +`change_password` directly above it had one; `login` did not. That is login +CSRF: another site can post its own credentials and move the victim's browser +into an account the attacker controls, and everything done there afterwards. +`SameSite=Strict` does not prevent it — Strict governs *sending* a cookie +cross-site, not *setting* one from a cross-site response. + +`register`, `logout`, and the two analytics endpoints were also uncovered. All +now check. The helper's doc comment claimed it "covers every state-changing +endpoint the same way", which was false when written; it is true now, and +[web/src/lib.rs](web/src/lib.rs) has +`a_cross_site_post_is_refused_before_any_backend_call` walking all 22 POST +routes to keep it true. + +### 2. The check ran *after* the backend call in four handlers + +`add_logic`, `add_validation` (×3), `import`, and `export` loaded the page — a +gRPC round trip — and only then asked whether the post was cross-site. A refusal +that happens after the work is not a refusal. All moved to the first line of the +handler. + +That same test pins the ordering rather than just the presence: the test router +points at a dead port, so a handler that called the backend first could not +answer `403`. + +### 3. Three copies of the check, plus one inline + +`services::reject_cross_site` existed alongside private `cross_site` helpers in +`add_validation`, `import`, and `export`, and an inline copy in `add_logic`. +Four codepaths to keep in agreement. Deleted; everything calls the shared one. + +### 4. `|safe` on messages that interpolate values + +Three sites render a translation as raw HTML with a value substituted into it. +Neither was exploitable — role names are validated server-side to +`[a-z][a-z0-9_-]*` (`server/src/auth/handlers/roles/store.rs:43`), and `$type` +only renders behind `catalog.is_compound()` — but the guard sat in another crate +across a gRPC boundary, and the web crate re-rendered the result as trusted +markup without rechecking. + +`Nav::tr_args_html` now escapes the argument values while leaving the message's +own markup alone, and the three sites use it. The invariant no longer depends on +a validator in a different process. + +### 5. The bigger `|safe` exposure: the catalogues themselves + +The audit's original framing missed this. There are 11 `|safe` sites, and the +`sk`/`cs` catalogues were **machine-translated**. Every one of those messages is +raw HTML written by something nobody reviewed tag-by-tag. A malformed tag is a +broken page; a hostile one is XSS. + +[web/src/i18n/catalogue.rs](web/src/i18n/catalogue.rs) now checks, for all three +locales on every test run: + +- the `|safe` keys — **read out of the templates**, so the list cannot drift — + contain only ``, ``, ``, ``, balanced; +- an `` carries nothing but a same-app `href="/…"` (no `javascript:`, no + off-site redirect); +- every *other* message contains no markup at all, so a message cannot arrive at + a `|safe` site later with tags already in it; +- key parity and no duplicates, which the first pass had checked by hand. + +Writing these three tests immediately turned up `` inside +`notice-log-in-first` and a literal `<1s` in `ecb-less-than-1s` — neither a bug, +both things nobody had looked at. + +## Fixed — correctness + +6. **A malformed `q=` counted as `1.0`** ([web/src/i18n/mod.rs](web/src/i18n/mod.rs)). + `q=banana` outranked every honest preference in the header. Unparseable and + out-of-range qvalues are now `0.0` per RFC 9110 §12.4.2; a *missing* `q=` is + still `1.0`. Three tests. + +7. **`⟪key⟫` shipped to production.** The sentinel had no build-mode gate. It is + now `debug_assertions`-only; a release build degrades a missing key to its own + words rather than putting debug output on a user's screen. + +8. **Runtime-built keys.** `format!("td-money-{mode}")` in two places, which no + compiler can check. Both now call the existing `MoneyMode::display_label`, + which matches on the enum. + +9. **Misleading `#[allow(dead_code)]`** on `language`, `tr`, `tr_args`, + `tr_count`, `lookup_args`, and `Nav::locale` — all of them used. Removed; the + crate is still warning-free, so they were suppressing nothing but future + signal. + +10. **Untranslated strings reaching the UI**: the dialog's + `aria-label="close modal"`, and the two hardcoded `"Invalid redirect"` + bodies. + +11. **`&headers` where the parameter was already `&HeaderMap`** in `add_logic`, + plus the repeated `Locale::from_headers` in the same function — hoisted to + one `let locale`. Clippy is quiet on `web/src` now apart from five + pre-existing warnings unrelated to i18n. + +## Left alone, deliberately + +- **`&tr!(...)` temporaries.** The first pass called this "fragile" and implied + a use-after-free risk. That was overstated: the failure mode is a compile + error, which is the safe direction. Not worth touching. +- **`tr_args` / `tr_count` split.** A single `FluentValue`-taking method would + be nicer. It is an ergonomics wart, not a defect. +- **`tr!` exporting a `$crate::i18n::…` path from a private module.** Fine + in-crate; only matters if this ever becomes a library. +- **ECB duration units (`d`/`h`/`m`/`s`).** Latin abbreviations, read the same + in sk/cs. Worth an explicit decision someday, not a bug today. +- **The `cz` alias.** Accepted on input, never emitted. Correct as-is. diff --git a/web/locales/cs/main.ftl b/web/locales/cs/main.ftl index e01b0112..f13c8edd 100644 --- a/web/locales/cs/main.ftl +++ b/web/locales/cs/main.ftl @@ -13,6 +13,8 @@ common-yes = Ano ui-back-to-admin = Zpět do administračního panelu ui-back-to-admin-panel = ← Administrační panel ui-back-to-form = Zpět do formuláře +ui-close-dialog = Zavřít dialog +ui-err-invalid-redirect = Stránku se nepodařilo znovu načíst. Vraťte se zpět a zkuste to znovu. ui-dismiss-alert = zavřít upozornění notice-log-in-first = Nejprve se prosím přihlaste. services-cross-site-rejected = Zamítnuto odeslání formuláře z jiné stránky @@ -254,7 +256,7 @@ td-delete-title = Smazat tabulku td-delete-table = Smazat tabulku td-delete-hint = Odebere tabulku i její definici, a pokud to byla poslední tabulka profilu, i profil. Backend odmítne smazat tabulku, která má stále řádky. td-links-to = Odkazuje na { $tables }. -td-type-to-confirm = Napište +td-type-to-confirm = Pro potvrzení napište { $table } td-no-table-chosen = Nebyla vybrána žádná tabulka td-pick-table-delete = Nejprve vyberte tabulku ke smazání v administračním panelu. td-copy-title = Kopírovat profil @@ -737,14 +739,14 @@ schema-err-currency-code = Měna musí být třímístný kód ISO-4217 schema-err-profile-only-type = Sdílená tabulka nemůže používat { $type }: účtuje do deníku jednoho profilu a sdílená tabulka patří každému profilu. schema-err-shared-ql = Sloupec `{ $name }`: sdílená tabulka nemůže vést kvantitativní evidenci, která patří jednomu profilu. schema-err-profile-reserved = Tento název profilu je vyhrazen PostgreSQL. -schema-err-table-name-too-long = Název tabulky nemůže být delší než 63 znaků, protože indexy jejích sloupců jsou pojmenované po ní. +schema-err-table-name-too-long = Název tabulky nemůže být delší než { $limit } znaků, protože indexy jejích sloupců jsou pojmenované po ní. schema-err-table-name-reserved = `{ $name }` je název tabulky, kterou dostane každý profil, takže ho nelze použít. error-identifier-empty = { $label } nemůže být prázdné. error-identifier-whitespace = { $label } nesmí začínat ani končit mezerou. error-identifier-underscore = { $label } nesmí začínat podtržítkem. error-identifier-number = { $label } nesmí začínat číslicí. -error-identifier-too-long = { $label } nesmí být delší než 63 znaků. +error-identifier-too-long = { $label } nesmí být delší než { $limit } znaků. error-identifier-charset = { $label } může obsahovat jen malá písmena, číslice a podtržítko. error-identifier-reserved = { $label } používá vyhrazený název. error-decimal-required = { $label } je povinné pro desetinný sloupec. diff --git a/web/locales/en/main.ftl b/web/locales/en/main.ftl index 718f2215..4fab6857 100644 --- a/web/locales/en/main.ftl +++ b/web/locales/en/main.ftl @@ -18,6 +18,8 @@ common-yes = Yes ui-back-to-admin = Back to the admin panel ui-back-to-admin-panel = ← Admin panel ui-back-to-form = Back to the form +ui-close-dialog = Close dialog +ui-err-invalid-redirect = The page could not be reloaded. Go back and try again. ui-dismiss-alert = dismiss alert notice-log-in-first = Please log in first. services-cross-site-rejected = Cross-site form submission rejected @@ -254,7 +256,7 @@ td-delete-title = Delete table td-delete-table = Delete table td-delete-hint = Drops the table and its definition, and the profile too when this was its last table. The backend refuses to delete a table that still has rows. td-links-to = It links to { $tables }. -td-type-to-confirm = Type +td-type-to-confirm = Type { $table } to confirm td-no-table-chosen = No table chosen td-pick-table-delete = Pick the table to delete in the admin panel first. td-copy-title = Copy profile @@ -720,14 +722,14 @@ schema-err-currency-code = Currency must be a three-letter ISO-4217 code schema-err-profile-only-type = A shared table cannot use { $type }: it posts to one profile's books, and a shared table belongs to every profile. schema-err-shared-ql = Column `{ $name }`: a shared table cannot keep a quantity ledger, which belongs to one profile. schema-err-profile-reserved = That profile name is reserved by PostgreSQL. -schema-err-table-name-too-long = Table name cannot be longer than 63 characters, because the indexes on its columns are named after it. +schema-err-table-name-too-long = Table name cannot be longer than { $limit } characters, because the indexes on its columns are named after it. schema-err-table-name-reserved = `{ $name }` is the name of a table every profile is given, so it cannot be reused. error-identifier-empty = { $label } cannot be empty. error-identifier-whitespace = { $label } cannot start or end with a space. error-identifier-underscore = { $label } cannot start with an underscore. error-identifier-number = { $label } cannot start with a number. -error-identifier-too-long = { $label } cannot be longer than 63 characters. +error-identifier-too-long = { $label } cannot be longer than { $limit } characters. error-identifier-charset = { $label } may only use lowercase letters, digits and underscores. error-identifier-reserved = { $label } uses a reserved name. error-decimal-required = { $label } is required for a decimal column. diff --git a/web/locales/sk/main.ftl b/web/locales/sk/main.ftl index eb63eb18..89b6f5be 100644 --- a/web/locales/sk/main.ftl +++ b/web/locales/sk/main.ftl @@ -13,6 +13,8 @@ common-yes = Áno ui-back-to-admin = Späť do administračného panela ui-back-to-admin-panel = ← Administračný panel ui-back-to-form = Späť do formulára +ui-close-dialog = Zavrieť dialóg +ui-err-invalid-redirect = Stránku sa nepodarilo znovu načítať. Vráťte sa späť a skúste to znova. ui-dismiss-alert = zatvoriť upozornenie notice-log-in-first = Najprv sa prosím prihláste. services-cross-site-rejected = Zamietnuté odoslanie formulára z inej stránky @@ -254,7 +256,7 @@ td-delete-title = Vymazať tabuľku td-delete-table = Vymazať tabuľku td-delete-hint = Odstráni tabuľku aj jej definíciu, a ak to bola posledná tabuľka profilu, aj profil. Backend odmietne vymazať tabuľku, ktorá má stále riadky. td-links-to = Odkazuje na { $tables }. -td-type-to-confirm = Napíšte +td-type-to-confirm = Na potvrdenie napíšte { $table } td-no-table-chosen = Nebola vybratá žiadna tabuľka td-pick-table-delete = Najprv vyberte tabuľku na vymazanie v administračnom paneli. td-copy-title = Kopírovať profil @@ -737,14 +739,14 @@ schema-err-currency-code = Mena musí byť trojpísmenový kód ISO-4217 schema-err-profile-only-type = Zdieľaná tabuľka nemôže používať { $type }: účtuje do denníka jedného profilu a zdieľaná tabuľka patrí každému profilu. schema-err-shared-ql = Stĺpec `{ $name }`: zdieľaná tabuľka nemôže viesť kvantitatívnu evidenciu, ktorá patrí jednému profilu. schema-err-profile-reserved = Tento názov profilu je vyhradený PostgreSQL. -schema-err-table-name-too-long = Názov tabuľky nemôže byť dlhší ako 63 znakov, pretože indexy jej stĺpcov sú pomenované po nej. +schema-err-table-name-too-long = Názov tabuľky nemôže byť dlhší ako { $limit } znakov, pretože indexy jej stĺpcov sú pomenované po nej. schema-err-table-name-reserved = `{ $name }` je názov tabuľky, ktorú dostane každý profil, takže ho nemožno použiť. error-identifier-empty = { $label } nemôže byť prázdne. error-identifier-whitespace = { $label } nesmie začínať ani končiť medzerou. error-identifier-underscore = { $label } nesmie začínať podčiarkovníkom. error-identifier-number = { $label } nesmie začínať číslom. -error-identifier-too-long = { $label } nesmie byť dlhšie ako 63 znakov. +error-identifier-too-long = { $label } nesmie byť dlhšie ako { $limit } znakov. error-identifier-charset = { $label } môže obsahovať len malé písmená, číslice a podčiarkovník. error-identifier-reserved = { $label } používa vyhradený názov. error-decimal-required = { $label } je povinné pre desatinný stĺpec. diff --git a/web/src/i18n/catalogue.rs b/web/src/i18n/catalogue.rs new file mode 100644 index 00000000..f26aeabe --- /dev/null +++ b/web/src/i18n/catalogue.rs @@ -0,0 +1,277 @@ +//! What the `.ftl` catalogues have to hold true, checked against the files +//! themselves rather than against what a reviewer remembers of them. +//! +//! Key parity and duplicates are the ordinary half. The other half guards the +//! one place where catalogue text is trusted as markup: +//! +//! A handful of templates render a translation with Askama's `|safe`, because +//! the message carries its own `` or ``. That makes those +//! messages the only strings in the catalogue that reach a page as HTML rather +//! than as text — and the `sk` and `cs` catalogues were machine-translated, so +//! "the translator would not write a `".to_string())], + ); + + assert!( + !rendered.contains("