Release: merge development into beta - #3090
Merged
Merged
Conversation
The 1.1.10-beta.20260829221810 release bumped the version on beta. Without this, development stays behind beta and the next development -> beta promotion conflicts on the version file. Version files resolve to development's side, which is the higher line, so this never moves a version backwards.
Supersedes #2993, which could not be updated in place (dependabot branch). What changed beyond the version bump: - `patches.lock.json` is new in v2 and is committed alongside composer.lock. It pins each patch by sha256, so a silently-edited patch file is now a lock mismatch rather than a quiet re-apply. - `extra.composer-exit-on-patch-failure` is removed. It is v1-only config; v2 has no such setting because it always throws. Verified rather than assumed: with a deliberately broken hunk, `composer install` exits 1 from Patcher/Patches.php:288 ("No available patcher was able to apply patch"). Behaviour is unchanged — the flag was already what v2 does unconditionally. The three existing patches all apply cleanly under v2. This matters: v2 drops the GNU `patch` backend and applies via `git apply`, which honours offsets but NOT fuzz. A patch that had been applying with fuzz under v1 would have started failing here. None did, so the current patch set is exact against its pins. Note for anyone building outside CI: v2 needs `git` on PATH (GitPatcher / GitInitPatcher); v1 only needed `patch`. GitInitPatcher creates a temporary `.git` inside the package dir and removes it again — verified 0 leftover .git directories under vendor/ after a clean install. Local verification (real exit codes, base development @8a5eef382): - composer install from an empty vendor/: exit 0, all 3 patches applied and their content verified present in vendor/ - composer install --no-dev --dry-run: exit 0 - lint / phpcs / phpmd / psalm / phpstan: all pass - phpunit: 17774 tests, 0 failures, 0 errors check:strict exits 1 both here and on clean development, for the same local-only reason: no coverage driver is installed, so PHPUnit reports "OK, but there were issues!". Identical warning/risky profile to the baseline (1 runner warning, 10 warnings, 43 skipped, 3 risky).
* chore(deps): bump theodo-group/llphant from 0.9.12 to 1.0.1 Supersedes #2986, which could not be updated in place (dependabot branch). Root cause of #2986's 14 red checks: exactly one of them was real, and it killed the other thirteen. `patches/llphant-ollama-usage-capture.patch` does not apply to 1.0.1, and with `composer-exit-on-patch-failure: true` that aborts `composer install` — so vendor/ never existed and every PHP job (lint, phpcs, phpmd, psalm, phpstan, phpmetrics, phpunit, license, security) died on a missing autoloader rather than on anything it was meant to measure. Both patches are re-anchored against 1.0.1: - think/keep_alive: context unchanged, hunk moved 257 -> 269. - usage-capture: hunk 1 failed because the constructor signature changed (`?LoggerInterface $logger = null` -> `private readonly LoggerInterface $logger = new NullLogger()`). Hunk 2 moved into the new `generateChatRecursive()` and gained a `$this->logger->debug($contents)` line — under GNU patch it was applying with **fuzz 1**, which is not a pass. The new hunk is exact. It is anchored on the `/** @var Message[] $toolsOutput */` line below it, because 1.0.1 has two identical `decodeJson` sites and only the recursive one should accumulate usage. psalm-baseline.xml loses one entry rather than gaining any: `OllamaConfig` now really declares `$apiKey`, so the baselined UndefinedPropertyFetch in ConversationManagementHandler is stale and psalm fails on UnusedBaselineEntry until it is removed. The upgrade retires a suppression. Nothing else in the app needed changing — with the patch deliberately left off, psalm and phpstan each reported exactly one error, both of them the missing $lastUsage property. The renamed ChatInterface methods (generateTextOrReturnFunctionCalled -> ...ToCall) are not called anywhere here. Transitive: openai-php/client v0.13.0 -> v0.19.2, guzzle 7.15.2 -> 7.15.5, new yethee/tiktoken 0.10.0. All MIT; `composer audit --locked` exit 0. Nothing in lib/ or tests/ touches the OpenAI SDK directly. Local verification (real exit codes, base development @0131ef736): - composer install from an empty vendor/: exit 0, all 3 patches applied and their content verified present in vendor/ - lint / phpcs / phpmd / phpstan: pass - psalm: exit 0, "No errors found!" - phpunit: 17775 tests, 0 failures, 0 errors check:strict exits 1 both here and on clean development for the same local-only reason: no coverage driver, so PHPUnit ends on "OK, but there were issues!". Identical warning/risky profile to the baseline. * chore(deps): relock the patch hashes after the development merge composer-patches v2 (#3086) records a sha256 per patch in patches.lock.json. This branch re-anchored both llphant patches for 1.0.1, so their content changed — and merging development brought in the lock carrying the OLD hashes. CI then failed with 'Hash mismatch for patch downloaded from patches/llphant-ollama-think-keepalive.patch', which reads like a corrupt download and is really a stale lock. Regenerated with the plugin's own 'composer patches-relock' rather than by editing the hashes by hand: the file also carries a top-level _hash whose derivation is the plugin's business, not something to reproduce from the outside. Diff is three lines — the two patch hashes and that _hash — and a recomputation check now finds zero stale entries. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Contributor
Author
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 174/174 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 22:55 UTC
Download the full PDF report from the workflow artifacts.
…tionContext (#3095) Nextcloud added IRegistrationContext::registerGlobalScaleService (@SInCE 34.0.3) and backported it onto stable33. Our test helper is a concrete implementation of that interface, so the new method turned it into an implicitly-abstract class and PHPUnit aborted with a fatal error before running a single test: PHP Fatal error: Class ...\RecordingRegistrationContext contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (IRegistrationContext::registerGlobalScaleService) That is why development went red with no change of ours: the PR that merged was green, and the stable33 branch tip moved underneath it. Both failing cells were NC stable33, exit code 255 on "Run PHPUnit tests". Adds the no-op recorder method, matching the other 37. Verified the helper now covers the full interface surface on stable33 (38 methods), stable34 (38) and master (39), so no further drift is pending.
Contributor
Author
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 174/174 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 23:56 UTC
Download the full PDF report from the workflow artifacts.
…at full parity (#2634) * l10n: sort all frontend bundles alphabetically 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. * l10n(nl): fix 10 broken and formal-address entries 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. * l10n(de): complete German frontend translation (1996/1996 keys) 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. * l10n(fr): complete French frontend translation (1996/1996 keys) 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. * l10n: drop 875 dead placeholder entries from 32 locale bundles 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 03cda6c27 ("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. * l10n(nl,de,fr): translate 15 dynamically-keyed strings that static extraction 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. * l10n(es): complete Spanish frontend translation (2011/2011 keys) 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. * l10n(it): complete Italian frontend translation (2011/2011 keys) 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. * l10n(pt): complete Portuguese frontend translation (2011/2011 keys) 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. * l10n(sv): complete Swedish frontend translation (2011/2011 keys) 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. * l10n(da): complete Danish frontend translation (2011/2011 keys) 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". * l10n(nb): complete Norwegian Bokmål frontend translation (2011/2011 keys) 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. * l10n(pl): complete Polish frontend translation (2011/2011 keys) 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". * l10n(cs): complete Czech frontend translation (2011/2011 keys) 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. * l10n(ru): complete Russian frontend translation (2011/2011 keys) 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. * l10n(nl,cs): fix two mistranslations found during the Russian review 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. * l10n: add frontend l10n tooling (CRUD, audit, unwrapped-string detector) 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. * l10n: gate English-identical values and plural arity, wire tooling to 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. * fix(l10n): decode \uXXXX and \xXX escapes when extracting keys 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}. * l10n: translate the Ukrainian frontend catalogue (uk) 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. * fix(l10n): stop reporting n() plural sources as missing keys 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. * fix(i18n): wrap unwrapped UI prose in ViewObject and MassValidateModal 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 } }). * fix(l10n): protect variable-keyed strings from the unused-key sweep 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. * l10n: rebuild en.js from source and full-sync the 13 finished locales 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. * l10n: translate the Greek frontend catalogue (el) 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. * l10n: translate the Finnish frontend catalogue (fi) 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. * l10n: translate the 7 CnAppNav menu-group labels 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. * l10n: stop translating a technical literal, fix the Dutch file plural 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. * l10n: translate the Hungarian frontend catalogue (hu) 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…
….1.10-beta.20260829221810 chore(release): sync beta back into development
Contributor
Author
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ❌ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-30 08:37 UTC
Download the full PDF report from the workflow artifacts.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Contributor
Author
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 | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 174/174 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-30 09:11 UTC
Download the full PDF report from the workflow artifacts.
Dexie refuses to run twice in one page: it throws "Two different versions of Dexie loaded in the same app". Nextcloud loads openregister's global integration script and hermiq's agent leaf on every page, alongside whichever leaf app you are in, so all three have to agree on one dexie. After the dependabot sweep on 2026-08-30 they did not. openregister resolved 4.4.4 while hermiq resolved 4.4.5, and the throw happened before the leaf app mounted, so every app page rendered as bare Nextcloud chrome with no content. This pins the floor at ^4.4.5 and regenerates the lock, matching the apps that were already there. Verified in the browser: the Dexie error is gone from the console and app pages render their navigation and content again.
Contributor
Author
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 | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 174/174 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-30 10:00 UTC
Download the full PDF report from the workflow artifacts.
Contributor
Author
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 | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 174/174 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-30 10:33 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.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.