i18n: fail safe on empty strings, normalise German to Sie

D2 -- returnEmptyString: false. i18next defaults it to true, so an empty
translation was returned as valid and rendered as blank UI instead of falling
back to English. Verified safe first: zero empty-string values across all 8
locales, no addResourceBundle or runtime resource injection, no public/locales
for the HTTP backend, and the three t(key, '') call sites resolve against key
families fully populated in en and de.

D3 -- German formality normalised to Sie throughout, 101 strings. There is no
deliberate du island: Sie outnumbered du roughly 6:1 (~390 vs 65 addressed
strings), every namespace with more than ten addressed strings was
Sie-dominant, and the guest gallery plus all public/billing surfaces were
already 100% Sie. Even customer.*, the reported offender, was internally mixed
rather than consistently du. Two detection passes: du-pronouns (now zero) and
du-imperatives without a pronoun (Klicke…, Aktiviere…, Wähle…). Placeholders
verified mechanically unchanged. Left alone: ten 1st-person progress labels
(Lade Benutzer…, Prüfe…, Teste Verbindung…) -- those are label style, not
address, and normalising two of ten would have made it worse.

C7 -- removeUnusedKeys stays false, but the comment now carries measured
evidence instead of an estimate. The honest attempt was made: 61 preserve
globs derived mechanically from all 82 dynamic key templates in src (far more
than the 5 families previously named) plus 17 constant-table prefixes cut
removals from 422 to 158. Two things still block it. 47 of the remainder are
the base form of a plural key that src does pass to t(); i18next tries the
_other suffix first so nothing visibly breaks, but covering them needs a
literal pattern per key and forgetting one silently deletes a live key --
exactly the failure the flag prevents. And pruning is not idempotent: run for
real, extract had to run three times before --ci --dry-run came back clean,
each pass uncovering another removal, so i18n:ci would fail on a correct tree
until someone ran extract enough times.

Also adds the three settings.analytics keys that 9251745a referenced in
AnalyticsTab without adding (proxiedNotice, proxiedNoticeText,
customOnlyCspWarningText) -- en from the source defaults, de translated.

Refs testplan REPORT.md C7, D2, D3.
This commit is contained in:
Paul Nothaft
2026-09-02 09:43:10 +02:00
parent 758dc005df
commit 413290af3e
4 changed files with 162 additions and 121 deletions
+25 -6
View File
@@ -24,12 +24,31 @@ export default defineConfig({
primaryLanguage: 'en',
// Pruning is unsafe in this codebase: a large share of keys is never visible to the
// AST extractor because it is built at runtime — t(`admin.activities.${type}`),
// t(`admin.notificationMessages.${type}`), t(`projects.status.${status}`) — or held in
// constant tables the extractor does not resolve (AdminSidebar `nameKey`,
// CrmDevelopmentPage `titleKey`/`descKey`, the crmSettings toggle map). Enabling it
// deletes ~355 live keys per locale, so removal stays a manual decision.
// Pruning is unsafe in this codebase. A large share of keys is never visible to the
// AST extractor because it is built at runtime — 82 distinct dynamic key templates in
// src, e.g. admin.activities, admin.notificationMessages, projects.status,
// accounting.expenseStatus, crmSettings — or held in constant tables the extractor does
// not resolve (AdminSidebar `nameKey`, CrmDevelopmentPage `titleKey`/`descKey`).
// Turned on as-is, it deletes 422 keys per locale, 203 of which src references.
//
// A `preservePatterns` rescue was attempted and measured: 61 globs derived
// mechanically from every dynamic template in src, plus 17 for the constant-table
// prefixes (78 patterns). That still leaves two unfixable problems:
//
// 1. 47 of the remaining 158 removals are the base form of a plural key
// (`upload.failures.title` next to `_one`/`_other`). src passes exactly those
// strings to t(), so they are referenced by any honest definition. i18next happens
// to resolve them via the `_other` suffix first, so nothing breaks today — but
// covering them needs 47 literal patterns and one more on every future {{count}}
// key, where forgetting one silently deletes a live key. That is the exact failure
// mode this flag is supposed to prevent.
// 2. Pruning is not idempotent. `extract` had to be run three times in a row before
// `extract --ci --dry-run` came back clean — each pass uncovered one further
// removal (`businessProfile.title`, then `cssTemplates.title`). i18n:ci would
// therefore fail on a correct tree until someone happened to run extract enough
// times. With pruning off the extractor settles in a single pass.
//
// Key removal stays a manual decision.
removeUnusedKeys: false,
preserveContextVariants: true,