l10n: put the translation files through the wringer — all 36 locales at full parity - #2634
Draft
SudoThijn wants to merge 82 commits into
Draft
l10n: put the translation files through the wringer — all 36 locales at full parity#2634SudoThijn wants to merge 82 commits into
SudoThijn wants to merge 82 commits into
Conversation
Rewrites every l10n/*.js with keys in case-insensitive alphabetical order so that subsequent translation diffs are small and reviewable. Previously the bundles carried historical insertion order, which meant any tool that rewrote a file produced a whole-file diff and buried the actual changes. Sorting is case-insensitive with a code-unit tie-break, so it is deterministic and does not depend on the Node/ICU localeCompare implementation. Purely mechanical: values were taken from HEAD and verified unchanged. Verified across all 37 files / 54113 keys: zero value changes, zero keyset changes, all files valid JS, OC.L10N.register executes with the expected key count and plural-forms string.
Ten Dutch entries were still defective after the main Dutch pass: Half-translated machine output, Dutch words in English word order: "Weet u zeker dat u wilt verwijderen the geselecteerd audittrails?" "No bestands have been extracted yet" "Kon niet update schema properties" "Select default organisatie" and two "Use filters to narrow down ..." strings left almost fully English Formal address, which the rest of the bundle and Nextcloud core both avoid: "Dank u!" -> "Bedankt!", "uw" -> "je", "Weet u zeker" -> "Weet je zeker" These slipped through because the previous check had no formal-address test at all, and its English-marker list excluded so many words that collide with Dutch that badly-mixed strings scored as clean. Note the embeddings warning exists twice in the bundle, once with a literal \n escape and once with a real newline; only the escaped copy was broken. Verified: 0 keys lost, 0 added, exactly 10 values changed, 0 formal-address entries remaining, 0 plural-arity errors.
Brings l10n/de.js from 950 to full coverage of every string the frontend
requests via t()/n(). No entry has a value equal to its key.
harvested 63 existing Transifex output for the identical source
string, from sibling Conduction apps and the
Nextcloud server tree
hand-translated 970 written for this app
formal -> informal 98 see below
placeholders 109 entries whose value equalled their key
identity keys 65 removed, see below
Register: informal du/dein throughout. Nextcloud ships the formal German
variant as a separate de_DE locale, and openregister has no de_DE, so plain
de.js is the informal bundle. Verified against the server tree:
core/l10n/de.js is 58 informal vs 1 formal, settings 108 vs 4. 98 pre-existing
entries used "Sie"/"Ihr" and were rewritten ("Verwalten Sie Ihre Register" ->
"Verwalte deine Register").
Identity strings are ABSENT rather than stored as value===key. For source
strings that are genuinely the same word in German ("Code", "Status", "Port",
"Maximum", "Repository") or must not be translated at all ("sk-...",
"https://example.com/webhook") OC.L10N falls back to the English source and
renders identical text, but the entry is no longer indistinguishable from an
untranslated placeholder. Tracked in de-identity.json.
Terminology: the source embeds Dutch legal vocabulary, which is mapped to
German equivalents rather than passed through -- "Inzage (Art 15)" ->
"Auskunft (Art. 15)", "Art 17 vergetelheid" -> "Recht auf Vergessenwerden",
"Art 20 portabiliteit" -> "Datenübertragbarkeit", "Bewaartermijn" ->
"Aufbewahrungsfrist", "verwerkingsactiviteit" -> "Verarbeitungstätigkeit",
"verantwoordingsdocument" -> "Rechenschaftsdokument", GDPR/AVG -> DSGVO.
Three harvested values were rejected as wrong for this app's context:
"Open" -> "Öffnen" (a button, not the adjective "Offen"), "Right" -> "Recht"
(an RBAC permission, not the direction "Rechts"), "Subject" ->
"Betroffene Person" (a GDPR data subject, not an email "Betreff").
Known source-side limitation: the "object{plural}" family interpolates a
literal "s"/"" for pluralisation, which cannot work in German. Those render as
"Objekt(e)"; "schema{plural}" keeps the placeholder because German does
pluralise Schema with -s.
Verified: 1996/1996 frontend keys translated, 0 absent, 0 value===key anywhere
in the bundle, 0 plural-arity errors, 0 unreviewed formal address, all 5 plural
keys carry 2-form arrays, file is valid JS and OC.L10N.register executes with
2324 keys. No pre-existing translation was lost: the only 33 baseline keys
removed were deliberate identity strings that had been value===key.
Brings l10n/fr.js from 951 to full coverage of every string the frontend
requests via t()/n(). No entry has a value equal to its key.
harvested 61 existing Transifex output for the identical source
string, from sibling apps and the Nextcloud server tree
hand-translated 920 written for this app
placeholders 27 entries whose value equalled their key
identity keys 86 removed, see below
Register: FORMAL (vous/votre), which is what Nextcloud core actually uses for
French. This differs from the informal de/nl bundles and was measured, not
assumed: across server/{core,lib,apps/*}/l10n, fr is 39 informal vs 412 formal
(core alone 76 vs 9, settings 171 vs 16), and core ships no separate formal
French variant. The rule is "match Nextcloud core", and for French that means
vous.
Identity strings are ABSENT rather than stored as value===key, so the runtime
falls back to the English source and renders identical text without the entry
being indistinguishable from an untranslated placeholder. French shares a great
deal of vocabulary with English here ("Action", "Configuration", "Description",
"Format", "Total", "Type", "Version", "Notifications", "Expiration", "Notes",
"Score", "Public"), so the identity list is larger than German's. Tracked in
fr-identity.json.
Typography follows French convention: a space before ':' '?' '!' and guillemets
« » for quoted UI labels, as Nextcloud French does.
Terminology: Dutch legal vocabulary in the source is mapped to French GDPR
terms -- "Inzage (Art 15)" -> "Accès (art. 15)", "Art 17 vergetelheid" ->
"droit à l'oubli", "Art 20 portabiliteit" -> "portabilité", "Bewaartermijn" ->
"Durée de conservation", "verwerkingsactiviteit" -> "activité de traitement",
"verantwoordingsdocument" -> "document de responsabilité", AVG/GDPR -> RGPD.
Five harvested values were rejected as wrong for this app's context:
"Open" -> "Ouvrir" (button, not the adjective "Ouvert"), "View" -> "Afficher"
(action verb, not the noun "Affichage"), "Right" -> "Droit" (RBAC permission,
not the direction "Droite"), "Subject" -> "Personne concernée" (GDPR data
subject, not email "Objet"), "Link" -> "Associer" (dialog button verb, not the
noun "Lien").
Note French plural-forms is "nplurals=2; plural=(n > 1)", unlike German's
"(n != 1)"; all 5 plural keys carry correctly ordered 2-form arrays.
Verified: 1996/1996 frontend keys translated, 0 absent, 0 value===key anywhere
in the bundle, 0 plural-arity errors, 0 unreviewed wrong-register entries, file
is valid JS and OC.L10N.register executes with 2315 keys. No pre-existing
translation was lost: all 49 baseline keys removed were deliberate identity
strings that had been value===key.
Removes entries that are BOTH unreachable and untranslated:
* no t()/n() call requests the key — neither a literal call nor any of the
enumerated dynamic ones (see below), and
* the value equals the key, i.e. it was never translated.
This cannot change what any user sees. An entry whose value equals its key
already renders the English source string; once removed, OC.L10N falls back to
the English source and renders the same string. Verified mechanically across all
37 files: 904 keys removed, 0 of them holding anything other than value===key,
and 0 existing values altered.
Unreachable keys that DO carry a real translation were deliberately left alone.
They are harmless, and deleting them would risk discarding real translation work
if the reachability analysis were ever incomplete.
Most of these are residue from two known events. Commit 03cda6c ("fix(i18n):
unwrap numeric/URL placeholders from t() per PR #1273 review") correctly stopped
wrapping numeric and infrastructure-URL placeholders in t(), but never removed
the keys it orphaned -- hence "3", "30", "http://localhost:11434" and
"https://api.fireworks.ai/inference/v1" in every bundle. Separately, the SOLR /
Zookeeper settings UI was removed from src/ without cleaning its strings, so
keys like "Zookeeper Hosts" and "SOLR Connection Settings" survive with no call
site. Confirmed absent from src/ before removal.
en.js is excluded: it is the English source bundle, where value===key is correct
by definition rather than a placeholder.
All 37 files remain valid JS.
…traction missed
Some strings reach t() as a variable rather than a literal:
t('openregister', action) PermissionMatrix.vue:41, over
actions: ['read','create','update','delete','manage']
t('openregister', step.status) ApprovalStepList.vue:17, over the approval
statuses used by lib/Controller/ApprovalController.php
t('openregister', preset.label) DashboardIndex.vue:91/120/360, over the date
presets declared at :212-216
None of these keys can be found by scanning for literal t() arguments, so all 15
were absent from every bundle. The Permission Matrix column headers, the
approval-status badges and the dashboard date-range presets were therefore
rendering in English even in locales reported as fully translated. The key list
now lives in dynamic-keys.json with its provenance, and feeds the same
absent/placeholder/register checks as every other key.
Two dynamic sites remain un-enumerable and are documented as such: ApprovalStepList
step.role (schema-configured, arbitrary) and MainMenu.vue:76 translate(key) (app
manifest labels). A third, RegisterSchemaCard.vue:714, wraps a runtime-built
template string in t() and so can never match a catalogue key — that is a source
bug rather than a missing translation.
Also in this commit, for nl only:
* "Driver" -> "Stuurprogramma", "Url" -> "URL", "object{plural}" -> "object(en)",
"log{plural}" -> "logboek(en)", and both real-newline variants of the
PERMANENT DELETION WARNING, which had only been done for the \n-escaped copies
* 29 identity strings ("Code", "Status", "Type", "Dashboard", "sk-...") converted
from value===key to absent, matching the treatment already applied to de and fr
* nl-identity.json reconstructed (72 entries) so the check is reproducible
Verified: nl, de and fr each report 2011/2011 keys translated with 0 absent,
0 value===key, 0 wrong-register and 0 plural-arity errors.
Brings es.js from 972 to 2011 reachable keys: 983 new translations,
16 placeholder entries replaced with real Spanish, and 17 legitimate
identity strings dropped so they fall back to the English source
instead of masquerading as translations.
Also fixes 106 PRE-EXISTING entries that the earlier register check
could not see. Spanish is pronoun-dropping, so formality lives in the
verb, not in a pronoun: "Seleccione un registro" is formal address with
no "usted" anywhere, and a check for the pronoun alone reported the
bundle clean. Widening the check to the usted imperative (3rd-sg
subjunctive: -ar -> -e, -er/-ir -> -a) plus the possessive su/sus
surfaced 107 formal entries, 105 of which were converted to the tú
forms Nextcloud core uses for Spanish. The remaining one was a
terminology split ("Rastro de auditoría" against nine occurrences of
"registro de auditoría").
The verb list was harvested from the sentence-initial and
post-punctuation words actually present in es.js rather than guessed —
"Gestione" and "Habilite" were both missing from the guessed list.
su/sus is both the formal "your" and the third-person "his/her/its/
their", so it is gated on the English source containing "your";
where the source says "its"/"their", su/sus is simply correct. Ten
positive/negative controls cover the gate. Only five strings that say
both "your" and "their" still need suppressing by hand.
Harvest sources are now ranked core-first. Previously the walk order
let sibling Conduction apps shadow server/, so generic UI strings were
taken from apps whose own Spanish is not authoritative. Eleven of the
67 harvested values were still wrong for this app's context and were
rewritten: Open/View/Link are verb buttons here (Abrir/Ver/Vincular,
not Abierto/Vista/Enlace), Right is a permission (Derecho, not the
direction Derecha), Subject is the GDPR data subject (Interesado, not
the email Asunto), and Languages are human languages (Idiomas, not
Lenguajes).
Unlike German and Dutch, Spanish pluralises with -s exactly as English
does, so the object{plural}/register{plural} family interpolates
correctly here and is translated with the placeholder intact.
Verified: 2011/2011 reachable keys, 0 absent, 0 value===key, 0 hybrid,
0 wrong register, 0 plural-arity errors, valid JS, and OC.L10N.register
loads all 2355 entries. Backend l10n/es.json is untouched.
Brings it.js from 969 to 2011 reachable keys: 983 new translations, 17
placeholder entries replaced with real Italian, and 19 legitimate
identity strings dropped so they fall back to the English source
instead of masquerading as translations.
No pre-existing entry needed rewriting — unlike Spanish, the Italian
already in the bundle was consistently informal (Seleziona, Gestisci,
Crea). That is a verified result rather than an assumption: the register
check for Italian was as blind as the Spanish one, matching only
(Lei|Vi preghiamo), which finds almost nothing in a pronoun-dropping
language. It now covers the usted-equivalent imperative, which in
Italian is the MIRROR of Spanish: -are -> -i (selezioni), -ere/-ire -> -a
(scelga, inserisca). That collides head-on with the INFORMAL -ere/-ire
imperative, which also ends in -i (scegli, inserisci), so the ending
alone proves nothing and the verb list is explicit.
Filtri, Ordini, Usi, Controlli, Termini, Continui and Faccia are left
out on purpose: each is an ordinary Italian noun or adjective and would
bury real hits in noise. Infinitive-as-instruction ("Eliminare",
"Utilizzare") is standard register-neutral Italian UI and is not
flagged. 16 positive/negative controls cover the pattern, including one
that caught a genuine inversion in my first draft: "Premi" is the
INFORMAL imperative of premere and had been listed as formal.
Six of the 70 harvested values were wrong for this app's context.
Subject was the worst: pipelinq's "Oggetto" is the email subject AND
this bundle's own word for Object, so a GDPR data subject column would
have read "Object" — it is "Interessato". Open/Link are verb buttons
here (Apri/Collega, not Aperto/Collegamento), Right is a permission
(Diritto, not the direction Destra), Other labels a group (Altri), and
Mappings is properly "Mappature". "Test" was also reclassified: it is a
webhook action button, so Italian wants the verb "Prova", not the noun
loanword.
Italian pluralises by vowel change, not with -s, so the
object{plural}/register{plural} family CANNOT use the literal "s" the
source interpolates — it would render "oggettos". Those are written with
an explicit both-forms notation (oggetto/i, registro/i, schema/i), and
file{plural}/log{plural} simply drop the placeholder because both nouns
are invariant in Italian.
"{count} email" is the one entry deliberately written as value===key:
"email" is invariant, so both plural forms equal the English source, and
leaving the key ABSENT is not equivalent — OC.L10N would fall back to
the English plural rule and render "{count} emails". apply.js gained a
narrow --allow-identity opt-in for exactly this case; --force still does
not lift the value===key ban.
Verified: 2011/2011 reachable keys, 0 absent, 0 hybrid, 0 wrong
register, 0 plural-arity errors, valid JS, OC.L10N.register loads all
2353 entries, and the only value===key entry is the documented
invariant plural. No pre-existing translation was altered. Backend
l10n/it.json is untouched.
Brings pt.js from 973 to 2011 reachable keys: 984 new translations, 17
placeholder entries replaced with real Portuguese, and 15 legitimate
identity strings dropped so they fall back to the English source
instead of masquerading as translations.
PORTUGUESE INVERTS THE SPANISH RULE, and getting that backwards would
have wrecked the whole locale. In Spanish, "Seleccione" / "su" is
deferential usted and had to be replaced with tú forms. In Portuguese
the same 3rd-person morphology ("Selecione" / "seu") is the NEUTRAL você
register that Portuguese software UI uses, and the 2nd-person tu forms
are the ones that read wrong. Measured rather than assumed, across
server/{core,lib,apps/*}:
pt_BR tu 0 : você 438
pt_PT tu 4 : você 128
Both variants converge on você, so the single generic "pt" bundle this
app ships is correct for both. The register check for pt was therefore
written to flag the TU forms, with a comment saying so, because the
obvious next move for anyone reading the Spanish entry would be to
"fix" it by analogy and break 2000 strings. 10 positive/negative
controls pin the direction down.
The bundle is EUROPEAN Portuguese and the new strings follow it:
ficheiro, registo, eliminar, guardar, utilizador, aplicação,
definições. That was measured too (306 pt_PT-style terms against 11
apparent pt_BR ones, and all 11 turned out to be correct anyway —
"padrão" translates Pattern, not "default", and "configurações"
renders configurations as distinct from settings/"definições").
Existing style is also preserved: infinitive for controls (Selecionar,
Criar, Eliminar) and você imperative for prose instructions (Selecione,
Configure, Introduza).
Only one pre-existing entry was changed: "Trilho de auditoria" against
nine occurrences of "registo de auditoria".
Three of the 31 harvested values were wrong for this app's context —
Right is a permission (Direito, not the direction Direita), Link is a
confirm button (Associar, not the noun Ligação), and Edit Endpoint kept
the loanword to match the bundle's existing "Adicionar Endpoint" rather
than openconnector's "ponto final". None of the harvest was
authoritative here: core ships pt_BR and pt_PT but no bare pt, so every
candidate came from a sibling Conduction app and each was reviewed.
Like Spanish and unlike Italian, Portuguese pluralises with -s, so the
object{plural}/register{plural} family works with the literal "s" the
source interpolates and is translated with the placeholder intact.
Verified: 2011/2011 reachable keys, 0 absent, 0 value===key, 0 hybrid,
0 wrong register, 0 plural-arity errors, valid JS, and OC.L10N.register
loads all 2357 entries. Backend l10n/pt.json is untouched.
Brings sv.js from 958 to 2011 reachable keys: 981 new translations, 15
placeholder entries replaced with real Swedish, and 23 legitimate
identity strings dropped so they fall back to the English source
instead of masquerading as translations. No pre-existing translation
was altered.
Swedish shares far more of this app's vocabulary outright than the
Romance locales do, which cut both ways. The identity list is nearly
twice as long (register, schema, status, organisation, person, port,
version, format, maximum, minimum are all Swedish words with the same
spelling) and the hybrid detector was firing on nine perfectly good
Swedish strings purely because they contained "register" and "schema".
Those words are now declared as collisions for sv, the same treatment
de and nl already needed, so a hybrid hit means something again.
Register/Schema are capitalised mid-sentence throughout, which is not
standard Swedish orthography but IS this bundle's established
convention for the domain entities — measured at 54 capitalised
against 2 lowercase before I added anything, so the new strings follow
it rather than splitting the file two ways. Terms with their own
precedent keep it: "slutpunkt" stays lowercase to match the existing
"Lägg till slutpunkt".
Swedish is informal by default (du) — 358 informal against 0 formal in
core — and needed no register conversion.
Four of the 45 harvested values were wrong for this app's context: Open
is a verb button here (Öppna, not the adjective Öppen that circles
supplies), Right is a permission (Rättighet, not the direction Höger),
Link is a confirm button (Länka, not the noun Länk), and Edit Endpoint
was recased to match the bundle. Core's "Webbadress" for URL was
deliberately NOT taken: this bundle uses "URL" consistently (Bas-URL,
Databas-URL, "namn eller URL"), so the standalone label stays identity.
Swedish pluralises by suffix change or not at all, never with -s, so
the {plural} family cannot use the literal "s" the source interpolates.
These are count-labels under a number, so: object{plural} and
register{plural} drop the placeholder entirely (both nouns are
invariant — "5 objekt", "5 register"), while file{plural},
log{plural} and schema{plural} use an explicit both-forms notation
(fil(er), logg(ar), schema(n)).
Verified: 2011/2011 reachable keys, 0 absent, 0 value===key, 0 hybrid,
0 wrong register, 0 plural-arity errors, valid JS, and OC.L10N.register
loads all 2347 entries with no benign suppressions needed at all.
Backend l10n/sv.json is untouched.
Every string the frontend reaches via t()/n() now has a real Danish
translation. 981 new entries, 15 English placeholders replaced, 23
identity strings dropped so the runtime falls back to the source.
No pre-existing translation was altered except one grammar fix (below).
Register: informal (du/din/dit/dine), verified against Nextcloud core.
The old detector for Danish was /(De|Deres)/ and reported 15 hits in
core. All 15 were false positives: lowercase de/dem/deres are the
ordinary words for they/them/their -- and "de" is also the definite
article -- so they capitalise at sentence start and become
indistinguishable from the formal pronouns. "De genererede billeder" is
"The generated images"; "Deres stier" is "Their paths". Core is
572 informal : 0 genuinely formal.
Rewrote the detector to require a MID-SENTENCE capital, excluding every
position where a capital is explained by orthography rather than
register (string start, after sentence punctuation, after newline or
bullet, after an opening quote). Danish opens quotes with the glyph
English uses to close them, so both directions count as sentence start.
Validated on 16 must-not-fire and 6 must-fire controls, then swept all
4487 Danish strings in core: 0 hits, down from 15. Applied to nb/nn too.
Domain-term capitalisation is the INVERSE of Swedish. Swedish measured
54 capitalised : 2 lowercase and so keeps Register/Schema capitalised
mid-sentence; Danish measures 1 : 15 and follows standard orthography,
so register/skema/organisation/objekt stay lowercase.
Bundle consistency over core, twice:
- imperative of -ere verbs: bundle is Aktivér 13:0, core prefers
Aktiver 21:7. Followed the bundle.
- "endpoint": bundle 5:0, harvest offered core-adjacent "slutpunkt".
Kept endpoint.
Where the bundle already had a mapping it wins outright: Host -> Vært,
so unlike Italian this bundle needed no identity entry for "Host *".
Harvest corrections (3 of 41 candidates were wrong in context):
- "Right" is a permissions-table header (EditOrganisation.vue:288),
not a direction -- Rettighed, not Højre.
- "Assigned collaborative tags" arrived as "Tildelte samarbejds tags";
Danish compounds are one word -> samarbejdstags.
- "Edit Endpoint" -> Rediger endpoint, per the bundle term above.
Single-word keys were resolved at the call site, not from the harvest:
Subject is the GDPR data subject (AvgIndex.vue:384) -> Registreret, not
the email sense; Open/View/Reject/Merge/Reverse/Link are verb buttons;
Score/Step/Survivor read off their table headers.
The {plural} source bug (the caller interpolates a literal "s") cannot
work in Danish, which pluralises by suffixing -er and mutates the stem
of register. Resolved per word: objekt(er), fil(er), log(ge),
skema(er), and register/registre where the stem changes.
48 identity strings stay absent rather than being written as value===key
(acronyms, loanwords Danish shares, and literal input examples such as
HTTP headers and YAML snippets, which would stop being valid hints if
translated). Rationale for each is recorded per key.
Also fixes a pre-existing grammar error: "Objekter bløde slettes" ->
"Objekter blødslettes".
…eys)
Every string the frontend reaches via t()/n() now has a real Norwegian
translation. 984 new entries, 15 English placeholders replaced, 20
identity strings dropped so the runtime falls back to the source. No
pre-existing translation was altered.
Register: informal (du/din/ditt/dine), measured against Nextcloud core
rather than assumed from Danish. The De/Dem/Deres detector corrected in
the previous commit carries over unchanged and pays off immediately:
the old /(De|Deres)/ found 10 hits in Norwegian core, all of them
sentence-initial "The/They"; the mid-sentence-capital version finds 0.
Core is 500 informal : 0 genuinely formal.
Norwegian agrees with Danish on orthography (register/skjema lowercase
mid-sentence, measured 1:15) but disagrees on almost everything else,
which is why each convention was re-measured instead of inherited:
- imperative of -ere verbs: nb is "Aktiver" 13:0, exactly mirroring
da's "Aktivér" 13:0. Same verb, opposite spelling.
- error phrasing: nb bundle uses the ACTIVE "Kunne ikke <infinitive>",
where the Danish bundle preferred a passive construction.
- ellipsis: nb bundle puts a space before it, 49:0 ("Laster inn ...").
Followed for every progress string in this commit.
- cache -> "buffer" (bundle-established: Appbutikk-bufferen,
navnebuffer), not the "hurtiglager" core sometimes uses.
- schema -> "skjema", endpoint -> "endepunkt" (Danish kept "endpoint").
Harvest corrections (7 of 46 candidates were wrong in context, the
highest error rate of any locale so far):
- "Right" is a permissions-table header, not a direction -> Rettighet.
- "Revoke" arrived as core's "Avslå", which means REJECT. Revoking a
token is "Tilbakekall".
- "People" arrived as "Mennesker" (humans in the abstract); it labels
the PERSON entity type in EntitiesTab.vue:102 -> Personer.
- "Link" arrived as the noun "Lenke" but is a confirm button
(LinkObjectDialog.vue:62) -> "Knytt til". Kept the lenke/tilknytning
split so Link and Connection stay distinct concepts, since the app
ships both as separate features.
- "Dashboard not found" -> Instrumentpanel, matching core's own
translation of the Dashboard app name.
- "Unknown widget type" arrived as "modultype"; core's dashboard app
leaves widget untranslated, and "modul" is already used in this
bundle for application modules, so "widgettype" avoids the clash.
- "Assigned collaborative tags" lacked plural agreement -> Tildelte.
Accepted core over instinct once: "Bucket" -> "Bøtte", because core's
files_external is exactly the S3 domain this field belongs to. The
Danish bundle kept "Bucket" only because Danish core offers no entry.
Likewise "Host *" -> "Server *", core's rendering for this same field.
The {plural} source bug needed the same per-word handling as Danish, but
with Norwegian's own forms: objekt(er), fil(er), logg(er), skjema(er),
and register/registre where the stem changes.
42 identity strings stay absent rather than being written as value===key.
"Min ms" is included with a note: "min" also means "my" in Norwegian, but
beside a millisecond unit the Minimum reading is unambiguous.
Every string the frontend reaches via t()/n() now has a real Polish
translation. 987 new entries, 15 English placeholders replaced, 14
identity strings dropped so the runtime falls back to the source. No
pre-existing translation was altered.
FIRST nplurals=3 LOCALE. All six plural keys now carry three forms
matching the declared rule (n==1 / n%10 in 2-4 / rest), verified at
runtime: obiekt / obiekty / obiektów. Every locale before this was
nplurals=2, so this is the first bundle where a two-form array would
have silently mis-rendered the genitive plural.
Register: informal, measured against core (154 informal : 0 formal).
Core's imperative style is 2nd-person singular -- Wybierz, Zapisz,
Kliknij (334 hits) -- with impersonal "Nie można" for errors (172), and
the polite 3rd-person "Proszę wybrać" appears only 19 times. Followed
that: 2sg imperatives, impersonal error phrasing.
The register detector needed rebuilding for a homograph problem that is
worse here than in Scandinavian. Formal address is Pan/Pani/Państwo, but
lowercase "państwo" is the ordinary noun for STATE / COUNTRY -- a word
this app plausibly uses, since it manages government registers -- and it
capitalises at sentence start like any other noun. Państwo therefore
counts only MID sentence; Pan/Pani match anywhere, since as address they
stay capitalised. 15 controls (10 must-not-fire, 5 must-fire) pass, and
the detector reports 0 across all 5228 Polish strings in core.
Caught a FALSE FRIEND that the value===key rule would have hidden:
"Data" was initially filed as an identity string, but Polish "Data"
means DATE -- core itself translates "Date" -> "Data". Left untranslated,
the object's data tab would have read "Date" to a Polish user and
collided with core's own term. It is now "Dane". Audited the key across
all nine finished locales: it/pt/es correctly carry Dati/Dados/Datos,
and sv/da/nb correctly leave it as identity because their word for date
is dato/datum. Polish was the only locale affected.
Orthography follows Swedish, not Danish/Norwegian: the bundle
capitalises domain terms mid-sentence (Rejestr 35:2, Schemat 37:1,
Obiekt 79:3), so those stay capitalised through all case inflections
(Rejestru, Schemacie, Obiektów). But "organizacja" measures 0:18 and
stays lowercase -- a per-word exception, not a blanket rule.
Harvest corrections (5 of 47 candidates wrong in context, plus 1 dropped):
- "Right" -> Uprawnienie; core's "Do prawej" means "to the right".
- "View" -> Wyświetl; core's "Podgląd" is the NOUN preview, but this
is a verb button (OrganisationsIndex.vue:90).
- "Revoke" -> Unieważnij; core's "Cofnij" means UNDO, which is not
what revoking a token does.
- "Link" -> Powiąż; the harvest gave the noun "Łącze" for a confirm
button (LinkObjectDialog.vue:62).
- "Mappings" -> Mapowania, not the Polglish "Mappingi".
- "Status" dropped to identity: identical in Polish and already used
10x in the bundle; openconnector's "Stan" is reserved here for Health.
Accepted core where it is in-domain even when the literal reading is odd:
"Bucket" -> "Kosz" (files_external IS the S3 config UI), the same call
made for Norwegian's "Bøtte". Polish core renders Trash as "Usunięte
pliki", so there is no collision. "Host" stays untranslated per core.
The {plural} source bug degrades further here: with three plural forms a
parenthetical cannot cover the genitive, so Obiekt(y) / Rejestr(y) /
Schemat(y) / plik(i) / log(i) are a documented approximation. The real
fix is for the caller to use n() instead of interpolating a literal "s".
Every string the frontend reaches via t()/n() now has a real Czech
translation. 985 new entries, 16 English placeholders replaced, 13
identity strings dropped so the runtime falls back to the source. No
pre-existing translation was altered.
Register: FORMAL -- the first formal-target locale since French, so the
detector polarity flips back. Measured against core: 177 vy/váš hits,
and all 6 apparent informal hits are the plural DEMONSTRATIVE "ty"
meaning "those" ("pouze ty stávající" = "only those existing"), not the
2sg pronoun. So core is 177 : 0 genuinely informal.
The old detector was /(Tvůj|Tvoje|Tvá)/ -- three possessive forms and
nothing else. That missed where Czech formality actually lives: the
IMPERATIVE ENDING. Formal is 2nd-person PLURAL in -te (Vyberte, Zadejte,
Spravujte); informal is the bare 2sg stem (Vyber, Zadej, Spravuj). So
"Vyber registr" is informal address with no pronoun present at all --
the same blind spot Spanish had, in a different language family.
Rebuilt with the full possessive paradigm plus a curated bare-imperative
list. 22 controls pass and the detector reports 0 across all 5005 Czech
strings in core.
Bare "ty" is deliberately NOT matched. Unlike Polish Państwo, where a
mid-sentence-capital rule separates the two readings, Czech informal "ty"
and demonstrative "ty" are identical in case and position, so no rule can
tell them apart. The possessives and imperatives are unambiguous, so
nothing is actually lost -- documented in the detector comment.
Second nplurals=3 locale, but a DIFFERENT rule from Polish: Czech splits
1 / 2-4 / 5+ where Polish keys on n%10. Verified at runtime that all six
plural keys carry three forms matching the declared expression
(objekt / objekty / objektů).
The bundle's own style, followed throughout: formal -te imperatives for
instructions, plus INFINITIVES for button labels (Zobrazit, Smazat,
Obnovit, Filtrovat), which are register-neutral in Czech and must not be
"corrected" to imperatives. Impersonal "Nepodařilo se" for failures and
"Při ... došlo k chybě" for errors, both already established here.
Orthography follows Danish/Norwegian, not Swedish/Polish: domain terms
stay lowercase mid-sentence (registr 0:35, schéma 0:41, objekt 1:79).
Bundle consistency over core once: the bundle uses "notifikace" (3:0)
where core prefers "oznámení". Kept the bundle's term.
Established terms adopted: ścieżka -> "auditní záznam" (audit trail),
"schránka" (clipboard), "mezipaměť" (cache), "měkce smazané" (soft
deleted), and core's "Hostitel" for Host and "Profilový obrázek" for
Avatar.
Harvest corrections (2 of 45 candidates wrong in context):
- "Right" -> Právo; core's "Vpravo" means "to the right".
- "Link" -> Propojit; the harvest gave the noun "Odkaz" for a confirm
button (LinkObjectDialog.vue:62).
Also translated "Test" rather than treating it as identity: Czech buttons
take infinitives here, so the webhook action reads "Testovat", matching
"Testovat připojení" elsewhere in the bundle.
"Data" stays identity, and this was checked rather than assumed after the
Polish false friend: Czech "data" IS the word for data, because Czech
uses "datum" for date. The trap is Polish-specific.
Fills every string the frontend reaches through t()/n() with a real
Russian translation. 2011/2011 keys, 0 absent, 0 placeholders,
0 register violations, 0 plural-arity errors.
Register: formal, measured rather than assumed. Nextcloud core ru
carries 328 formal pronouns and 164 formal 2pl imperatives against
ZERO of either informal marker across 3905 strings -- the least
ambiguous reading of any locale so far.
Rebuilt the register detector, which previously covered only five
nominative possessives. Russian informal address hides in three
places a pronoun check misses:
* the oblique cases (тебя/тебе/тобой and declined твой), which is
most of what running prose actually uses;
* the imperative ending -- formal is 2pl -ите/-йте (Выберите),
informal the bare 2sg (Выбери), so "Выбери реестр" is informal
with no pronoun present at all. Same blind spot Czech had;
* the 2sg present -ешь/-ишь (хочешь, увидишь). Feminine soft-sign
nouns end in -чь/-щь/-ышь/-ушь (ночь, помощь, мышь, тушь), never
-ишь/-ешь, so the ending is unambiguous.
вы/вам/ваш are deliberately NOT matched: lowercase вы is the ordinary
polite address here, not a plural-only form, so it is evidence of
nothing. 32/32 controls pass, 0 hits on core.
First non-Latin-script locale, so the --latin script-coverage check
replaces the --hybrids check. It started at 24 hits; 16 were genuinely
untranslated English (the entire browser/VAPID web-push block, plus
Slug) and are now translated. The 11 that remain are reviewed-benign:
every word of prose is translated and the Latin run is a literal --
a file path, an API field name (conversationId, fileCollection), or a
product name (Zookeeper). Byte-majority cannot tell those apart from
an untranslated string.
Harvest review caught four core/sibling values that were wrong in
sense for this app's context and would have passed every automated
check:
* Right -> "По правому краю" (right-ALIGNED) where the key is a
permissions-table header. Now "Право".
* View -> "Режим просмотра" (view MODE) where the key is an action
button. Now "Просмотр".
* Open -> "Открытый" (the adjective, harvested from circles) where
the key is an action button. Now "Открыть".
* Search -> "Найти" (the verb) where the key is a tab/field label,
and the bundle already reads "Поиск / Представления". Now "Поиск".
Also corrected Link (noun -> "Привязать", it is a confirm button),
People ("Люди" -> "Персоны", it labels the PERSON entity type),
Mappings (dropped a sibling app's "(Mapping)" gloss), and the two
Dashboard strings to the bundle's own "Дашборд" rather than core's
"Панель управления" -- bundle-internal consistency outranks core.
Bucket keeps core's literal "Корзина" from files_external even though
Russian cloud docs prefer "бакет", applying the same in-domain-core-
wins rule used for Polish "Kosz" and Norwegian "Bøtte". The bundle has
no other Корзина string, so nothing collides.
One pre-existing mistranslation fixed: Test was "Тест" (the noun) on
what is a webhook test BUTTON; now "Проверить".
nplurals=3 with a third distinct rule -- Russian keys form 0 on
n%10==1 && n%100!=11, so the arrays were built against the ru
expression rather than copied from Polish or Czech. All 6 plural keys
carry 3 forms.
22 keys are left ABSENT as identity strings (ID, Id, ID:, URL, Url,
UUID:, CSV, PDF, RBAC, DSAR, Deck, Excel/OpenDocument format names,
API-key prefix hints, literal header/YAML examples). OC.L10N falls
back to the English source, which renders the same correct text --
writing them as value===key would be indistinguishable from an
untranslated placeholder and would never get revisited. Slug was NOT
treated this way: a lone Latin word in a Cyrillic UI reads as
untranslated, so it is "Слаг".
The five object{plural}-style keys remain parenthetical approximations
("объект(ы)"). Three-form agreement means a parenthetical cannot cover
the genitive; the real fix is for the caller to use n() instead of
interpolating a literal "s".
l10n/ru.json (backend catalogue) untouched.
Both were caught by cross-locale probes while resolving the same keys for Russian, and both survive every automated check because the values are real words that differ from their keys. nl: Right was "Rechts", which is the DIRECTION "right". The key is a column header in the organisation permissions table (src/modals/organisation/EditOrganisation.vue:288), so it means a permission. Now "Recht". Every other locale already had the noun (Recht / Droit / Derecho / Diritto / Direito / Rättighet / Rettighed / Rettighet / Uprawnienie / Právo / Право). cs: Uses and Used by were BOTH "Používá". Those are two separate tabs on the object view (outbound vs inbound relations, ViewObject.vue:254 and :290), so the pair rendered identically and the user could not tell which direction a tab showed. Used by is now "Používáno v". No other locale collides on this pair.
Brings the l10n/*.js toolchain into the repo: a shared library plus four
CLIs — l10n-ai.js (key CRUD), check-l10n.js (audit en.js against src/),
clean-l10n.js (remove unreferenced keys) and find-unwrapped.js (find
prose that was never wrapped in t()).
These existed untracked, and enter the repo with four defects fixed. All
four were the kind that stay invisible until they cost you data or a
review cycle.
n() was invisible to the usage scanner. collectUsedKeys and
findKeyReferences matched only `\bt\s*\(`, so every plural key came back
unreferenced despite live call sites. That armed clean-l10n.js: it
deletes en.js-minus-used from ALL 37 locale files, so adding the plural
source keys to en.js — which is correct and expected — would have made
the next --apply erase them everywhere, including populated plural
arrays. Demonstrated against a fixture before fixing. The three scripts
had three separate copies of the extractor; they now share one that
handles t(), n() (BOTH key arguments) and the $t/$n template variants,
while rejecting identifiers that merely end in t or n (format(, fn(,
min(). As a direct consequence `rm` now correctly refuses to delete a
key referenced only from an n() call.
serializeJs reformatted every file it touched. It emitted tabs,
"key": "value", a trailing comma and `)`, where the shipped files use
four spaces, `"key" : "value"`, no trailing comma and `);` — so a
one-key edit produced a ~4400-line diff. The key order was wrong too:
localeCompare matches ZERO of the 37 files, case-insensitive code-unit
order matches 36, and localeCompare varies by Node/ICU version, which
made the sort order depend on who ran the tool. Round-trip is now
byte-identical for 36/37 files; en.js is the lone outlier, still in the
original extraction order, and will re-sort once on first write.
The eslint pass was destroying the format it was meant to normalise.
Both writers ran `eslint --fix` on the locale files, and l10n/ is not
ignored — l10n/cs.js alone reports 9760 fixable "errors". The fix
rewrites the file to tabs and SINGLE quotes, undoing the serializer
immediately and diverging from what Transifex regenerates. Locale files
are generated data, not source code, so runEslintFix is gone.
find-unwrapped.js hung forever, which is why it could never be wired
up. A bare '<' in template text ("5 < 10") is rejected as a tag open and
falls through to the text branch, whose loop stops immediately because
it is already sitting on '<' — the region is empty, the index never
advances. Now completes over the full tree in ~0.1s. Two further fixes
there: the app id no longer defaults to the hardcoded literal
'opencatalogi' (a different app — a wrong app id makes every wrapped
string look unwrapped), and looksLikeProse no longer discards
"Creating..." / "Loading..." / "Saving...", which its dotted-identifier
filter matched via the trailing run of dots. That last one was
suppressing the single most commonly unwrapped class of label.
Verified: add/set/rm/remove round-trip byte-identically and produce
one-line diffs, `set` still refuses plural arrays, `rm` still blocks on
live references, and all five files are lint-clean.
… npm
Extends the parity gate with the two checks that would have caught this
translation effort's real failures, wires the tooling to npm, and
replaces the l10n guidance.
The parity gate's central assumption was backwards. Its comment read
"Values identical to English are allowed (cognates / proper nouns /
acronyms are legitimately the same) and only counted." But absent and
identical are OPPOSITES, not degrees of the same problem:
absent -> OC.L10N falls back to the English source, so the UI
renders correct text AND the gap stays visible to tooling,
keeping the key on the work list.
identical -> renders the same characters but is indistinguishable from
finished work, to tooling and to the next maintainer, so
it is never revisited. A permanent invisible hole.
Identical is therefore the worse of the two and is the one worth gating.
This is exactly how ru shipped 24 untranslated English strings behind an
otherwise clean report. Cognates now belong ABSENT rather than written
out; --allow-identical restores the old tolerance for a bulk migration.
Also added: plural arity against each locale's OWN declared nplurals.
That is the one l10n defect invisible to reading the file — OC.L10N
indexes the array with the plural expression's result, so a short array
renders blank for some counts. Note arity alone is not sufficient
protection: ru, pl and cs all declare nplurals=3 with three mutually
incompatible expressions, so a Polish array pasted into Czech has the
right length and the wrong boundaries.
npm wiring: test:l10n:parity, check:l10n, clean:l10n, find:unwrapped.
The last three were documented but had never existed as npm scripts, so
every command in the old guidance failed.
CLAUDE.md was an unedited copy from opencatalogi: the wrong app id
throughout (a wrong id fails lookup silently and renders untranslated
text with a green pipeline), three npm scripts that did not exist, and
it forbade touching l10n/*.json — the file the sanctioned extractor
actually writes. Rewritten with the verified commands, the two
translation sets described as the independent catalogues they are, and
the rules established across 12 completed locales.
One obsolete rule deliberately reversed: the old text said never to
narrow `add --locales` and never to defer a locale. Written for a
two-locale app, that now demands 37 hand-written values per string and
invites precisely the placeholder filler the value===key rule forbids.
`en` is required; the rest are optional and better left absent.
The per-language method — measuring formality register against Nextcloud
core instead of assuming it, why harvested values must be checked at the
call site, plural incompatibility, and the established per-locale
conventions — moved to docs/l10n-ui-translation.md so it is read on
demand rather than billed on every call.
readStringLiteral handled only \n, \t and \r; every other escape fell
through to `else value += n`, which drops the backslash and keeps the
letter. So the source literal
t('openregister', '⚠️ PERMANENT DELETION WARNING ...')
extracted as the key "u26A0uFE0F PERMANENT DELETION WARNING ...", while
at runtime JS produces "⚠️ PERMANENT DELETION WARNING ...". The two
never match, so any translation stored under the extracted key is dead
on arrival, and check-l10n reports the real key as missing forever.
The 12 finished locales happen to hold the correct emoji key, so nothing
shipped broken -- but the tooling could not see that, and reported those
two keys as untranslated in every locale.
Decode \uXXXX, \u{XXXXX} and \xXX, and add the remaining single-letter
escapes (\b, \f, \v, \0) so the extracted key is byte-identical to the
runtime key. Verified against seven literal forms, including the ⚠️ /•
case and a surrogate-pair \u{1F600}.
1985 of 2011 frontend keys now carry a real Ukrainian translation, up
from 1005. The remaining 26 are deliberate: acronyms and formats
identical in Ukrainian (ID, URL, UUID:, CSV, PDF, RBAC, IBANs), proper
names (Deck, Excel (.xlsx), OpenDocument (.ods)), literal placeholder
examples (sk-..., org-..., fw_..., myapp, example URLs, header/YAML
samples) and one pure-placeholder format string. They are left absent
rather than written out, so the runtime falls back to English and the
keys stay visibly untranslated.
Register: formal, measured against Nextcloud core rather than assumed.
Across core/lib/apps uk.json, 632 distinct strings carry formal markers
(ви/ваш, 2pl imperatives in -іть/-те) against 2 informal, and both of
those are deliberately casual content (a user-status prompt and a sample
calendar event) rather than UI chrome. Detector validated on 22
must-fire / must-not-fire controls; it finds 0 informal markers in the
finished uk.js. Matching core, ви/ваш is used for address and possession
and actions are infinitives (Додати, Зберегти, Переглянути).
Harvested 24 values from core/lib and bundled apps, ranked core first.
Three were correct translations of the wrong sense and were rewritten
after checking the call site:
People PERSON entity-type label, not "Users" -> Люди
View row action button, not display mode -> Переглянути
Bucket histogram score range, not basket/bin -> Діапазон
Right (permission column, not text alignment), Subject (GDPR data
subject, not mail subject) and Open (NcButton verb, not adjective) were
checked against the same trap list and translated in their real sense.
Plurals use Ukrainian nplurals=3 (1/21 | 2-4 | 5-20,0); all 6 plural
keys carry 3 forms. The object{plural} family follows the parenthetical
convention already used by ru and cs -- об'єкт(и) -- because the source
interpolates a literal "s" instead of calling n().
Verified: 0 value===key, 0 bad plural arity, 0 informal markers, no
Latin-only values, loads under OC.L10N.register with the correct
plural-forms header, and diffing against the previous file shows 988
keys added, 8 value===key cognates removed, 15 placeholders replaced,
and 0 existing real translations altered.
extractTCalls pushed BOTH arguments of an n() call into the used-key set, so every plural source string was compared against en.js as if it were a catalogue key of its own. It never is: an n() call has two source strings but one key -- the singular -- and the plural lives in that key's value array. The six plural sources were therefore permanently reported as missing, and no amount of correct translation could clear them. Track plural source -> singular key while extracting, and treat a plural source as satisfied when its singular key holds an array. check:l10n now reports 0 missing instead of 6 unfixable ones.
Six strings in two ternaries rendered English for every user despite five
of them already being translated in all 13 finished locales -- the
literal was simply never passed through t():
ViewObject.vue isCopied ? 'Copied' : 'Copy'
isSaving ? ('Creating...' | 'Saving...')
: ('Create' | 'Save')
Copy, Creating..., Saving..., Create and Save were all already in the
catalogue and translated; only Copied is new.
Also wraps "Mode:" and "Error Details" in MassValidateModal. Those two
mattered beyond the display bug: both keys existed in en.js with no t()
call, so they read as dead and were on the unused-key removal list.
Wrapping them keeps their translations rather than discarding work that
would be needed the moment the string was wrapped.
t() is available in these templates via main.js's
app.mixin({ methods: { t, n } }).
Static extraction cannot see a key passed to t() through a variable:
t('openregister', action) PermissionMatrix.vue:41
t('openregister', step.status) ApprovalStepList.vue:17
t('openregister', preset.label) DashboardIndex.vue:91,120,360
t('openregister', key) MainMenu.vue:76 (manifest labels)
All four are real, live keys. actions and the date presets are hardcoded
frontend arrays; step.status is a raw DB enum that the approval-steps API
returns verbatim, so the backend never localises it and the frontend must.
They therefore look unused. Once en.js was completed to cover them,
clean-l10n --apply would have deleted 17 keys from all 37 bundles,
silently un-translating the Permission Matrix headers, the approval
status badges, the dashboard date presets, and the "Data sources" and
"Endpoints" menu labels.
Adds DYNAMIC_KEYS + collectDynamicKeys() to lib/l10n.js, documenting
where each key comes from, and teaches both consumers to treat them as
used: check-l10n no longer reports them unused, clean-l10n never offers
them for removal. clean-l10n now prints the protected count so the
exclusion is visible rather than implicit.
en.js had drifted badly from src/: 1410 keys where the code uses ~2000,
so check:l10n reported 997 missing and 405 dead and neither number could
be trusted as a completeness signal for any locale.
en.js is now generated from the actual t()/n() call sites and is a strict
superset of every locale: 2018 keys. The six n() keys hold proper
[singular, plural] arrays instead of being absent, and the 15
variable-keyed strings were added here rather than deleted from the
locales, because they are genuinely used (see previous commit).
Dead keys removed from all 37 bundles (13,483 entries), all verified to
have no t() reference and no complete quoted literal in src/ -- obsolete
features: agents, conversations, collections, Solr/Zookeeper, memory
prediction. Also removed 240 keys that existed in a locale but not in
en.js at all, which is backwards -- a translation for a string the source
does not contain: nl's entity-type map keys (PERSON/EMAIL/... are JS
object keys in formatType, not translation keys), three PHP %1$s
notification strings that belong to the backend .json catalogue, two
pre-fix mangled ⚠ variants, and 226 obsolete SOLR/agent keys in tr.
The 13 finished locales (nl de fr es it pt sv da nb pl cs ru uk) are now
key-for-key identical to en.js -- 2018 keys each, 554 cognates written
out explicitly. This reverses the earlier "leave cognates absent" rule
for these locales only; the 23 unfinished ones keep their current shape
so their real progress stays measurable. Note the consequence:
test:l10n:parity now reports 22-70 English-identical per finished locale
where it previously reported them as missing. Those counts are the
cognate counts, not defects.
Wrong-sense and gap fixes found by cross-checking the finished locales
against each other. Most one-off absences are legitimate -- French really
does share Action/Configuration/Contacts with English, Dutch shares
Object/Complex, German shares Name/Status -- so each was judged per
language rather than by majority vote. The genuine errors:
Bucket WRONG SENSE everywhere. It labels a histogram score range,
but nb had "Bøtte", pl "Kosz" and ru "Корзина", all
"basket/pail". Now a range in all 13. These are the only
three pre-existing translations this commit overwrites.
Object A/B, Object #{id} absent in de and fr, which render Object as
Objekt/Objet, so these showed English.
Multitenancy, GitHub/GitLab Personal Access Token absent in de, fr.
Name *, Name* absent in fr, which has Name -> "Nom".
Facetable absent in es; log{plural} absent in es and pt.
Account absent in pt, sv. Endpoints absent in pt. Agents absent in nl.
Copied new key, translated for all 13.
Verified: en.js is a superset of all 36 locales, the 13 finished ones
have identical key sets, 0 bad plural arity anywhere, every bundle loads
under OC.L10N.register with plural arrays matching its own nplurals, and
diffing against HEAD shows 0 real translations altered beyond the three
Bucket fixes above.
el now carries a real Greek translation for every string that needs one:
1007 keys added and 15 English placeholders replaced, taking it from
1011 to 2018 keys -- key-for-key identical to en.js, matching the
full-sync shape of the other finished locales. The 26 remaining
English-identical values are deliberate cognates: acronyms and formats
Greek keeps as-is (CSV, PDF, RBAC, URL, UUID:, IBANs, DSAR, Slug,
Webhook, Email -- core Greek also renders Email as "Email"), proper
names (Deck, Excel (.xlsx), OpenDocument (.ods)) and literal placeholder
examples (sk-..., org-..., fw_..., myapp, sample URLs, header/YAML
snippets).
Register: formal, measured against Nextcloud core rather than assumed.
Core/lib alone gives 3 informal vs 135 formal distinct strings; with all
bundled apps, 8 vs 541. The 8 informal hits are demo content ("Γεια σου
κόσμε!"), the standalone "Εσύ" label, and one residual 3sg-past false
positive that itself contains formal σάς. So σας/εσείς for address, 2pl
imperatives in -τε (Επιλέξτε, Πατήστε, Εισάγετε) and 2pl present
(Μπορείτε, Έχετε).
Building that detector took three corrections, because the naive version
reported 532 informal vs 639 formal -- effectively noise:
σε read as the 2sg clitic, but it is overwhelmingly the
preposition "to/in" (351 hits). Dropped.
-εις / -άς matches plural NOUNS (ειδοποιήσεις) and genitive
singulars (γραμματοσειράς), not just 2sg verbs. Replaced
with a closed verb list.
-σε verbs the 2sg imperative is homographic with the 3sg past: "Ο
{actor} δημιούργησε" is "created", not "create!". Now
requires no sentence-initial 3rd-person subject -- and
that cue had to be anchored to the start, because mid-
sentence "το" is the neuter article ("πάτησε το κουμπί").
Validated on 19 must-fire / must-not-fire controls, including those
false-positive strings; the finished el.js has 0 informal markers.
Convention follows core Greek: actions are verbal nouns (Αποθήκευση,
Διαγραφή, Προσθήκη, Επεξεργασία), not imperatives. Glossary: Μητρώο
(register), Σχήμα, Αντικείμενο, Οργανισμός, Διακριτικό (token), Τελικό
σημείο (endpoint), Ιστορικό ελέγχου (audit trail), Όψη (facet),
Απόκρυψη (redaction), Εγγραφή αναφοράς (golden record).
Harvested 24 values from core/lib and bundled apps. Bucket was the
familiar wrong-sense trap -- files_external offers "Κάδος" (bin/basket)
where the string labels a histogram score range, so it became "Εύρος".
People correctly harvested as "Άτομα" here (unlike uk, where core gave
"Users"), and Right/Subject/Open were translated in their verified
senses: Δικαίωμα (permission, not direction), Υποκείμενο (GDPR data
subject, not mail subject), Άνοιγμα (action, not adjective).
Plurals use Greek nplurals=2; all 6 plural keys carry 2 forms. The
object{plural} family follows the parenthetical convention already used
by ru/cs/uk.
Verified: 0 missing, 0 empty, 0 bad plural arity, key set identical to
en.js, loads under OC.L10N.register, and diffing against HEAD shows 0
existing real translations altered or removed.
fi goes from 1011 to 2018 keys -- key-for-key identical to en.js -- with
1007 keys added and 15 English placeholders replaced. The 24 remaining
English-identical values are deliberate cognates: acronyms and formats
(CSV, PDF, RBAC, URL, UUID:, IBANs, DSAR, Slug, Webhook), proper names
(Deck, Excel (.xlsx), OpenDocument (.ods)) and literal placeholder
examples.
Register: 2sg (sinuttelu), and this is the first locale in this effort
that is NOT formal -- so it was worth measuring rather than carrying the
previous locales' answer over. Finnish does not map onto the Slavic/Greek
T-V pattern, and core is unambiguous: the formal pronoun te/teidän has
ZERO hits in core+lib, while sinä and the 2sg possessive -si are
pervasive ("salasanasi", "Kirjautumispolettisi"). core+lib scores 2sg 138
vs 2pl 5; with all bundled apps, 554 vs 31, and the 2pl residue is
false-positive: -kaa/-kää also forms A-infinitives and 3sg ("Haku alkaa"
= search begins), so that ending is not usable as a marker and a closed
verb list is used instead.
Because 2sg is correct here, the register detector is INVERTED relative
to el/uk/ru: it flags 2pl, not 2sg. Validated on 23 must-fire /
must-not-fire controls -- including the -kaa false positives and correct
2sg forms that must never be flagged -- and the finished fi.js has 0
formal-2pl markers.
Convention follows core Finnish: buttons and actions are 2sg imperatives
(Tallenna, Poista, Peruuta, Muokkaa, Luo, Lisää, Kopioi), the exact
opposite of Greek's verbal nouns. Error messages take the natural Finnish
nominal shape ("Asetusten tallentaminen ei onnistunut") rather than a
literal "Failed to ...". Token is "poletti", matching core's
"Kirjautumispolettisi".
Harvested 22 values from core/lib and bundled apps. Revoke needed
correcting: settings offers "Peru oikeus", which is permission-specific,
but the call site revokes an API token -> "Mitätöi". Bucket was absent
from the Finnish harvest entirely, so it was translated fresh as "Väli"
(range) rather than inheriting the basket/bin sense that was wrong in
nb/pl/ru. People correctly harvested as "Ihmiset" for the PERSON entity
type; Right/Subject/Open translated in their verified senses (Oikeus,
Rekisteröity, Avaa).
Plurals use Finnish nplurals=2; all 6 plural keys carry 2 forms.
Verified: 0 missing, 0 empty, 0 bad plural arity, key set identical to
en.js, loads under OC.L10N.register, and diffing against HEAD shows 0
existing real translations altered or removed.
These labels live in src/manifest.json and reach t() only through CnAppNav's `translate` prop (MainMenu.translate), so static extraction never saw them: Administration, Audit, Data quality, Documentation, Features & roadmap, Integration, Search / views. They were absent from en.js and from all 36 locale bundles, i.e. the top-level navigation groups have been rendering in English in every language. Also narrow collectDynamicKeys() to the manifest fields that are actually translated. It previously harvested every label/name/title in the manifest, which pulled in observability.metrics[].name (Prometheus metric identifiers such as objects_created_total) and pages[].title, which CnPageRenderer forwards to the page component as a raw prop without translating. Only menu[].label (recursed through children) and the two nav label overrides reach t(). en.js and the 16 completed bundles are now 2025 keys, key-for-key identical, with no extra keys in any locale.
The webhook headers placeholder is a code example, not UI copy, so it
does not belong in t(). Unwrapping it makes
"X-Custom-Header: value\nAuthorization: Bearer token" a dead key, so it
is dropped from en.js and all 36 locale bundles. It was value===key in
every one of them, so no translation is lost.
Also fix nl "file{plural}", which was left as "bestand{plural}". The
source interpolates plural: count !== 1 ? 's' : '', an English plural
marker, so that rendered "bestands"; Dutch is "bestanden". Its siblings
already sidestep this as "logboek(en)" / "object(en)", so this follows
them with "bestand(en)". nl "register{plural}" -> "registers" is left
alone because the English -s happens to be correct Dutch there, as it is
for es and pt throughout.
Note the underlying source defect this exposes: hardcoding 's' is only
correct for languages that pluralise with -s. Turkish (-lar/-ler) still
renders "dosyas"/"nesnes", and it cannot work at all for Finnish
partitives, Hungarian (no plural after a numeral) or the three-form
Slavic plurals. These call sites should use n() with a real plural key.
hu.js goes from 1011 to 2024 keys, key-for-key identical to en.js: 1013
added, 16 English placeholders replaced with real translations, and 23
deliberate cognates (Audit, CSV, PDF, RBAC, Id, Port, URL, format names,
and literal example values shown verbatim in inputs).
Register: Hungarian core is formal. The measurement is unambiguous —
core+lib+apps has 43 hits for Ön/Önnek/Önt against 3 for te, and every
instructional string uses the polite third-person imperative
("Kattintson", "Lépjen", "szerkessze"). Formal Hungarian also takes the
3sg possessive, so "your password" is "a jelszava", never "a jelszavad".
Validated with a closed-list detector (2sg pronouns, 2sg verb forms, 2sg
possessives) over 10 must-fire and 28 must-not-fire controls; suffix
matching is unusable here because word-final -d and -sz are also the
natural endings of kód/mód/rend/föld and húsz/ész/dísz.
Conventions follow core: buttons are -ás/-és verbal nouns (Mentés,
Törlés, Létrehozás, Hozzáadás), not imperatives; quotes are the low-high
pair; plural arrays carry the same form twice, since Hungarian does not
pluralise after a numeral.
Domain terms match the 1011 strings already in the bundle:
Nyilvántartás, Séma, Objektum, Forrás, Végpont, Szervezet, Ügynök. GDPR
wording uses Hungarian statutory terms (érintett, jogalap, megőrzési
idő, elszámoltathatóság, adathordozhatóság), and the Dutch source terms
are rendered the same way de/fi/el handle them, keeping Autoriteit
Persoonsgegevens as a glossed proper name.
One pre-existing value corrected: "Contacts" held "Kapcsolatok"
(= connections), but its only call site is RelationsTab.vue's entity-type
map for address-book contacts, and "Relations" already owns
"Kapcsolatok" — the two were indistinguishable in the UI. Core uses
"Névjegyek" (apps/dav/l10n/hu.json), which also matches the "névjegy"
wording used throughout the rest of the bundle.
Verified: key set identical to en.js, 0 empty values, 6 plural keys all
at nplurals=2, placeholders preserved in both directions, 0 informal
forms against 122 explicitly formal ones, 0 of the 1011 pre-existing
translations lost or altered apart from the documented Contacts fix, and
the bundle loads under OC.L10N.register.
Takes l10n/ga.js from 1053 to 2052 keys — full key-for-key parity with en.js
— and adds ga to FINISHED_DEFAULT, so the parity gate now holds it. 1000 new
values, 15 placeholders replaced, 1 pre-existing value corrected.
Register: the first locale with NO T-V DISTINCTION. Measured 440 second-person
singular markers against 0 plural across core's 33 ga catalogues (5395 values);
not one occurrence of sibh, sibhse, bhur, agaibh, daoibh, libh, oraibh, uaibh,
chugaibh or an -aigi/-igi 2pl imperative, confirmed by raw grep as well as by
the detector, and the bundle's own 1036 pre-existing values agree (15 vs 0).
sibh is strictly plural in modern Irish, so "informal" here names the only
address form available rather than a choice. It is recorded that way to set the
gate's polarity: patchcheck now refuses 2pl address, which for a single-user UI
is always a defect — the politeness plural imported by analogy from de/fr/nl.
Buttons: bare 2sg imperative, joining ca/et/hr/sl/sr. Measured from core's own
short labels (Sabhail, Scrios, Cealaigh, Cruthaigh, Deimhnigh, Coipeail,
Roghnaigh, Bain, Dun, Bog, Athnuaigh, Athchoirigh). The imperative is therefore
excluded from the detector, and ga is the first locale where BOTH runbook §6.5
tests agree: it is the label convention, and for the whole -ail class it is
spelled identically to the verbal noun, which is live in prose as the
progressive (Ag sabhail..., Ag coipeail sonrai..., Ag tastail...).
Plurals: nplurals=5, the largest in the set, and the library reaches only three
indices — measured over counts 0-120: 0 <- n==1, 1 <- n==2, 2 <- 0 and every
n>=3. Forms 3 and 4 are dead. Unlike rm that collapse is harmless, and it is
measured rather than assumed: across core ga's 101 fully-translated five-form
arrays, form2 != form3 in ZERO cases and form3 != form4 in ZERO. What decides
the arrays is the counted-noun rule, not the form index — Irish takes the
SINGULAR after a numeral, so a value carrying {count} needs the same counted
singular in all five forms while a value with no numeral needs a real split.
Core separates cleanly on that axis (53 of 73 numeral-bearing arrays keep the
singular; 28 of 28 without a numeral pluralise), so 5 of 7 arrays here are
legitimately all-forms-identical. No initial mutation after a digit, which is
also core's practice (%n comhad, never chomhad); a fixed numeral in a label
does take it (Last 3 months -> 3 mhi anuas).
The five {plural} keys drop the placeholder for a bare counted singular rather
than the parenthetical the bundle's own (s) siblings use, because every call
site renders the label beside a numeral and Irish takes the singular there —
grammar outranking house style, recorded in pluralHackNote.
Orthography: a third outcome for §8.10. ga has no independent capitalisation
convention — casing MIRRORS THE ENGLISH SOURCE. Where the key is title-cased the
value capitalises 76:1; where the key is prose, 0:193. A naive per-term count
reads MIXED for every term and would have looked like carelessness.
Tooling: detectors/ga.js with 73 must-fire/must-not-fire controls, all passing,
built from closed word lists. Three suffix rules are documented as unusable:
-igi is also the plural of every -ig noun (oifig -> oifigi, absent from the
6431-value corpus, which is why a suffix rule would have shipped); -ibh collides
with third-person diobh "of them", one letter from 2pl dibh and the reading that
actually occurs here; and `do` is at once the 2sg possessive, the preposition,
the past particle and half of `le do thoil`, in 527 of 6431 values — excluded
wholesale, the largest recall loss in any detector here, and on the correct-
register side so it can never produce a false formal hit.
Four deliberate divergences from core, all recorded: Webhook stays the English
loan (28 pre-existing uses) against core's Crucai greasain; Flow is sruth and
Workflow sreabhadh oibre, where core's Sreabhadh would collapse two concepts;
Update is the imperative Nuashonraigh, not core's verbal noun Nuashonru; and
Revoke is Culghair, not core's Chulghairm, which carries a stray lenition.
Wrong-sense harvests caught at the call site: Bucket is a histogram bin
(QualityIndex.vue), so Banda, never core's Buicead; Labels is the file-tag
column (UploadFiles.vue) so Clibeanna, while singular Label is a facet range
caption so Lipead — the §8.4 Label/Labels split, acted on for the first time;
Right is the RBAC <th> so Ceart, not Deas; Apply is Cuir i bhFeidhm, not core's
Cuir iarratas isteach, which is a job application.
14 cognates recorded with justifications. One pre-existing defect corrected:
Loading organisations... was the only one of 35 Loading X... values not built as
Ag <verbal noun> <object>..., and carried the wrong ellipsis glyph.
Verified: selfcheck 15/15, runtime all-pass, gate negative test confirms the
parity gate genuinely fails and names ga when a key is removed, and all 13
recorded locales regression-checked (selfcheck + detector controls + runtime).
check:specs, test:l10n:parity and format are green. test:l10n is RED and was
ALREADY RED AT HEAD, independently of l10n work: the development merge
28d24aa de-Dutchified the AVG source strings and added flow strings, leaving
17 keys used in src/ but missing from en.js. Reproduced in a clean worktree at
HEAD; neither en.js nor src/ is touched by this commit. The fix is a rename for
11 of the 17 plus three new keys, and it belongs in its own commit — recorded
with the procedure in docs/l10n-workflow.md §10.
29 of 36 locales now complete. Seven remain: mt is lb sq mk be bs.
Takes l10n/mt.js from 1052 to 2052 keys — full key-for-key parity with en.js —
and adds mt to FINISHED_DEFAULT, so the parity gate now holds it. 1000 new
values, 15 placeholders replaced, 9 pre-existing values corrected.
Register: core ships ZERO mt catalogues, so this is the §6.4 fallback — the
second locale after rm with no core evidence at all. The fallback was WIDENED to
the sibling apps' frontend mt.js, which tripled the corpus from 1015 values to
3422 and turned a thin 26-vs-0 into 128 vs 0. Two constraints made that sound:
only .js bundles, because the backend .json is a separate catalogue with a
separate consumer (§1) and openregister's own mt.json contains an `int` that
would otherwise have been miscredited to the frontend; and the sibling scan
excludes byte-identical mislabelled catalogues as harvest.js does.
mt IS NOT THE ga CASE, and that is the finding most worth carrying forward.
Irish has no T-V distinction, so its "informal" names the only address form.
Maltese genuinely HAS the politeness options — `intom` serves as a polite
singular like French vous, and Is-Sinjur/Is-Sinjura with third-person agreement
is the deferential register — and both are measured ABSENT. So mt's `informal`
is an ordinary measured choice like nl's, and its gate catches real deference
rather than an impossible form. Two consecutive locales measuring 2sg-only is
not evidence they are the same case.
Markers split 75 tieghek, 35 `jekk joghgbok`, 15 int/inti, 4 prepositional. That
middle figure is transferable: Maltese's politeness formula carries a 2sg OBJECT
SUFFIX, making it a real address marker rather than a courtesy word, and at 35
uses it was the second commonest marker in the bundle. The detector's first
draft omitted it and a must-fire control caught the omission.
Two probe bugs of my own are recorded because both would have shipped a wrong
number: the first register probe omitted the /i flag and scored ZERO for the
pronoun, missing three sentence-initial `Inti cert li trid...` values; and the
sweep's placeholder-stripping inserts spaces, which made a doubled-space check
report 195 defects that were entirely its own artefact. A measurement that comes
out at exactly zero deserves a second look.
Plurals: nplurals=4, and the header and library AGREE EXACTLY — verified
index-by-index over counts 0-130, all four forms reachable. First low-resource
locale here with no plural surprise: no rotation as in lv, no collapsed form
count as in rm/ga/tr. The arrays are still the hardest in the set, because
Maltese counting is SEMITIC: the noun is plural only after 2-10 (and 0), and
singular after 1, after 11-19 (hdax-il ktieb) and after 20+ (ghoxrin ktieb). So
forms 0, 2 and 3 normally carry the same string and only form 1 differs. §2.3
had flagged this bundle's one pre-existing array as "suspect, forms 2 and 3 fall
back to the singular"; verification CLEARED it and it is left untouched. The
harvest corroborated the rule independently — Last 7 days -> L-ahhar 7 ijiem
(plural) against Last 30 days -> L-ahhar 30 jum (singular). One further trap:
the whole predicate agrees, not just the noun.
Tooling: detectors/mt.js with 68 controls, all passing, from closed word lists.
Four suffix rules documented as unusable: the t- prefix is the 2sg imperfect AND
the 3sg FEMININE imperfect across the whole verb system (tista' is both, 23
occurrences split both ways, plus 24 of trid); -u is the 2pl imperative AND the
3pl of everything (nstabu occurs 24 times in "Ma nstabu l-ebda X"); -t is both
2sg and 1sg perfect, differing only by an internal vowel; and -kom/-ek are
ordinary word endings, so both prepositional-pronoun paradigms are enumerated.
Both losses fall on the CORRECT-register side, so they thin the evidence without
ever producing a false formal hit.
Loans: this bundle keeps English IT vocabulary consistently, and the siblings
disagree on two — they render Repository as Rezpozitorju and Logs as Registri,
but the file writes Repository and `Path fir-repository` and keeps Logs in four
independent places, so the file wins on lexicon (§3.5). Registri is unavailable
for Logs in any case: it is already this bundle's plural of Register.
43 cognates recorded with justifications, including the _{count} email_ plural
array, whose every form renders the English source because Maltese keeps
email/emails as loans — the same shape as Romanian's documented case, needing
the record for runtime-check while hasIdenticalForm keeps selfcheck from calling
it stale.
Nine pre-existing defects corrected, in three classes. SIX were the same one:
values whose English key says AUDIT but whose Maltese said verifika
(verification) — a different concept in this app, on the same screen (Verifika
tal-katina). 24 values use awditjar for audit against those 6. A seventh, `Audit
trail #{id}`, had been left as untranslated English merely re-cased — the same
key that was the outlier in sr. An eighth, `Log integrity`, read "integrity of
the REGISTER" on a screen listing Registri. And `Loading organisations...` was
the 1-of-19 outlier on both its verb and its ellipsis glyph — the same key that
was the 1-of-35 outlier in ga, on the same two counts.
Capitalisation is a partial per-term list (the sr/rm shape): four families
capitalised mid-sentence, the rest lowercase, and Registru is capitalised while
skema is not though they are the app's paired core concepts. THIS PASS BROKE IT
AND THE CHECK CAUGHT IT — measuring the pre-existing half separately from the
newly written half showed HEAD at Fajl/Fajls 43:0 while the new values had
lowercased it 24:4. 21 values were normalised via --allow-replace; no
corrections entry, because those keys are absent at HEAD (§6.3). Whole-bundle
counts read CAPITALISED either way, the pre-existing majority outvoting the
drift, so splitting the corpus at HEAD is what found it.
One inconsistency recorded rather than changed: entry/entries renders as
entrata/entrati 19 times and annotazzjoni/annotazzjonijiet 18 times for the same
rows, split by screen. New keys follow the adjacent existing value per screen.
Verified: selfcheck 16/16, runtime all-pass, gate negative test confirms the
parity gate fails and names mt when a key is removed, and all 14 recorded
locales regression-checked.
check:specs, test:l10n:parity and format are green. test:l10n remains RED and
was ALREADY RED AT HEAD, independently of l10n work — the development merge
28d24aa left 17 keys used in src/ but missing from en.js; neither en.js nor
src/ is touched here. Procedure in docs/l10n-workflow.md §10.
30 of 36 locales now complete. Six remain: is lb sq mk be bs.
runtime-check.mjs treated every header/library plural disagreement as the `lv`
case — a rotation of the same categories — and told the reader to "order the
ARRAYS by the library" and record pluralOrder:"library". That advice is wrong
for a whole second class of disagreement, and both remaining flagged locales
turn out to be in it.
The check now classifies. A disagreement is a PERMUTATION when each library
index maps to exactly one header index across counts 0..200 and the mapping is
injective — precisely the condition under which permuting the arrays can satisfy
the header everywhere. Otherwise it is a BOUNDARY disagreement, where the two
partition the counts differently and no reordering exists that reconciles them.
Measured, over counts 0..200:
is 17 disagreements (21, 31, 41 ... 191). Header is correct CLDR Icelandic
(n%10!=1 || n%100==11), so 21 takes the singular — "21 hlutur". The
library files `is` under its coarse `number === 1 ? 0 : 1` group, so form
0 is reachable ONLY at n=1 and those 17 counts render the plural.
mk 2 disagreements (11, 111), in the OPPOSITE direction. The library does
implement the modular rule but drops the n%100!=11 guard
(`number % 10 === 1 ? 0 : 1`), so it selects the SINGULAR where Macedonian
takes the plural.
lb sq 0 — plain `n != 1` headers, in the library's `n === 1` group.
bs be 0 — three-form Slavic, library matches exactly.
So the shape to check for is a two-form header whose expression is MODULAR
rather than `n != 1`, because the library's coarse groups are mostly written
`n === 1`. §2.3 had flagged is and mk to check; both are confirmed, and neither
is what §6.7 assumed.
pluralBoundary:"library" is the acknowledgement for the second class, and it is
deliberately a separate field rather than a second meaning for pluralOrder: the
two assert different things. pluralOrder says the arrays were reordered and the
locale is now fully correct. pluralBoundary says some counts are knowingly
wrong and pluralNote says which. Collapsing them would let a locale claim the
stronger property while having the weaker one.
Added to loadLocaleConfig's whitelist in the same commit that starts reading it
— §4.4 records that a new functional field is otherwise silently dropped, which
already bit once with pluralOrder itself.
The permutation branch is now a real assertion rather than a bare console NOTE,
so an acknowledged locale is counted in the pass tally instead of skipping the
check.
Verified across all 15 recorded locales per §8.9 (a new assertion is not
trustworthy until it has run against every finished locale): lv still classifies
as a permutation and passes on its existing record; tr/rm/ga keep the
fewer-forms NOTE; ca et hr lt ro sk sl bg sr mt still agree exactly; every one
still reports ALL RUNTIME CHECKS PASS. `is` now fails with the boundary message
and the actionable remedy, which is the pass that follows.
CI: check:specs PASS, test:l10n:parity OK, format clean. test:l10n remains red
at 17 missing keys — pre-existing per §10, and neither en.js nor src/ is touched
here.
1053 -> 2052 keys, full parity with en.js. 31 of 36 locales now finished;
lb sq mk be bs remain, in that order.
REGISTER: informal, 626 informal (2sg) markers against ZERO formal, over core's
28 is catalogues (3610 values) plus this bundle's own 1054 pre-existing values,
which add 0 formal. Because that is a zero it was re-checked by raw grep across
the 28 catalogues rather than trusted from the detector alone (§8.3): yður,
yðar, yðvar, yðr, þið, ykkur, ykkar, þéra and þérun are all literally absent,
nine tokens at zero.
This is a THIRD kind of "informal" and must not be collapsed with the two
locales before it. ga: Irish never had a T-V distinction, so the label names the
only address form. mt: Maltese has one and it is current, simply unused. is:
Icelandic HAD one (þér + 2pl verb, possessive yðar) and abandoned it in the 20th
century, so its V-forms are archaic rather than absent or merely unfashionable.
That changes what a slip looks like: the realistic error here is not yðar at all
but the plain modern plural þið/ykkar, because a translator importing a
politeness plural from de/fr/nl reaches for the form that is actually current.
detectors/is.js gates on both and keeps them distinct.
BUTTONS: infinitive, register-neutral, joining cs/lt/lv/sk/rm. 29 of 35 distinct
core values over ~35 bare action keys (Vista, Eyða, Breyta, Afrita, Staðfesta,
Endurheimta, Loka, Búa til, Bæta við, Hætta við ...), zero verbal nouns, exactly
one enclitic imperative (Choose = Veldu against Select = Velja, so an outlier).
The bundle's own 21 action keys agree unanimously.
DETECTOR: 84 controls, all passing — the largest set in the project. Two things
in it are reusable:
· THE BIGRAM. þér is at once the 2sg DATIVE ("þér er ekki heimilt") and the
archaic polite NOMINATIVE, and all 54 core occurrences are the dative,
verified at their call sites. hafið is at once "the ocean", the past
participle of hefja, and a 2pl verb. Neither word is decidable alone — but
"þér hafið" can only be the V-form, since a dative þér takes no finite 2pl
verb and "the ocean" does not follow a pronoun. So bare þér counts as
informal and the PAIR counts as formal, matched in both orders because a
question inverts it. Two ambiguous tokens, jointly unambiguous.
· THE CONJUGATION-CLASS SPLIT, the second PARTIALLY-detectable locale after bg
and the first where the split follows from a rule rather than a word list.
Icelandic fuses the pronoun onto the imperative (nota -> notaðu), so the
imperative IS an address marker. Class 1 verbs take -uðu in the 3pl past
against -aðu in the imperative, so they are distinct and usable; class 2
verbs build the 3pl past IDENTICALLY to the imperative, so settu is both
"enter!" and "they put". The collision is attested rather than hypothetical:
komu occurs 4x as a 3pl past ("Það komu of margar beiðnir") and völdu twice
as the weak adjective *selected*, never as imperatives. staðfestu is trebly
ambiguous, being also the oblique of the noun "confirmation".
is is also the first locale where §6.5 test 1 comes out NO — labels are
infinitives, so counting the imperative does not flag every button. Both tests
are genuinely independent and both had to be run.
No -ið suffix rule exists: -ið is the neuter DEFINITE ARTICLE as well as the
2pl ending (lykilorðið, skjalið, safnið, nafnið), the bg -те situation and
slightly worse, since five 2pl forms are themselves homographs of common words
and two of those five occur in core in the non-verb reading while NONE occurs
as a verb. Icelandic "please" (vinsamlegast) is an adverb and inflects for
nothing, so unlike mt there is no politeness-formula signal — checked, per
§8.1, rather than assumed.
PLURALS: header nplurals=2, plural=(n%10!=1 || n%100==11) — modular, and it
disagrees with the library. This is a BOUNDARY disagreement, not an ordering
one, so pluralBoundary:"library" is recorded and pluralOrder deliberately is
not; there is nothing to reorder. Measured over 0..200: 17 counts disagree, all
of the form n≡1 (mod 10) with n>=21, and at each the library renders the PLURAL
where correct CLDR Icelandic takes the singular (21 hlutur, not 21 hlutir).
Those 17 counts stay wrong and pluralNote lists them. Accepted rather than
worked around, and the reasoning is the MIRROR of rm's: form 1 is correct for 0
and 2-20, the overwhelming majority of counts a user meets, so contorting it
into a number-neutral shape would trade 17 wrong counts for ~180 unidiomatic
ones. rm needed that contortion because its collapsed form was wrong at EVERY
count but 1. The tooling change in the preceding commit is what tells the two
apart.
The 6 new arrays follow real case governance rather than swapping a noun: eyða
takes the dative ("Eyða {count} hlut" / "hlutum"), fjarlægja and endurheimta the
accusative ("{count} hlut" / "hluti"). The one pre-existing array was already
correct and agrees its whole predicate (færsla hefur / færslur hafa); it is the
model and is untouched.
The five {plural} source-hack keys need THREE shapes, chosen per noun, because
the house parenthetical only works where the plural is stem+r: skrá(r) and
gagnaskrá(r) keep it, while hlutur/hlutir, annáll/annálar and skema/skemu take
the slash. Grammar outranks house style here (§7.4) — the pre-existing
schema(s) -> skema(r) is exactly the non-word this avoids, since the plural of
skema is skemu.
CAPITALISATION: none at all, a FOURTH outcome for §8.10 and the one that
genuinely has no term list. Every term is one-sided lowercase mid-sentence, and
— this is what distinguishes it from ga rather than merely resembling it —
conditioning on the English key's casing changes nothing (skema 0:9 under
title-cased keys, 0:14 under prose; skrá 0:5 and 0:9). ga mirrors the source;
is is flat lowercase. They need opposite handling for every title-cased key.
63 AUDITED CORRECTIONS to pre-existing values, in four classes, each recorded
individually in locales/is.json:
1. REGISTER TERMINOLOGY, 34 values. Icelandic skrá means both "a file" and
"a register", and the bundle used it for both, so four pairs of distinct
English keys rendered BYTE-IDENTICALLY (All registers / All Files -> "Allar
skrár"; Register / File; Registers / Files; No registers found / No files
found) and one value was unreadable: "No register objects reference this
file" -> "Engin skrárhlutar vísa í þessa skrá", both senses in one sentence
as the same word. Core is locks skrá = file (Skrá, Skrár, Skráaforrit) and
ships no Register/Record/List key at all, so register is the side that had
to move. THE OWNER DECIDED to adopt gagnaskrá — asked explicitly per §6.4
step 2, because this is the app's primary noun and the choice propagated
through ~76 keys. Not a coinage: the bundle already used gagnaskrá in
exactly this sense in one value, so this is the §3.5 rule.
2. THE sía GENDER ERROR, 21 values. The bundle declined sía ("filter") as
MASCULINE — Virkir síar, Ítarlegir síar, Tiltækir síar, Engir virkir síar,
Hreinsa alla síar. Icelandic sía is FEMININE with nom/acc plural síur, and
this is confirmed by core rather than by my own grammar: core is renders
the key Filters as "Síur" and contains the exact phrase "Virkar síur",
against this bundle's "Virkir síar"; core's only sí- forms anywhere are
síur (3) and síu (3), never síar. Noun and every adjective corrected. The 5
pre-existing dative-plural síum values were already right and are untouched.
3. THE TWO REMAINING COLLISIONS, 6 values, both settled by core with no ask
needed. Slug shared Auðkenni with ID while both are field labels on the
SAME EditSchemaProperty screen -> Stuttheiti (the shape core lt takes with
Trumpinys). Refresh shared Uppfæra with Update -> Endurnýja, which is
core's own word for it. Configurations/Settings both stay Stillingar: the
§8.5 unavoidable-collision case, correct in each and core's word for
Settings.
4. FOUR INCIDENTAL DEFECTS, each fixed inside a value already being touched
for another reason: eyðdi -> eyddi (past of eyða), eyðingardagsetning ->
accusative eyðingardagsetningu after eftir, vefkrókurskráningarfærslur ->
vefkrókaskráningarfærslur (a compound takes the genitive plural vefkróka-,
not the nominative singular vefkrókur-), and "Skrá ekki fundin" ->
"Gagnaskrá fannst ekki", core's construction. Also normalised were the
invalid genitive skráar (the genitive of skrá is skrár) in three keys.
NOT ATTEMPTED, and stated so the bundle is not read as clean: a full grammatical
audit of the pre-existing half. Isolated agreement slips outside those four
classes are left under §3.8 — "Sérsniðin API endapunktur" wants Sérsniðinn for
the masculine noun, and the pre-existing schema(s) -> skema(r) carries a
non-word. Both are recorded in locales/is.json. The line drawn was: fix a
systematic error with a core-confirmed model, or a collision, and otherwise
leave a real translation alone.
TWO THINGS I GOT WRONG AND CAUGHT MYSELF:
· I introduced declined forms of a word this bundle treats as indeclinable.
skema appears 25 times bare at HEAD against a single declined skemanu, and
my new values had skemað 5 times plus skemans once — defensible Icelandic,
inconsistent with the file, which §3.5 settles against. Normalised. Nothing
else catches this: the values are not empty, not identical, not wrong-arity,
and each reads correctly alone. It is the §8.10 split-at-HEAD check applied
to DECLENSION rather than casing, and it is now written into the runbook.
· The first run of that check hid the finding, because I wrote skema\w* and
\w is ASCII-only, so it silently truncated skemað to skema. Same family as
the ga/mt \b lesson. Use \p{L}.
Sweep otherwise clean. The 39 remaining flags were all verified false positives
of my own heuristics: "settings" inside the literal path lib/settings/config.json,
32 product names and identifiers (Nextcloud app names, CSV, RBAC, pgvector, BCP
47, GraphQL, filecollection ...), and 4 hits on the {register} PLACEHOLDER plus
KvK fyrirtækjaskrá, which is an external Dutch company register rather than one
of the app's Register objects and so correctly does not take gagnaskrá.
Also corrected: "Loading organisations..." was the 1-of-23 outlier on BOTH its
construction (Hleð inn + dative, where all 22 siblings are Hleð + accusative)
and its ellipsis glyph (… where its English source has three dots — the only
glyph mismatch in 45 values). That is the same key that was the outlier in ga
and in mt, on the same two counts, which now makes it three locales running.
Verified: selfcheck ALL CHECKS PASS (2052 keys, 8 recorded cognates, 63 audited
corrections), runtime ALL RUNTIME CHECKS PASS including the acknowledged
boundary, gate negative test passes all four of its assertions, and all 15
recorded locales still pass selfcheck + detector controls + runtime after the
shared-tooling change. is.js is 2058 lines, exactly matching en.js, and 32 files
now share that count.
CI: check:specs PASS, test:l10n:parity OK (31 finished, 15 cognate-enforced, 5
in progress), format clean. test:l10n remains RED at 17 missing keys —
pre-existing per §10, unchanged in count, and neither en.js nor src/ is touched
by this commit.
The is pass scope-limited itself to "fix a systematic error with a
core-confirmed model, or a collision, and otherwise leave a real translation
alone", citing §3.8. The owner rejected that line: existing translations need
reviewing and fixing if they are bad. They were bad.
Reading all 1052 pre-existing values found 235 further defects, on top of the 63
already corrected. NONE of them is visible to any gate — a wrongly-inflected or
garbled value is not empty, not identical to English, not wrong-arity, and reads
as finished work to anyone skimming the file.
METHOD, and the honest split: mechanical checks (quantifier/adjective agreement
against a per-noun gender lexicon, postposed possessive and participle
agreement, case governance) found **4** of the 239. The other 235 needed reading
every value. Worth stating plainly, because the checks feel like the work and
are not: what they cannot see — a malformed compound, a nonsense word, a foreign
stem, a right word in the wrong sense — was the overwhelming bulk of it.
THE MECHANICAL CHECKS MISLED ME TWICE, both now written into §6.9:
· The agreement checker's first run produced 30-odd false positives and buried
the four real findings, because in the Icelandic strong declension the
NEUTER PLURAL nominative is homographous with the FEMININE SINGULAR. `Öll
skemu`, `Engin mál` and `Möguleg tvítök` are all correct. Each adjective
form has to license a SET of gender/number pairs, not one. It also had to
stop looking across punctuation ("Get ekki eytt: hlutir eru enn tengdir" is
two clauses) and skip a participle used as a supine after a modal
("Notendur geta valið tilkynningar" is not agreement).
· Case-governance checking is worthless for feminine and neuter singulars,
which are syncretic for nominative/accusative/dative in most Icelandic
declensions — `bæta við skrá` IS the dative. All 24 hits from the first
attempt were false. Restricted to masculine singulars and plurals, where the
forms differ, it drops to zero false positives. Phrasal verbs need excluding
too: the `til` in `búa til` ("create") governs nothing.
WHAT WAS WRONG, by class (codes recorded per key in locales/is.json, documented
in its auditNote):
41 HYPHEN acronym/product name juxtaposed to a noun without the
compounding hyphen Icelandic requires — API lykill,
LLM stilling, HTTP aðferð, Nextcloud hópa, n8n stillingu,
Ollama vefslóð, AI umboðsmann, API kalli
33 COMPOUND malformed compounds: Hámarks framkvæmdatími ->
Hámarksframkvæmdartími (9 of these), Gagnagrunnupplýsingar ->
Gagnagrunnsupplýsingar, útdrátturs -> útdráttar (invalid
genitive), Yfirlitvilla -> Yfirlitsvilla, Uppfærslaaðgerðir ->
Uppfærsluaðgerðir, vefkrókarskráningar -> vefkrókaannálar,
Heildartala stærð -> Heildarstærð, Heildar -> Heildartölur
23 SENSE a real word carrying the wrong meaning: Fylgni (correlation)
for Compliance, Almenningur (the public, a noun) for Public,
Einkamál (a private affair) for Private, Úrræðastími (remedy
time) for Timeout, Veldisvísir (exponent) for Exponential,
Meðaltímaframkvæmd (a backwards compound) for Avg Execution
Time, notandaumboðsmaður (a user's human agent) for the
HTTP User-Agent, Hlýna (to become warm) for Warmup
20 TERM-ORG `stofnun` (an institution) where this bundle's word for
organisation is `skipulagsheild`, which outnumbered it 41:22
19 CASE wrong case after a governing verb or preposition: Frá
dagsetning -> dagsetningu, Til dagsetning -> dagsetningar,
Fjarlægja sía -> síu, Vefkrókur eytt -> Vefkrók eytt,
Útflutningur lokið -> Útflutningi lokið, Pakka farmur ->
farmi, Leita yfirlit -> Leita að yfirlitum
14 GARBLED-OR-FOREIGN see below
13 NUMBER singular where the English is plural, mostly `ferlatal` for
`ferlatöl` across 11 audit/search-trail keys
11 TERM-LOG `skráning` where this bundle's word for log is `annáll`
(Log integrity was already `Heilleiki annáls`)
11 AGREEMENT gender/number disagreement: ekkert titill -> enginn titill,
Allar flokkar -> Allir flokkar, Öll vefkrókur -> Allir
vefkrókar, Engar tengsl -> Engin tengsl, eyddar hlutir þínar
-> eydda hluti þína, Gögn ... afritaðar -> afrituð, Engin
hreinsunardag stillt -> Enginn hreinsunardagur stilltur
9 AGREEMENT-OR-FORM Endurtaki -> Endurtek (wrong 1sg), Helstu eyðarar ->
eyðendur (agent noun), Tölutakmörkunar -> Tölutakmarkanir
(genitive singular for a nominative plural)
7 TERM-CONFIDENCE `öryggi` (security — already this bundle's word for
Security) for Confidence -> `vissa`
6 TERM-AUDIT `úttekt` where the bundle uses `endurskoðun` in ~40 keys
5 TYPO-FERSLA `fersla`, which is not an Icelandic word — it is `færsla`
4 TERM-TOKEN `tóken`, an English loan, where the bundle uses `teikn`
3 TYPO `vektor` missing its ó
1 TERM-FILTER `Filters` -> `Síar`, which the first round MISSED — see below
THE THINGS THAT WERE NOT WORDS AT ALL, since they are the clearest evidence the
audit was warranted: `Stav` (Slavic for "status" — a straight wrong-language
contamination of the kind §6.6 warns about in harvest sources, here sitting in
the committed bundle), `skrivaðgang` (a Danish/Norwegian stem for
`skrifaðgang`), `Búningaaðgerðir` for *Create Operations* (`búningur` is a
costume), `vefkróklevranir`, `búsum`, `tökmörkun`, `kerfisssstillingum`,
`Misheppnaðst`, `Grunvefslóð`, `Mynsturnvandamál`, `Leynigerðarlykill`,
`Bútar til að vektórafræðilegar`, `leitarferlatasskráningar`, `greinunargæðum`,
`Nafnlægt`, `fylgstu gildi`, `Nauðsynlegur staðs`.
TWO OF MY OWN MISTAKES:
· I MISSED `Filters` -> `Síar` in the first round's sweep, because I wrote the
regex without the /i flag and the key is capitalised. That is the same
lesson §8.3 already records from the mt pass, and I repeated it in the same
session. The audit caught it.
· I PROPAGATED a pre-existing typo into my own values: `endurskoðunarferslur`
for `endurskoðunarfærslur`, because the corrections patch preserved the
surrounding wording of values I was touching for another reason. When
reusing a pre-existing phrase, verify the phrase — do not assume the half
you are not changing is correct.
Also corrected on re-reading the FULL English of two keys whose truncated
display I had translated from: "Higher values capture more detail but increase
vector size" (I had written "increase vectorization cost") and "Save anyway?" (I
had written "Saving does not re-validate them"). Both were my own errors from
this session, introduced by working off a truncated column.
corrections in locales/is.json now holds 291 entries. At this volume per-key
prose stops being readable, so the 228 new ones carry short CLASS CODES with the
codes documented once in auditNote — the 63 from the first round keep their
individual prose.
{plural} SOURCE KEYS: recorded as a known defect the owner will fix once all
translations are finished (§7.4, §10). A pass should now pick a shape, note it in
pluralHackNote, and ignore the issue — no more weighing parenthetical against
slash per noun.
Docs: §5 step 5 and §6.9 rewritten so the audit is a required step with a stated
method rather than something you do on noticing; §7.4 and §10 record the {plural}
decision; CLAUDE.md states that §3.8 guards taste, not grammar;
docs/l10n-ui-translation.md records the findings under `is` traps.
Verified: selfcheck ALL CHECKS PASS (2052 keys, 8 cognates, 291 audited
corrections), runtime ALL RUNTIME CHECKS PASS, all 15 recorded locales still
green, is.js still 2058 lines matching en.js. check:specs PASS,
test:l10n:parity OK (31 finished, 15 enforced), format clean. test:l10n still
red at the same 17 pre-existing keys (§10); neither en.js nor src/ is touched.
63 + 235 double-counted the 7 keys corrected in both rounds. The corrections map in locales/is.json holds 291 entries, which is the figure to cite: 63 from the first round plus 228 new ones from the audit.
cs is one of the sixteen locales finished before any of this tooling existed
(§9.2), so it had never had a register measurement, a detector, a cognate review
or a grammar pass. This does all four and audits all 2052 values, not a
pre-existing half.
THE HEADLINE IS THAT cs IS HEALTHY, and it is worth stating because it
contradicts what the re-audit was set up to expect. These sixteen are DOUBLY
un-audited, so the prediction was that they would be as bad as is or worse:
is (pre-existing half) cs (whole bundle)
values audited 1052 2052
defects 235 (22%) 113 (5.5%)
garbled / foreign 14 0
agreement failures 11 0
wrong plural arrays — 0
dominant class bad compounds, terminology drift and
wrong senses internal inconsistency
Czech is an actively maintained Nextcloud locale with a real translator
community and the bundle reads like it. The lesson for the remaining fifteen is
that the defect rate tracks how healthy the locale is upstream, not how long it
went un-audited — so budget these passes for terminology counting, not grammar
repair.
MEASUREMENTS, all previously assumed rather than measured:
· register FORMAL — 828 formal (vy) markers vs ZERO informal (ty) over core
cs's 32 catalogues / 5005 values, plus 243 vs 0 in the bundle's own 2052.
§8.3 says a zero deserves a second look, so it was re-checked by a raw
unguarded scan over 25 informal tokens across the combined 6685 values: all
absent. cs is the ORDINARY Slavic T-V case and that is the point — it has a
live vy/ty distinction in current use, so unlike the three locales done
before it (ga has none, mt has one and leaves it unused, is had one and
abandoned it) there is no structural story. Those three were exceptions; cs
is the baseline they were exceptions to.
· buttons INFINITIVE — 48 bare action keys resolved against core cs give 27
infinitives, ZERO imperatives, ZERO verbal nouns; the rest are legitimately
not verbs (Storno, Zpět, Settings). So the 2sg imperative can be counted as
an informal marker without flagging every button, as in sk.
· nplurals=3, ABSOLUTE boundaries (1 / 2–4 / else), agreeing with the library,
so no pluralOrder or pluralBoundary is needed. All 7 arrays were already
correct, including _Successfully restored {count} object_, which switches the
participle per form (Obnoven / Obnoveny / Obnoveno) as Czech requires.
THE DETECTOR'S BOUNDARY GUARD IS LOAD-BEARING IN A WAY NO PREVIOUS LOCALE'S WAS,
and this is the durable finding (now §8.1):
· EVERY Czech 2sg imperative is a proper PREFIX of its 2pl counterpart,
because the 2pl is the 2sg plus -te: vyber/vyberte, zadej/zadejte,
nastav/nastavte, zvol/zvolte. This bundle holds 64 "vyberte" and core 48
"zadejte", so an unguarded 2sg list scores the commonest FORMAL shape in the
corpus as informal and INVERTS THE VERDICT OUTRIGHT. Worse than the sk vy-
prefix trap, because it hits the markers themselves. Several stems are also
prefixes of the app's own nouns: nastav ⊂ nastavení, zobraz ⊂ zobrazení.
· the informal possessive "tvá" is a substring of vytvářet / vytváření ("to
create"), which a raw scan finds 48 times — every one inside that verb.
· bare ty/ti stay unmatched per §8.2, and the cost is now measured: ty occurs
6 times in the corpus, all demonstrative, and ti zero times.
· bare "si" is unmatched for a DIFFERENT reason from hr/sk/sl. There it is the
2sg of "to be" as well as the reflexive clitic, so it is ambiguous. Czech's
2sg of být is "jsi", so si is only ever reflexive — empty, not ambiguous.
· "prosím" gives no free signal: it is 1sg, so it inflects for the speaker.
66/66 controls pass.
WHAT WAS WRONG, by class (codes recorded per key in locales/cs.json):
TERM-CONFIG 32 the noun "configuration" onto konfigurace
TERM-* 28 embedding→vnoření (8), chunk→úsek (4), audit trail→
auditní záznam (3), search trail→záznam vyhledávání (4),
View entity→pohled (5), Refresh→Znovu načíst (7, core),
Validate→kontrola (3), Test→Otestovat (3), erasure→
výmaz (2), purge (1), confidence→spolehlivost (2)
CONSISTENCY-* 16 (optional)→nepovinné (5), cannot be undone→vzít zpět
(4), type-to-search family (3), API key word order (3),
Merge Operations header (1)
SENSE 7 Commit message→Zpráva commitu (read "commit" as
"confirm" in a bundle full of Git); Complex→Složitý
(komplexní is a false friend — comprehensive, not
complicated); Uses→Použití (3sg verb for an AppTab
title); falling back; "in chunks"→v dávkách
CASE 6 No register objects reference this file — lost the "na"
that odkazovat governs; Queue / sync health → "Stav
frontu" used the genitive of masculine "front" (a front
line) where a queue is feminine "fronta"→"fronty";
"událost, kterou naslouchat" put an accusative on
naslouchat, which governs the dative, with no modal;
Search GitHub/GitLab dropped the locative
DANGLING-PREP 4 "Detekováno v" / "Extrahováno v" / "Aktualizováno v"
left the preposition without an object → the bundle's
own "Datum X" column pattern
COLLISION 3 Poor==Low, Reachable==Available, Objects being
analyzed==Objects Analyzed
COGNATE-FILLER 2 Driver, N/A — see below
TYPO 2 "fazetování" for "fasetování", twice
NORMALISE 2 Id→ID, Url→URL
WORDFORM 1 "vyžadovanost" is a nonce -ost formation
THE CONFIGURATION / SETTINGS SPLIT WAS THE OWNER'S CALL. The bundle rendered the
app's Configuration entity nastavení in 33 values and konfigurace in 31, and the
split ran THROUGH individual features rather than between them, which is what
made it a defect and not a preference: the Configurations screen was titled
Konfigurace while its own buttons read "Nové nastavení" / "Upravit nastavení" /
"Exportovat nastavení"; LLM Configuration was "Konfigurace LLM" but "Loading LLM
configuration..." was "nastavení LLM"; and "Failed to save configuration" was
byte-identical to "Failed to save settings". Core cs renders the generic heading
Configuration → Nastavení, pointing the other way, so it went to the owner per
§6.4 step 2. Decision: konfigurace for the noun, nastavení for Settings —
knowingly diverging from core, because core has no Configuration ENTITY and so
its rendering does not transfer to a domain where you create, edit, export,
import and publish them. Verbs untouched; "Configure X" → "Nastavte X" cannot
collide with either noun.
COGNATE REVIEW (§9.2), 26 identical values: 22 genuine, 2 filler, 2 casing
defects. Driver was filler — almost every locale translates it (de Treiber, fr
Pilote, es Controlador, hr Upravljački program), only cs and da did not, and
core cs itself writes "ovladače databází"; N/A likewise, with sibling sk
rendering "Neuvedené". Slug by contrast is kept by twelve bundles and stays.
TWO METHOD NOTES THAT COST SOMETHING, both now in §6.9 / §9.2:
· MECHANICAL CHECKS FOUND ESSENTIALLY NOTHING HERE — ellipsis glyph, trailing
punctuation, capitalisation, number agreement, whitespace and placeholder
checks between them produced one real finding and a dozen false positives.
On is they found 4 of 239; here ~0 of 113, because this bundle's defects are
semantic rather than morphological and no checker can see that "Zpráva
potvrzení" reads commit as confirm. Counting competing renderings per
English term produced about 70 of the 113 and needs no Czech at all. Do that
first. The exact-value-collision scan was second best.
· CORE OVERTURNED FOUR CANDIDATE FIXES, and a majority inside the bundle is
not authority on its own. "Loading…" → "Načítání…" looks like the outlier
against 37 sibling "Načítá se" values and is CORE'S OWN FORM, so the one
value I was about to correct was the only one that matched core. "Current
password" → "Dosavadní heslo" looked like a wrong sense and is core
verbatim. Bucket is untranslated in core and there means an S3 bucket, so
core is no authority for this app's histogram-bin sense and the value was
left alone.
Also: CHECK THE CALL SITE BEFORE CALLING A FORM WRONG. "folder" → "složky" looks
like a genitive where a nominative belongs and is correct — the key is a button
in the middle of a split sentence whose preceding fragment ends "přejděte do",
which governs the genitive. That is the §6.9 case-governance trap exactly.
Collisions left deliberately (§8.5): Konfigurace serves Configuration and
Configurations, Organizace both Organisation forms, Akce both Action forms,
Integrace both Integration forms — in each the Czech feminine -e noun is
genuinely syncretic between nominative singular and plural, so there is nothing
to disambiguate.
Verified: selfcheck ALL CHECKS PASS (22 recorded cognates, 113 audited
corrections), runtime-check ALL RUNTIME CHECKS PASS, check:specs PASS,
test:l10n:parity OK, format clean, l10n/cs.js 2058 lines = l10n/en.js, gatetest
confirms the parity gate genuinely holds cs. selfcheck + detector controls +
runtime-check re-run green across all 16 recorded locales. test:l10n stays red at
17 keys, unchanged from HEAD and unrelated — that is the §10 / §6.15 source-side
backlog.
sk is a Tier 2 locale: it already had a measured register, a detector, a cognate
review and a terminology record, but a `corrections` count of ZERO, so the audit
was the only missing piece. It was picked next on purpose rather than another
Tier 1 locale, to test whether a 0 means "clean" or merely "unverified" — and
because it is the closest sibling to cs in the whole set, which isolates upstream
maintenance from language family.
THE ANSWER IS "MOSTLY CLEAN", and the cs lesson holds:
is (pre-existing) cs (whole) sk (whole)
values audited 1052 2052 2052
defects 235 (22%) 113 (5.5%) 57 (2.8%)
garbled / foreign 14 0 1 (a typo)
agreement failures 11 0 0
wrong plural arrays — 0 0
wrong case 19 6 0
dominant class bad compounds, terminology ONE term, plus
wrong senses drift stray one-offs
So a 0 count means unverified, not clean — there were 57 real defects nobody had
looked for. But it does NOT mean a quarter of the file is waiting. sk and cs are
both actively maintained Nextcloud locales and both read like it; the defect rate
tracks upstream health, not how long the locale went un-audited. Tier 2 is
cheaper than the is numbers made it look.
WHAT WAS ACTUALLY WRONG is unusually concentrated: 37 of the 57 are one term.
· AUDIT TRAIL, 36 keys + 1. The bundle rendered the app's audit-trail entity
as `audítny záznam` — "audit record" — which forced "audit trail entry" to
come out as `záznam audítneho záznamu`, "record of the audit record", in 8
keys, with two stray renderings beside it (`Auditná stopa` ×1,
`audítorská stopa` ×2). Escalated as the owner's call: first-class entity
noun, 30+ keys, no core authority. The owner chose to re-coin as
`auditná stopa`, so "trail" is stopa, "entry" is záznam, and the stutter
dissolves by construction instead of being patched key by key.
The pre-existing outlier `Audit trail #{id}` → `Auditná stopa #{id}` turned
out to be the ONLY key already written the chosen way and needed no change:
the outlier was the model. That is the cs `Loading…` lesson in a new shape.
The adjective was also misspelled in all 35 keys that carried it — `audítny`
with a long í, where Slovak adds -ný to the stem with no vowel change
(kredit → kreditný, limit → limitný). The long í belongs to a different
lexeme, `audítor` (Latin audītor), whose adjective is `audítorský`; `audítny`
blended the two. The bundle's own `audit`, `auditu`, `auditovanie` and
`auditujte` keep the short i, matching core sk's "Auditovanie", and it
lengthens correctly in `Audítori` — so it already had the distinction and
applied it to the noun and the verb while getting the adjective wrong.
`Audítori` is preserved untouched for exactly that reason.
· TWO TABS CARRYING EACH OTHER'S MEANING. `Uses` → `Používa sa` ("it is used")
and `Used by` → `Používa` ("it uses") were inverted. Both are AppTab titles
over opposite relation directions. Every locale checked marks the direction
(cs Použití / Používáno v, de Verwendungen / Verwendet von, pl Używa /
Używane przez); sk was the only one with the pair swapped. Fixing `Uses` also
resolved a byte-identical collision with `In use`, whose `Používa sa` is
correct as a card badge.
· A SUBJECT/OBJECT REVERSAL. "No register objects reference this file" read
`Tento súbor neodkazuje žiadne objekty registra` — "this file references no
register objects", the opposite claim. Now `Na tento súbor neodkazujú…`,
which also supplies the `na` that odkazovať governs.
· SEVEN DANGLING PREPOSITIONS in <th> column headers — `Detected At` →
`Zistené o` and five siblings left `o`/`z`/`podľa` with no object, and `o` +
locative would mean "at (o'clock)" anyway. Fixed onto the bundle's own
`Deleted Date` → `Dátum odstránenia` pattern.
· The rest are one-offs: a Czech spelling (`strategie` for `stratégie`);
`Breaking change` → `Rozbíjajúca zmena`, a literal calque where 17 of 20
locales say "incompatible change"; `Commit Message` → `Správa potvrdenia`,
reading commit as confirm, the same defect the cs pass fixed; the
`Loading organisations...` outlier, wrong on the same two counts as in ga, mt
and is; and four consistency normalisations.
CHECKING CORE BEFORE "FIXING" AN OUTLIER OVERTURNED FIVE CANDIDATES, a higher
rate than cs's four, which makes it the highest-value step in the method:
· `Refresh` and `Restore` both render `Obnoviť` — and core sk collapses them
the same way, so the collision is core's, not the bundle's.
· `First` / `Last` / `Previous` → `Prvé` / `Posledné` / `Predchádzajúce` is
core sk VERBATIM, not neuter-where-Stránka-is-feminine.
· bare `Search` → `Hľadať` is core sk verbatim in 3 catalogues, despite every
compound key in the bundle using `Vyhľadať`.
CHECKING THE CALL SITE OVERTURNED FIVE MORE, and they are worth listing because
each looked like a textbook defect:
· `Handler` → `Riešiteľ` is right: `c.handler` is a person, an AVG case worker.
· `Fair`/`Good`/`Poor` → neuter `Uspokojivé`/`Dobré`/`Slabé` is right: they are
KPI labels over `skóre` (neuter), so they need not match the feminine
`Vysoká`/`Stredná`/`Nízka` confidence family.
· `Filter Statistics` → `Filtrovať štatistiky` is right: it is an h3 heading
OVER filter controls, so the infinitive action-label rule applies.
· `Requested at` → `Požiadané` and `Expires` → `Vyprší` are not dangling — the
template supplies the colon and a date follows.
DELETE/REMOVE WAS ESCALATED AND DELIBERATELY LEFT. The bundle renders both as
`Odstrániť` across 122 values (88 Delete, 41 Remove). Core sk splits them
cleanly — Delete → `Zmazať`(6)/`Vymazať`(3) and never `Odstrániť`; Remove →
`Odstrániť`/`Odobrať` — and `Zmazať` was the only free slot, since the bundle has
already spent `Vymazať` on Clear and on the GDPR Erase/`vymazanie` family, where
it is the standard term for the Art 17 right and cannot move. The owner chose to
leave it: both are valid Slovak, the collision is soft because the two actions
rarely share a view, and §3.8 protects an existing real translation from a change
of taste. Recorded in locales/sk.json as a deliberate divergence from core, with
the counts, so the next reviewer neither re-litigates it nor mistakes it for an
oversight.
METHOD NOTE: mechanical morphology checks were NOT written. Term counting showed
no morphological drift to chase, and the runbook's own numbers (4 of 239 on is,
about 0 of 113 on cs) say to skip them. Reading every value found the typo, both
reversals and all seven dangling prepositions — everything no checker can see.
Verified: selfcheck and runtime-check ALL PASS for sk; check:specs, parity and
format green; sk.js 2058 lines, equal to en.js; gatetest confirms the parity gate
genuinely holds sk. selfcheck + runtime re-run across all 16 recorded locales
(tr ca et hr lt lv ro sk sl bg sr rm ga mt is cs) — 0 failures. test:l10n is
still RED at 17 keys, unchanged: proved by `git stash -u`, re-run, compare, pop,
not assumed. That failure is the pre-existing §10 / §6.15 source-side defect and
is unrelated to this pass.
…of its cost
The sk audit took a whole session for 57 corrections over 2052 values, and almost
all of that went into two things that did not have to be slow: reading every
value in sequence, and forming candidate corrections that were then killed. These
three reports attack both. Every yield figure below is MEASURED against sk's 57
known corrections and against is as it stood before its own audit — not estimated.
core-diff.js pre-empts 6 of 6 false candidates
termdrift.js surfaces the term behind 37 of 57
spell.js 1 of 57 on sk; 5 of 5 garbled words on is
(+ the existing §6.9 dangling-preposition regex: 7 of 57)
Together they reach roughly 48 of sk's 57 from a few hundred lines of report
rather than 2052 values of prose.
CORE-DIFF IS THE ONE THAT MATTERS MOST, and not because it finds defects — it
stops you inventing them. Both the cs and sk passes checked core only AFTER
forming candidates, and core then overturned four and six respectively. On sk:
Refresh/Restore (core collapses them onto Obnoviť exactly as the bundle does),
First/Last/Previous and bare Search (core ships the bundle's wording verbatim).
Each looked like a textbook defect on grep evidence alone. All six sit in this
report's AGREE list, which is to be read as "never question these" — so the
whole false-candidate round disappears. Delete → Zmazať also lands at the top of
DISAGREE, i.e. the pass's biggest terminology decision surfaces in the first
minute instead of an hour in.
TERMDRIFT REMOVES THE GUESS FROM §6.9. That section already said to count
competing renderings first, and it produced ~70 of cs's 113 corrections and 41 of
is's. But all three passes counted BY HAND against a guessed term list — register,
schema, file, audit… On sk the term that had actually drifted was found only
because `audit` happened to be on the guess list. This indexes every English
content word instead. On the pre-audit sk bundle it prints 42 entries, one being:
"trail" — 15/16 keys use "zazna-", 1 do not:
"Audit trail #{id}" "Auditná stopa #{id}"
That single line is the entry point to 37 of the 57 corrections. Note it points at
the MINORITY side — which is the side the owner then chose, and which became the
convention for the other 36 keys. The tool surfaces the split; it does not and
must not pick the winner.
SPELL IS FOR WRONG-LANGUAGE CONTAMINATION, the class a reader's eye slides over.
Against is as it was before its audit it catches all five words that pass found by
reading, including the two the docs single out as proof the defects are not merely
stylistic: Stav (Slavic, in an Icelandic bundle) and skrivaðgang (a Danish stem
where Icelandic needs skrifaðgang). On sk it catches the one typo, strategie, and
suggests exactly stratégie.
Dictionaries come from LibreOffice's repo via fetch-dicts.js, NOT from the distro.
Arch/CachyOS official repos carry dictionaries for only ~10 of our 36 locales —
sk, cs, sl, hr, bg, lt, lv, et, is, ga, mt and every Nordic language are absent —
and package names differ per distro, so "install the dictionaries" is not a
reproducible instruction. The repo route covers 30 of 36, needs no root and pins
identically on every machine. Six locales have no hunspell dictionary at all
(fi ga lb mk mt rm); that is recorded as a known gap rather than silence, because
ga/mt/is-like locales are precisely where the garbled-word class concentrates.
WHAT WAS BUILT, MEASURED AND DELETED: a cross-locale outlier report, clustering
all 36 locales' values per key by shared character n-grams and flagging a locale
sharing nothing with the consensus. Recall 1 of 57, precision 1 of 65 — it found
Breaking change and nothing else, because app-specific terminology has no
cross-locale consensus to be an outlier against. Shipping a 1/65 tool into a
runbook the next pass trusts is worse than not having it, so it is not here. The
cross-locale check that does work is the one that already exists — `l10n-ai.js get
<key>`, used on demand once you have a suspicion, which is how Breaking change
(17 of 20 locales say "incompatible") and the swapped Uses/Used by pair were both
settled. Targeted beats scanned for that question.
Two bugs found while validating spell.js, both worth naming because both faked a
clean result:
· it suppressed unknown words hunspell had no suggestion for. That is exactly
backwards — a word so mangled the dictionary cannot propose a neighbour is the
MOST likely real defect, and is's Misheppnaðst and levranir are that shape.
· hunspell -l can name a SUB-token of a word (it splits on hyphens and
apostrophes), which was absent from the origin-key map and threw, killing the
report right after its header. A crash that prints a header and no findings is
indistinguishable from "nothing found", which is the worst failure available
to a reading aid.
All three reports exit 0 whatever they find, so none can gate a build or be
mistaken for one, and none of them writes: apply.js remains the only writer.
Runbook §6.9 now carries the three reports, their measured yields and the
fetchdicts prerequisite INLINE, and scripts/l10n/README.md lists the four scripts.
The method deliberately lives in the runbook rather than in a separate document, so
that a pass reading §6.9 gets it without a second lookup. The blind spots are
recorded in each script header: termdrift cannot see inflection-level or word-order
splits, and the Slovak dictionary rejects BOTH auditný and audítny, so the 35-key
adjective misspelling that dominated the sk pass was invisible to spell.js.
Verified: all three run clean on sk/cs/is; spell exits with guidance rather than a
stack trace for a locale with no dictionary; check:specs, test:l10n:parity and
format green; test:l10n unchanged at 17; selfcheck passes for all 16 recorded
locales; no l10n/*.js modified by this commit — lib.js is untouched, so no shared
behaviour can have shifted.
Carries the one-time machine setup for the spell pass, each check's measured yield against sk's 57 corrections, the blind spots, and the record of what was built and deleted (the cross-locale outlier scanner, at recall 1 of 57). §6.9 keeps its inline summary — a pass reading the runbook should not need a second lookup to know the order — and now also points here for the detail.
The sk audit was committed as complete at 57 corrections. A second pass over the
same bundle, run as a parallel fan-out on a fresh context rather than by
re-reading, found five more. Four are not marginal:
· "Array Object Configuration:" — "Konfigurácia objektu polia" used the
NOMINATIVE PLURAL of pole where the genitive singular poľa belongs, so it read
"configuration of the object of the fields". The bundle already declines this
noun correctly elsewhere (na úrovni poľa, vyprázdnenie poľa, and the real
nominative plural in fasetovateľné polia) — the form was available and simply
not used. This makes the first pass's claim of "zero wrong case in sk" wrong.
· "Draft denial" — a noun where the measured convention takes the infinitive.
It is the primary NcButton of DenialComposerDialog.vue, carrying the saving
spinner, and its own sibling button "Compose denial" was already the
infinitive Zostaviť zamietnutie.
· "Analyze existing properties for improvement opportunities" — item 5 of the
"Analysis steps:" list in ExploreSchema.vue, whose other five items are all
infinitives (Získať, Extrahovať, Zistiť, Identifikovať, Porovnať). This one
alone switched to formal 2pl.
· "Property name of inversed relation" — obrátenej RELÁCIE uses the bundle's
word for SESSION (Priemer vyhľadávaní/reláciu) to mean RELATION, which the
bundle otherwise renders vzťah without exception (Relations → Vzťahy). 4 to 1.
· "Control which object views…" — ovládať means to operate a device, not to
decide which items are selected. The softest of the five and flagged as such
in its record entry so it can be reverted cheaply.
Each was confirmed at its call site before being accepted.
THE LESSON IS ABOUT METHOD, NOT THIS LOCALE. A single sequential reader has a
recall ceiling that care does not remove, because what survives a first read is
exactly what a tired eye normalises — a plausible-looking case ending, one list
item out of six, a noun that could pass for a heading. The read-through should be
run as a fan-out over independent chunks with a central adjudicator, and a
completed audit must NOT be read as proof a locale is clean. Recorded in
locales/sk.json under auditSecondPassNote so the next pass inherits it.
Verified: selfcheck and runtime-check ALL PASS; parity and format green; sk.js
still 2058 lines; corrections now 62.
The guidance to "parallelise the read-through" was abstract in the speedup doc
("readers", "one reviewer") and ENTIRELY ABSENT from the runbook, which is the file
a pass actually reads. So the most useful process change was documented in a way
that no agent could act on. §6.9 now carries it as a numbered recipe naming the
Agent tool: slice into ~4 chunks, one subagent per chunk in a single message,
shared context, structured candidates, central adjudication.
WHY IT MATTERS MORE THAN SPEED: the sk audit was committed complete at 57
corrections, and a second pass over the same bundle in a fresh subagent context
found five more — a nominative plural for a genitive singular, a noun where the
button convention takes an infinitive, one formal-2pl item in a six-item list of
infinitives, and the bundle's word for SESSION used to mean RELATION. The first
pass had also claimed "zero wrong case in sk", which was false. sk is 62, not 57.
A single sequential reader has a recall ceiling that care does not remove, so a
completed audit must not be read as proof a locale is clean.
Two constraints recorded with it, both measured rather than assumed:
· CHEAP MODELS GENERATE, THEY DO NOT DECIDE. On the pre-audit sk bundle a
Haiku-class reader returned one real finding at MEDIUM confidence and two at
HIGH confidence that were both wrong — nemajú for the correct 3sg nemá after a
genitive-plural numeral, and the masculine-animate obnovení for the correct
inanimate obnovené. Applying either writes a NEW error into correct Slovak and
nothing in this repo could catch it. Its only correct finding was its least
confident, so its confidence field is not usable as a filter.
· SUBAGENTS NEED A READ BOUNDARY. A subagent touching the checkout gets
CLAUDE.md and this runbook auto-attached, and §6.9 now names sk's findings and
trap list outright. Harmless in a real pass, fatal to any attempt to MEASURE a
reader against an already-audited locale — point those at a scratchpad copy.
No code change; docs only.
The speedup doc had grown into a 230-line narrative and the §6.9 additions read as essays. Both are now terse rule lists — the speedup doc is 64 lines, and the §6.9 fan-out and core/call-site blocks lost roughly two thirds of their length. Every rule and every measured figure is kept; the surrounding prose is not. No behaviour change; docs only.
… spell.js Both found by running the audit reports against `ca`, the first locale whose orthography the spell report had not met. fetch-dicts: `ca fr sv lt be` all failed. LibreOffice does not lay its dictionaries out uniformly — `ca`, `fr_FR` and `sv_SE` keep theirs in a `dictionaries/` subdirectory, `lt_LT` uses the bare `lt` basename rather than `lt_LT`, and `be_BY` publishes only `be-official`. Five map entries corrected; all five now fetch. 28 of 36 locales have a dictionary, up from 23. spell.js: U+00B7 MIDDLE DOT is word-INTERNAL in Catalan and was missing from the token class, so every geminate-l word split into two junk halves — `col·lecció` became `col` + `lecció`, `paral·lelisme` became `paral` + `lelisme`, and `instal·lada` became `insta` + `lada`. Seven such halves were reported as misspellings. The `ca` report drops from 72 flagged words to 64, and none of the eight lost was a real word. Catalan is the only locale here that uses the interpunct, which is why five earlier passes never saw it. Neither script touches lib.js, so shared behaviour cannot have shifted and the all-locale regression loop does not apply.
Second Tier 2 locale, and the fourth audited overall. Whole bundle, 2052 values, 128 corrections (6.2%) — above cs (5.5%) and sk (3.0%), below is (22%). Method: the three reports first (core-diff, termdrift, spell), then an exact-value-collision scan, a dangling-preposition sweep and an elision sweep, then the whole listing read as a four-way subagent fan-out with every candidate adjudicated centrally against core and the call site. The fan-out produced 85 candidates and about 20 died in adjudication — the same ratio sk saw. TWO ON-SCREEN COLLISIONS, which is a defect class neither cs nor sk had: Settings/Configuration both rendered Configuració, and three dialogs use them as SIBLING TAB LABELS (EditConfiguration 33/128, EditWebhook 23/215, EditOrganisation 35/380) — two tabs with the same name. Core ca splits them (Paràmetres/Configuració) and so did the bundle's own one key containing both words. Escalated because it is a first-class entity noun over 30+ keys; the owner chose to normalise all 35. Configuració is now the Configuration entity and Paràmetres the Settings sense. Logs/Registers both rendered Registres, and ViewSource.vue builds its tab bar as tabs: ['Registers', 'Logs'] while both its empty states read the same. Core ships no Log key, so core could not decide it; 13 of 16 sibling locales distinguish the two and only the three Ibero-Romance ones collapse them — es and pt, both still un-audited, so not evidence. Registres is locked as the primary entity, so Logs took the qualifier its 16 sibling log keys already use. Other classes: 11 dangling prepositions in column headers (Detected At → Detectat a, and To Date → Fins a la data, which is the Catalan idiom for "up to now"); 5 RBAC permission-matrix headers written as imperatives instead of verbal nouns (§8.7, the same defect hr and lt had); 6 keys glossing "record" as registre and so colliding with Register; 4 token → testimoni (core-backed); 4 (s)-parentheticals expanding to non-words (dia(es) → "diaes"); 2 elision failures; 15 wrong senses including four false friends that read perfectly in isolation — inconsistent (= flimsy), citació (= summons), desplaçament (= scrolling), autoritzat (= authorised, not authoritative). CORE AND THE CALL SITE OVERTURNED THE REST, and two of them were collisions indistinguishable from the two real ones: Refresh/Update → Actualitza and Remove/Delete → the suprimir family are both core ca VERBATIM, so both stay. The call site killed Good/Fair/Poor and Successful/Failed as agreement failures (no feminine antecedent; rastre is masculine) — the same false positive sk produced — and Fonament and Punts finals died on the bundle's own vocabulary. All recorded in ca.json with the counts, including what was deliberately left. ca.json also gains spellAllow (43 domain words), so the next spell run over this locale is short. Verified: selfcheck ALL CHECKS PASS (128 audited corrections recorded), runtime-check ALL RUNTIME CHECKS PASS, check:specs, test:l10n:parity, format all green, gatetest confirms the gate holds ca, 2058 lines matching en.js, and selfcheck + detector controls + runtime-check re-run green across all 16 recorded locales. test:l10n stays RED at its pre-existing 17 keys (§10) — this pass touches no src/ and adds no key, so it cannot affect that count.
…fixes
Register FORMAL, 200 markers vs 9 over 3321 translated values. Buttons are
INFINITIVE, 64 vs 0 over 59 action keys. nplurals=2, plain `n != 1`, and
runtime-check confirms header and library agree at every count.
THIN CORE COVERAGE IS MORE DANGEROUS THAN NONE. rm and mt had zero lb
catalogues, so coreCatalogues() threw and §5 step 2 could not run by
accident. lb has exactly ONE catalogue with 72 values, so the call
SUCCEEDS — and those 72 values contain no address form at all, so the scan
would have reported a verdict computed from 0 markers of either polarity.
detectors/lb.js therefore counts core's markers rather than its catalogues,
prints "too thin to decide anything", and falls back to the app family's own
frontend bundles as the mt pass did (1054 own + 2386 sibling values).
openregister's own half carries 0 informal markers; all 4 informal values
live in launchpad and openconnector and are real slips there. bs, at 55
values, is the same trap.
fold() DOES NOT LOWERCASE — a first for this codebase, and the whole design
rather than an optimisation. Lowercase `dir` is the informal 2sg dative;
capitalised `Dir` is the polite 2pl nominative. Both are attested (1 and 82).
A case-folding detector merges all 83 and inverts the verdict. The residue is
recorded in UNDETECTABLE: a value opening with the informal dative takes a
sentence-initial capital and cannot be distinguished; all 11 such values in
the corpus are polite.
Two further exclusions, both measured rather than reasoned. The MODALS
syncretise 1sg/2sg/3sg, so `muss` and `weess` carry no address information —
all 15 bare `muss` in the corpus are 3sg — while the regular 2sg of the same
verbs (kanns, wëlls, sollst) does. That is a partial paradigm split by
LEXICAL class, a third way after bg's and is's conjugation-class splits. And
`-t` is both the 2pl and the 3sg ending, so `kënnt` ("you can" / "he comes")
and `braucht` are out; only 2pl forms whose 3sg differs are kept.
THE AUDIT: 94 corrections, and the overwhelming majority are ONE obligatory
orthographic rule. The Eifeler Regel deletes a word-final -n before any
consonant but n/d/t/z/h and keeps it otherwise. It fires several times per
sentence, no gate can see it, and this bundle broke it in BOTH directions.
It is the one mechanical morphology check that has ever paid off here,
because the trigger is deterministic rather than agreement-based; the method
is docs/l10n-workflow.md §8.11.
THE PER-LEMMA CHECK IS NECESSARY BUT NOT SUFFICIENT, and this is the lesson
worth keeping. Comparing each lemma only against itself excused every lemma
occurring in a single environment: `Lueden` appears only WITH the final -n
and `Deele` only WITHOUT it, so both read as internally consistent. 26 values
were left uncorrected on the strength of a "16:0, no counter-example" count
that was really one copy-pasted phrase repeated thirteen times. Aggregating
by WORD CLASS shows the family does both — 118 kept against 108 deleted —
with directly parallel pairs: Lueden vum / Deele vun, Späicheren
feelgeschloen / bäisetze wëllt, erstellen wann / zesummeféiere wann. A
uniform lemma is evidence of one decision, not of a rule. Run both cuts.
Corrected in this commit; 48 further values fixed on that basis.
TRUE RESIDUE: 2 values, both pre-existing, both a bare sinn/ginn infinitive
governed by a preceding modal ("muss aktivéiert sinn mat", "kënne
erëmgewonne ginn wann"), where reducing to si/gi collides with the pronouns
and the family offers no directly parallel example either way. §3.8 governs
those two.
Splitting at HEAD also caught 44 violations in the half written during this
pass — more own-drift than any previous locale produced. Two Germanisms of my
own were caught the same way (Vom -> Vum, and `zu` for `ze` before an
infinitive), and the auto-fixer's own bug (extern -> the non-word exter) is
why §8.11 names the -ern exemption class.
Both cheap audit reports were structurally blind to all of this: corediff had
only 4 shared keys, termdrift found almost nothing because a heavily
compounding language buries the stem inside Lëschtenusiicht and
Webhook-Liwwerung, and the spell report does not exist for lb. Where all
three come back thin, ask what rule the language has that they cannot check.
Capitalisation is a fifth §8.10 outcome: grammatically FORCED, since
Luxembourgish capitalises every noun. Measured anyway and unanimous; the
conditioned measurement changes nothing (39:1 title-cased, 172:2 prose). Two
false positives worth knowing: {register}/{schema} are placeholders, and
lowercase `filteren` is the verb.
46 cognates recorded, every one with its §3.3 distinct-value count. Two
looked like cognates and were FILLER — `GitHub Personal Access Token` and its
GitLab sibling, which 32 locales translate, the exact shape the cs pass
caught. Conversely `Mappings` was kept on openconnector's evidence, which
owns the concept and compounds it as Mapping-Detailer. `Driver` is flagged in
locales/lb.json as the weakest entry in the set. Lexicon settled against core
and recorded: Files -> Datei, the app family being 111:0 against core's
Fichieren.
All 15 §8.4 wrong-sense offenders verified at their call sites: Right is an
RBAC permission, People the PERSON entity type, View a row action, Search a
field label, Handler a person, Bucket a histogram bin, Subject the GDPR data
subject.
selfcheck ALL CHECKS PASS, runtime ALL RUNTIME CHECKS PASS, gatetest
confirms the parity gate genuinely holds lb, and all 17 recorded locales
re-verified with no regression. check:l10n is byte-identical to HEAD at 129
issues and test:l10n is still RED on the same 17 pre-existing keys — both
unrelated to this work (§10).
Also folds the orphaned docs/l10n-audit-speedup.md into runbook §6.9 and
deletes it; that change predates this pass and no references to it remained.
Sixth low-resource locale. 1053 -> 2052 keys, key-for-key identical to en.js.
MEASURED
- Register: FORMAL, the least ambiguous verdict in the set. Core 218 polite
markers to 1 (5 catalogues, 603 values); the four sibling frontends 556 to 2.
`ti` occurs ZERO times in 3980 values, and all three informal values in the
corpus live outside this bundle (one core/encryption, two openconnector — now
recorded as §11 reciprocal work).
- Buttons: a FIFTH §7.3 pattern and the first non-categorical one. 2sg
imperative for a label, 2pl once the string is a sentence, sliding with
length: 219:1 at <=14 chars, 32:42 at 40-79, 7:35 at 80+, crossover ~40.
`Select`/`Choose` prompts take 2pl at any length (67:26).
- Plurals: nplurals=2, plain `n != 1`; header and library agree at every count.
- `ju lutem` is register-bearing (83:0 against `të lutem`) — only the second
locale where the politeness formula pays, because Albanian's inflects for the
ADDRESSEE rather than the speaker.
DETECTOR — 86 controls, all passing
Four measured exclusions, two of them total: `-ni` is the 2pl ending AND the
definite singular of every masculine -n stem (~45 nouns in the corpus); `-sh` is
the 2sg subjunctive AND the ablative plural of every noun; bare `do` is 2sg AND
3sg of `dua` AND the future particle (101 occurrences of `do të` in third-person
prose); and the whole `-oj` class is a 2sg/3sg homograph with no conjugation
split to rescue it. `tij`/`tyre` excluded as third person.
The left guard carries a HYPHEN — new in this set — because Albanian attaches
the definite ending to acronyms after one (`UUID-je`, `Token-i`, `PHP-ja`), so
`(?<!\p{L})` matched an inflectional ending as the 2sg copula. The apostrophe is
deliberately unguarded: `t'ju` is formal, `s'ke` informal.
AUDIT — 160 corrections over 1018 pre-existing values (15.7%)
- 39 ORTH-VEKTOR: `vectoriz-` -> `vektoriz-`. Albanian `c` is /ts/, and the
bundle already wrote `vektoriale`/`vektorë` 15 times — one value carried both
spellings, which settles it.
- 36 BUTTON-2SG: 2pl imperatives on short labels, against the length gradient.
- 18 TERM-ENTRY: `hyrje` for *entry*, where core sq uses that word for ACCESS
and renders "directory entries" as `zëra`. termdrift reported the minority
side and the minority was right (§6.9).
- 12 COLLISION-LOG: `Logs` and `Registers` both rendered `Regjistra` and are
ADJACENT TABS in src/modals/source/ViewSource.vue:246. `ditar` takes the log
sense, as every finished locale distinguishes them.
- 3 COLLISION-PURGE: `Purge` collided with `Clear` on `Pastro` -> `Spastro`.
- plus 9 TYPO, 9 SENSE, 8 AGREEMENT, 6 AGREEMENT-PARTICIPLE, 6
LEFT-IN-ENGLISH, 4 TERM-DEFAULT, 4 ARTICLE, 3 NUMBER, 3
CONSISTENCY-PROGRESSIVE, 3 TERM-ENDPOINT, 2 TERM-FACET, 2 GARBLED, 1 each
TERM-ACCESS / TERM-PREVIEW / PERMISSION-NOUN / CONSISTENCY.
TOOLING
loadLocaleConfig now whitelists `spellAllow`, which spell.js already read but
never received — ca.json's 43 recorded words were being dropped and every run
printed "0 allowlisted". `spell ca` goes from 63 unknown words to 19, `sq` from
185 to 25 (the remainder are sub-token artefacts).
DOCS
§8.10 gains the finding most likely to transfer: a capitalisation ratio
conflates prose with Title-Cased headings, and the confound points toward doing
the work. The naive scan reported this bundle capitalising domain terms 25-35%
against a family rate near zero; conditioned on the English key it is 0 of 177,
and all 110 "defects" were headings correctly mirroring their source.
VERIFIED
selfcheck ALL PASS, runtime ALL PASS, detector 86/86, gatetest confirms the
parity gate genuinely fails and names sq. All 17 recorded locales re-checked
green after the lib.js change. check:specs PASS, test:l10n:parity OK with 33
finished locales, format clean. check:l10n unchanged from HEAD at 129 issues;
test:l10n still RED on the same 17 pre-existing keys (§10) — this diff touches
neither src/ nor en.js.
Takes mk from 1053 to 2052 keys, at full key-for-key parity with en.js, and
audits the whole pre-existing half.
Register: FORMAL. Core is usable here (24 catalogues, 3424 values) and gives
565 formal markers against 59; the bundle's own 1053 values give 31 against
zero, so core and the file agree and there was nothing to overrule. Split the
59 before reading it: 25 twoj* possessives, 14 ti/te/tebe pronouns, ~20 2sg
presents, concentrated in one catalogue (settings 34) and reading as app-store
blurbs. Macedonian's T-V distinction is live and ordinary, so this is the plain
cs-style measured choice with no structural story.
Buttons: the sq LENGTH GRADIENT, replicated, with the crossover in the same
place at ~40 characters. Core runs 128:1 for the bare 2sg imperative at <=14
chars and 6:12 the other way at 80+. Its Select/Choose and Enter/Type prompts
take the 2pl at any length as sq's do — but Search goes the other way and is
not close (core 61:1 for 2sg), so the override is lexically bounded rather than
"any prompt".
Plurals: pluralBoundary "library". The header is modular, the library drops the
n%100!=11 guard, so only 11 and 111 disagree — the mirror of is, and narrower.
Form 0 stays the plain counted singular because it is still reached correctly at
1, 21, 31. No productive counted form: the izbrojana forma in -a exists but core
writes the plain plural for the app's nouns, so bg's hazard does not transfer.
Detector: VI is the Macedonian acronym for AI and a homograph of the formal
dative clitic vi. Case is the only discriminator, so fold() consumes the
all-caps form before lowercasing — the lb dir/Dir problem from the other end.
The hyphen goes in the left guard only. 67/67 controls pass. Two useful
negatives: bare ti is usable, and bare te is usable where bg's is not.
Audit: 114 of 1053 pre-existing values corrected (10.8%), and 105 of those are
ONE mechanical rule. Macedonian sentence-cases common nouns in prose and in
headings; the family is unanimous (siblings 1:273, core 2:~480 in prose) and
the bundle ran 41:146 in prose and 65:507 in headings, all 41 prose hits inside
four terms while objekt — the app's most central noun — was 0:26. That
inconsistency is what makes it a defect rather than an sr-style convention.
The other 9: 4 agreement (prosecno + a plural noun in stat labels, resolved
with the bundle's own Maks./Min. abbreviation style), 3 progressive-form
outliers against a 20-value convention, 2 terminology (dnevnik -> log, akcija
-> dejstvo), and 1 Serbo-Croatian value. Zero garbled words, zero wrong plural
arrays, zero register deviations, right alphabet throughout.
Audit trail #{id} carried "Revizorski trag", Serbo-Croatian in both halves,
against 20 of this bundle's own values using "reviziska traga" — the openbuild
Croatian catalogue reaching a committed bundle for the second time, in the same
key it reached in sr.
Three measurement gaps met while measuring the casing, two of them new: a stem
alternation without the i flag hides the entire thing being measured; (?<=.)
does not exclude a LATER sentence's first word (3 hits, each of which would
have been "corrected" into an error); and an opening parenthesis, a leading
emoji and deliberate all-caps each license a capital (14 of 132 raw hits).
The lb question was asked and came out no: clitic doubling of definite objects
looks like the Eifeler Regel and is not, because the trigger is semantic and
the clitic's position depends on clause type. 0 of 5. Recorded so the next pass
does not rebuild it.
12 recorded cognates. Reachable/Edit labels/No label/No labels were replaced
after being written earlier in this same pass, so apply's suggestion to record
a corrections reason does not apply to them (workflow §6.3).
selfcheck ALL PASS, runtime ALL PASS, gatetest confirms the parity gate holds
mk, and all 19 recorded locales re-verified. check:l10n stays at its 129
baseline.
Fifteen locale passes appended a case study each to whatever section they touched; exactly one commit ever removed anything. The rules had come to carry 3-5x their own length in retrospective evidence for themselves. Cut, in order of size: - the per-locale LINGUISTIC reference — register evidence per locale, suffix traps and pronoun homographs per language. Already in docs/l10n-ui-translation.md, which this file's own header table names as its home. Replaced with the generalisable rule plus a pointer. - per-locale case studies through §6 and §8, down to the rule each taught. - §9.2's prose retelling of its own defect-rate table; the table stays. Kept deliberately: the plural-boundary table (§7.1), the wrong-sense harvest table (§8.4) and the casing-outcome table (§8.10) — dense, app-specific, not per-locale. Also the counter-examples in §6.5 and §8.2, because the doc's most-repeated lesson is "do not port an answer across locales", and a rule stripped of its counter-example reads as universal when it is conditional. Two mechanism changes, since the cut alone would only re-accrete: - the file declares a 1600-line ceiling, and §5 step 11 now says a pass earns room here by deleting, not appending. - CLAUDE.md referenced the doc as an @ import. That syntax can pull all ~1500 lines into context on every call, and every other reference in the repo uses a plain path. Now plain too. No content moved elsewhere. All 63 headings kept, so the §N.M cross-references in CLAUDE.md, scripts/l10n/README.md, docs/l10n-ui-translation.md and the locale JSONs still resolve; verified inbound and internal.
Brings l10n/be.js from 1053 to 2052 keys, key-for-key identical to en.js.
REGISTER: formal, and the first verdict resting on an asserted ABSENCE rather
than a ratio. detectors/be.js over core's 14 be catalogues (1873 values) counts
334 formal markers and ZERO informal; the bundle's own pre-existing half runs
156 to zero. Neither corpus contains a single ты / цябе / табе / тво- form.
BUTTONS: infinitive, register-neutral (the cs/lt/lv/sk/rm/is/lb group).
Resolving 44 bare action keys against core gives Захаваць / Выдаліць / Дадаць /
Скасаваць / Рэдагаваць — forty-odd infinitives and not one imperative. Sentence-
length prompts take the 2pl imperative, which is the ordinary formal shape.
The 2sg imperative IS counted, the minority outcome: §6.5 test 1 passes because
labels are infinitives, test 2 because the enumerated forms are unambiguous. The
catch is that Belarusian builds the 2pl imperative as the 2sg plus -це
(выберы → выберыце), so the trailing (?!\p{L}) guard is the only thing between
the polarities — the West Slavic hazard in a different branch, so it is not a
family property. 85/85 controls, covering the -це locative trap (фармаце,
праекце, тэксце, запыце — nouns, not 2pl verbs) and the -ш trap (ваш, больш,
менш, хэш, кэш), either of which would invert the verdict.
PLURALS: nplurals=3, and the header, boundaries and expression are byte-identical
to Russian's while form 1 still needs a different NOUN — Belarusian takes the
nominative plural after 2-4 where Russian takes the genitive singular (2 запісы,
not 2 записа), with the adjective and verb going plural alongside. Core be is
one-sided across all 30 of its arrays. Header and library agree at every count.
AUDIT of the pre-existing half: 41 defects in 1053 values (3.9%), in line with
sk (2.8%) and cs (5.5%). Two were RUSSIAN values committed in a Belarusian
bundle — Audit trail #{id} and NO ACTION. Belarusian has no и, щ or ъ, so one
grep finds that class exactly; the same grep established that openbuild's entire
be.json is a separate Russian translation (426 lines), which harvest.js's
byte-identity guard cannot see, so all eight of its candidates were rejected.
The rest: a Russian calque for "default" against the file's own прадвызначан-,
Cancel against core's Скасаваць in six catalogues, Share disagreeing with the
bundle's own noun Абагульванні, Fixed rendered as "corrected" in a retry-policy
list, Totals byte-identical to Results on one sidebar, four dangling
prepositions in <th> timestamps, and nine View X action buttons written as
verbal nouns against the measured infinitive convention — the tell being that
one NcActions menu carried Выдаліць and Прагляд падрабязнасцей side by side.
SANDHI (§8.11): the question came back YES and the bundle already obeyed it.
у after a consonant or pause, ў after a vowel — deterministic, several times per
sentence, invisible to every gate. HEAD half: 98 correct word-initial ў and 47
correct у, zero violations either way. The half written here: 169 and 58, also
zero. Casing is flat lowercase (0 capitalised against 458), unchanged when
conditioned on the English key's own casing. Ellipsis mirrors the source key.
Nine cognates recorded with justifications. Five values written during the pass
were corrected before commit and so carry no corrections entry: No label (facet
sense written for the file-tag call site), a capitalisation outlier, a wrong 2pl
imperative спашлецеся for спашліцеся, a term split пераазначэнне against the
file's own перавызначэнне, and two coinages replaced with the bundle's own
vocabulary.
selfcheck 16/16, runtime all-pass, gate negative test passes, check:specs and
format green, check:l10n unchanged at its 129 baseline. test:l10n stays RED for
the pre-existing reason in §10 — a source-side rename job, its own commit.
The FINISHED_DEFAULT line also carries 'bs', added concurrently in the same
working tree by the parallel Bosnian pass; it shares the one line with 'be' and
could not be staged apart from it.
…heck
Two gaps found while working bs, both in classes no gate can see.
casing.js — the §8.10 mid-sentence capitalisation measurement, which five
passes had done by hand and which decided a third of sr's values and the whole
dominant class of mk's. Encodes the three documented ways the hand measurement
misleads: a missing `i` flag hides the very occurrences being counted, `(?<=.)`
excludes the value's first word but not a later sentence's, and an opening
bracket / leading emoji / deliberate all-caps each license a capital. Conditions
on the English key so prose and Title-Cased headings are never pooled — the
distinction that made sq's ~110-value defect class evaporate to 0-of-177.
Prints the bundle against sibling-frontend and core baselines plus a per-term
up:down split, and --mine restricts it to what the working tree changed, which
is the split-at-HEAD check for a pass's own drift. Found one real defect that
way on bs, and reported its own false positive first: a one-letter word cannot
be classified as all-caps on its own, so every `O` inside an all-caps heading
scored as a mid-sentence capital until a run had to be open for it to count.
script-coverage.js — a homoglyph check, and it now runs for EVERY locale rather
than only the non-Latin ones. A Cyrillic о (U+043E) inside a Latin word renders
identically to a Latin o while breaking search, sorting and speech; the value is
not empty, not identical to English, not wrong-arity, and reads as finished work.
The existing two lists cannot see it because they ask whether the expected script
is PRESENT, and it is. All 30 Latin-script locales had no coverage of this at all.
It found one defect in bs (`proširenо`) and one in mk (`првa`, a Latin a inside a
Cyrillic word — the inverse direction).
Two iterations were needed and both are recorded in the file:
- A hyphen is a morpheme boundary and must break the run. Macedonian, Serbian,
Bosnian and Albanian all attach case endings to a Latin acronym across one
(API-клуч, webhook-a, UUID-je), which is correct morphology. Testing whole
words reported 105 hits on mk and 19 on uk, essentially all of it that
construction.
- mk writes `сè` with a Latin è and core mk does the same in 7 values with zero
of the Cyrillic U+0450, so those 29 hits are the prevailing convention, not
defects. Recorded in a new `homoglyphAllow` field rather than suppressed in
the script — a section that always shows 29 hits trains the reader to skip it.
Suppressing them is what exposed mk's one real defect underneath.
`homoglyphAllow` is whitelisted in loadLocaleConfig, the trap that silently
dropped pluralOrder and spellAllow before it, and verified observably: mk goes
29 -> 1 and bs stays at 1.
Regression-checked across all 21 recorded locales — selfcheck, detector controls
and runtime-check all pass, and the non-Latin locales come back 0 homoglyphs
except mk's recorded set.
Completes the last locale: all 36 are now at full key-for-key parity with en.js.
2052 keys, 21 recorded cognates, 0 absent, 0 empty, 0 bad arity. selfcheck 16/16,
runtime all-pass, detector 58/58 controls, and the parity gate negative-tested so
the claim that it holds bs is checked rather than assumed.
REGISTER: formal (Vi), 367 second-person plural markers against 1 singular over
3416 frontend values. Core could not decide it — Nextcloud ships ONE bs catalogue
whose 55 values yield 0 markers of EITHER polarity, which is the §2.3 thin-core
trap where the scan succeeds instead of throwing. The §6.4 fallback over this
bundle plus three sibling frontends settled it. The single informal hit in the
corpus is not in this app: it is openconnector's "Još nemaš posredovanih
vjerodajnica…", the same defect class the sq pass found in the same bundle (§11).
BUTTONS: bare 2sg imperative against formal prose (pattern 2). The lexical prompt
override is a THIRD distinct partition of the same verb set after sq's length
grading and mk's grouping — here the English verb decides and the line runs
between Select (2sg, 20 of 20, 14-45 chars) and Choose/Enter (2pl, 5 of 5 each,
15-128 chars). Length is not the variable.
BYTE-IDENTITY WITH A SIBLING LANGUAGE IS NOT EVIDENCE HERE, which is the finding
worth carrying. §2.3 predicted the openbuild Croatian catalogue would be found
inside this committed bundle as it was in sr and mk. It was not: for bs that
catalogue is filed under bs.json, the backend set, which §1 puts out of scope.
And BCS share too much for the test to mean anything — `Revizijski trag #{id}`
matches hr exactly and is correct Bosnian. What paid instead was measuring
standard-language lexicon per term: the bundle is genuinely Bosnian, ijekavian
like Croatian (Osvježi, Sljedeće) with an Eastern-leaning lexicon (Sačuvaj not
Spremi, Kreiraj not Stvori), and a sweep of the whole known bs/hr divergence set
found exactly two Croatianisms.
AUDIT of the 1019 pre-existing translated values, 26 corrections, all recorded
with class codes in locales/bs.json:
TERM-HR 10 pružatelj -> pružalac (7, Bosnian forms this agent-noun class in
-lac); predložak -> šablon (3). Both had this pass's own half on
the other side, so the bundle was split against itself in both.
CONSISTENCY 9 Izvor capitalisation (5), kôd circumflex (2), log/zapis, and
the ... vs … outlier
SENSE 4 Commit Message was "confirmation message" on a GitHub publish
form; Totals/Register Totals used iznos, a monetary amount, over
object counts; Top Deleters used brisač, a wiper
SWAP 2 Uses/Used by written the wrong way round — adjacent tab titles,
and nothing mechanical catches a converse pair reversed
GARBLED 1 a Cyrillic о inside "prošireno"
Three no-change decisions are recorded with their counts, because an unrecorded
"left alone" is indistinguishable from "never looked": the audit-trail term keeps
both its genitive and adjectival forms (stylistic variation, not drift), Settings
keeps Postavke against the lone core catalogue, and the -irati/-ovati verb split
is conventionalised per lemma with cross-app corroboration.
CAVEAT ON RECALL: §6.9 prescribes a subagent fan-out for the read and this pass
could not use one, so the read was sequential. On sk a second fan-out found 5
defects a sequential read had missed. Treat this bundle's pre-existing half as
audited once, not twice.
The docs also carry the concurrent be pass's edits, since both passes were in
flight in the same files.
test:l10n:parity now fails for any required locale missing a key, the same way it already did for empty values and wrong plural arity. There is no per-locale gated set and no env override for it. All 36 locales are at parity, so a per-locale exemption list has nothing left to express — and it is the knob someone reaches for to turn a red build green, which is the one move the gate exists to prevent. If an English string lands without translations, the failure is the missing translation: §6.15 has the procedure, and the failure output now points at it. Also drops the progress/backlog reporting branch, which could only ever have reported an empty set, and the `finished` flag on each failure entry. gate-negative-test.js asserted the old "(declared FINISHED)" wording, so it would have gone green on a gate that had stopped naming the broken locale at all. It now asserts the failure names the locale, which is the property that actually matters — a gate that fails without saying which of 36 bundles broke sends the reader through all of them. Re-verified on bs, be, mk and nl: two cognate-enforced, one from the pre-rule set, and it fails and names each. Cognate enforcement is untouched and still opt-in per locale on scripts/l10n/locales/<loc>.json, because 15 locales predate that rule and carry ~375 unreviewed identical values. Reviewing them is §9.2. Verified: parity green over both translation sets, all 36 locales; format and check:specs green; negative test green on four locales.
| * @return {RegExp} Test against a bare filename. | ||
| */ | ||
| function localeFileRe(loc) { | ||
| return new RegExp(`^${loc}(_[A-Za-z]{2,3})?\\.json$`) |
| .replace(/`[^`]*`/g, ' ') | ||
| .replace(/https?:\/\/\S+/g, ' ') | ||
| .replace(/\b[\w.-]+\.(json|js|vue|ods|xlsx|php|md|ya?ml)\b/gi, ' ') | ||
| .replace(/\b[a-z_]+(?:_[a-z_]+)+\b/gi, ' ') // snake_case identifiers |
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ❌ | ||||
| test-l10n-parity | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 528/528 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-20 14:49 UTC
Download the full PDF report from the workflow artifacts.
§7.4 lumped the fourteen literal-`(s)` keys (`register(s)`, `configuration(s)`,
`{days} day(s) left`, …) in with the `{plural}` source hack and told a pass to
ignore both. They are unrelated: in `{plural}` the morphology is interpolated by
the call site, whereas `(s)` is ordinary translatable text inside the key, so a
locale may render it however it likes — including dropping the parenthetical —
and there is nothing to fix in `src/`.
Measured across all 36 bundles, three strategies are in use and all are correct:
own parenthetical (`mk`/`nb` 14 of 14, `da` 13, `sq` 12, `et`/`ro`/`uk` 11), no
parenthetical at all (`bs`/`hu`/`it`, 14 of 14), and keeping `(s)` where `-s` is
the native plural marker (`es` `pt` `fr` `ca` `rm`, plus Dutch `configuratie(s)`
and Latvian `konfigurācija(s)`).
So "the value contains `(s)`" is not an audit signal — it flags the whole
Romance group for writing correct Romance. Intra-locale inconsistency is the
signal that works, and needs no knowledge of the target morphology. It locates
two defects, both in Tier 1 locales and now cross-referenced from §9.2:
- `de` writes `Schema(s)`/`Schema(s) ausgewählt` beside its own
`Konfiguration(en)`, `Objekt(e)`, `Tag(e)`. German pluralises this
`Schemata`/`Schemen`, never `Schemas`.
- `nl` writes `schema(s) geselecteerd` beside `object(en)`, `dag(en)`. Dutch is
`schema's` or `schemata`.
`Dashboard(s)`/`Widget(s)` in `de`/`nl`/`lb`/`da` are not defects by contrast —
those languages keep `-s` on English loanwords. `lv` mixes `(s)`, `(i)`, `(ām)`
and `(us)` across eight keys and needs a case-by-case read.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ❌ | ||||
| test-l10n-parity | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 528/528 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-20 15:29 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The l10n files have been through the wringer.
All 36 non-English locales now carry a real translation for every one of the
2052 keys in
en.js— key-for-key, no gaps, no English fallbacks hiding in thebundles. Along the way a lot of what was already in there turned out to be wrong
rather than missing: wrong-language values, terminology that drifted two ways in
one bundle, buttons written against the locale's own convention, plural arrays
copied between languages that don't share boundaries. Those got audited and fixed
per locale rather than just topped up.
The checks were auditing the wrong file
tests/l10n/check-l10n.jswas checking frontendt()literals againstl10n/en.json— the backend catalogue, read by PHPIL10N, which no frontendcode path ever loads. So it demanded bookkeeping in a file the browser never sees
while
l10n/en.js, the one it actually loads, went unaudited and drifted about700 keys behind
src/. Nothing caught it, because a missing key makesOC.L10Nfall back to the English source string, which renders correctly.The two sets are separate concerns with separate consumers, not two renderings of
one source, and the check now says so. The backend
.jsonset still has noscanner — it would need to walk
lib/for PHP$l->t(), notsrc/.Same file, second bug: an
n()call has two source strings but only one cataloguekey, and it is neither of them — it's the
_singular_::_plural_identifier.The gate required the bare singular, which renders correctly at count 1 and falls
back to English for every other count, so every "3 objects" in all 36 locales
rendered English while the gate stayed green.
Also in here
check-l10n-parity.jsused to holdonly a hand-maintained "finished" list to full parity, with an env override.
With nothing left in progress that list was just a knob for turning a red build
green, so it's gone: missing keys, empty values and wrong plural arity are fatal
for every locale. It covers both sets, keeping them apart where they differ —
plural arity is checked on the frontend set only, since
.jsonplurals use akeyed object shape.
scripts/l10n/— status, worklist,harvest, register detectors, apply-with-gates, selfcheck, runtime check, and the
reading aids (core diff, term drift, spell, script coverage, casing). Plus the
runbook in
docs/l10n-workflow.mdso the next person doesn't rebuild it.