Skip to content

fix(branding): brand the fork's own wording across every locale - #957

Merged
Deeds67 merged 4 commits into
mainfrom
fix/branding-en-gb-overrides
Aug 7, 2026
Merged

fix(branding): brand the fork's own wording across every locale#957
Deeds67 merged 4 commits into
mainfrom
fix/branding-en-gb-overrides

Conversation

@Deeds67

@Deeds67 Deeds67 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Found while verifying an RC build on the personal instance: the sidebar CTA read "Buy Noodle Gallery".

Branding had applied correctly — this was a locale coverage gap, and every existing leak scan was blind to it because the offending strings contain no upstream name.

Root cause

patch_i18n can only rename. Some overrides don't rename Immich, they say something different from it: the fork sells nothing, is written by someone else, and has no link proxy. Any locale without its own override therefore kept saying upstream's thing under our brand.

en_GB is the case that surfaced it — a partial Weblate locale (2,278 of en's 3,123 keys) resolved on top of en, so the keys it omits were always fine and the keys it defines carried upstream's wording.

Changes

1. en_GB — 7 keys. Six genuinely dialectal ones ("bin", "optimisations", "customised") are left to Weblate.

2. Six shipped locales (de, fr, nl, it, es, pl) — new overrides-{nl,it,es,pl}.json, gaps filled in de/fr, covering the 10 keys where the fork's English changed meaning rather than name. Notably version_announcement_closing, which signed release notes "Your friend, Alex" in all six.

3. All 89 locales, CTA only — 50 locales offered a purchase that doesn't exist. Each value is that locale's own Weblate translation of purchase_panel_title ("Support the project") with the object replaced by the brand name, so the verb and register come from a human translator in this same panel. Overrides are written only for keys a locale actually defines, so nothing loses its English fallback. Incidentally fixes ar, whose شراء immich was lowercase and so never caught by the case-sensitive rebrand.

4. my.immich.appgetMyImmichLink() hardcoded a third-party proxy that redirects a visitor to whichever instance they configured there. A Noodle Gallery user has configured nothing, so the copied link landed on an Immich-branded setup page and named immich. Now builds on page.url.origin. Function and i18n key names stay as upstream has them to hold the rebase diff to one line.

Tests

Three structural invariants, not spot checks:

  • for every override key, branded en_GB must equal branded en unless waived as a UK variant;
  • FORK_VOICE_KEYS is derived from the files and fails if the list misses one, then every shipped locale must translate each;
  • across all 89 locales, no locale may render a CTA that isn't served by an override.

Each was watched failing first (12, 55 and 94 failures respectively) before the fix.

  • i18n branding test: pass
  • Umbrella gate gallery-branding-check.sh (the CI job): pass
  • Full web unit suite: 4331 passed
  • shellcheck clean, 58 override files valid JSON

Caveats

  • nl/it/es/pl and the 50 CTA translations have had no native-speaker review. Where the brand slot needed inflection the name is left uninflected; six locales (gsw, is, kn, mn, ms, sq, yue_Hant) had no source phrase and follow the nearest related language.
  • Out of scope, found while auditing: 14 user-facing lowercase immich leaks survive branding, including pl: welcome_to_immich = "Witamy w immich". The rebrand is case-sensitive by design and the leak scan greps capital-I only.
  • version_announcement_closing still says "Your friend, Alex" in the other 82 locales.

Deeds67 added 3 commits August 7, 2026 08:51
…r name

en_GB is a partial Weblate locale (2,278 of en's 3,123 keys) that svelte-i18n
resolves on top of en, so every key it omits already rendered branded English.
The keys it does define came from upstream and only ever had the brand name
swapped by patch_i18n step 3 — so the sidebar CTA read "Buy Noodle Gallery"
instead of "Support Noodle Gallery", and the release note signed off "Your
friend, Alex". Both are branded and grammatical, so every existing leak scan
was blind to them: neither string contains the upstream name.

Add branding/i18n/overrides-en_GB.json for the seven keys carrying upstream
wording, and leave the six genuinely dialectal ones ("bin", "optimisations",
"customised") to Weblate.

The regression test now asserts a structural invariant rather than these two
strings: for every key in overrides-en.json, branded en_GB must equal branded
en unless the key is on an explicit UK-variant waiver list. A Weblate sync that
retranslates one of them fails the test instead of shipping.

Found on the personal instance while verifying an RC build.
…cale

Some overrides don't rename Immich, they say something different from it: the
fork sells nothing, is written by someone else, and has no link proxy. patch_i18n
step 3 can only rename, so any locale without its own override kept saying
upstream's thing under our brand — "Kup Noodle Gallery", "Il tuo amico, Alex",
"Noodle Gallery Photo Server <noreply@example.com>".

Add overrides-{nl,it,es,pl}.json and fill the gaps in de/fr, covering the ten
keys where the fork's English changed the meaning rather than the name.

Also point the Ctrl+Shift+M link at the instance the user is on. It was
hardcoded to https://my.immich.app, a proxy that redirects a visitor to whichever
instance THEY configured there — so for a Noodle Gallery user the copied link
landed on an Immich-branded setup page, and the link they shared named immich.
Every user here has exactly one instance, so link to it directly. The keys keep
their upstream names to hold the rebase diff to a single line.

The test derives which overrides changed meaning and fails if FORK_VOICE_KEYS
misses one, then requires every shipped locale to translate each of them. Adding
a semantic override without translating it now fails CI.
The CTA is the one string where upstream's meaning is not merely off-brand but
false: the fork sells nothing, so every locale that carried its own translation
of "Buy Immich" offered a purchase that does not exist — "Kup Noodle Gallery",
"Comprar Noodle Gallery", "Immichを購入" rebranded. 50 locales were affected;
the other 39 never define the key and already inherited the branded English
through svelte-i18n's fallbackLocale.

Each value is that locale's own Weblate translation of purchase_panel_title
("Support the project") with the object replaced by the brand name, so the verb
and register come from a human translator working in this same panel rather than
from me. Six locales have no such translation (gsw, is, kn, mn, ms, sq,
yue_Hant); those follow the nearest related locale. Where the brand slot would
need inflection the name is left uninflected, as UIs conventionally do.

Overrides are written only for keys a locale actually defines, so no locale
loses its English fallback for a key it never translated.

Unlike the fork-voice keys, the test enforces this across ALL 89 locales: a
wrong CTA is worse than an untranslated one, and a Weblate sync that reintroduces
a purchase verb now fails CI.

Incidentally fixes ar, whose "شراء immich" spelled the name lowercase and so
was never caught by the case-sensitive rebrand.
@Deeds67 Deeds67 added the changelog:fix Bug fix for changelog label Aug 7, 2026
…ifiers

The rebrand matched only the capitalised name so that docs.immich.app and the
app.immich:// scheme would survive it. The cost was that a translator who
lowercased the brand in prose leaked it, in 14 user-facing strings across 9
locales — including "Witamy w immich" on the Polish welcome screen. Every leak
scan was blind to them too: they grep the capitalised form.

Shield the real identifiers first, then the bare lowercase mention can be
rewritten with no exception carved out for it. Sentinels are private-use code
points, which cannot occur in a translation, so shielding can never collide with
real text; the replacement stays a literal split/join, so introducing this could
not reintroduce the regex hazard that idiom exists to avoid.

Georgian falls out correctly: "immich-ის" rebrands to "Noodle Gallery-ის",
because a case suffix is not an identifier and nothing shields it. Verified
tiles.immich.cloud (55), smtp.immich.app (56) and <noreply@immich.app> survive
byte-for-byte.

The identifier list is a shared global the test sources, and the test asserts
over the SHIELDED text — so a Weblate sync that introduces a new identifier form
fails loudly instead of being quietly rewritten into a hostname that never
existed. That is also why the Tamil typo diles.immich.cloud is shielded rather
than corrected.
@Deeds67
Deeds67 merged commit eb41aec into main Aug 7, 2026
45 checks passed
Deeds67 added a commit that referenced this pull request Aug 7, 2026
…958)

The in-app release announcement is signed by a person, not the product, so
the brand swap in patch_i18n could never fix it: upstream signs it "Your
friend, Alex" and a rename only ever produces "Your friend, Alex" again.
PR #957 hand-translated the sign-off for the seven locales the product
ships in; the other 47 that define the key still named upstream's author,
in 15 different transliterations ("Твой друг Алекс", "صديقك، أليكس",
"당신의 친구, Alex가").

Swap it at build time instead of enumerating locales. config.json gains
the fork's author name and every known spelling of upstream's, ordered so
no entry is a substring of a later one; swap_author_name() applies them
with a literal split/join, scoped to the sign-off key alone — the name
appears in no other key in any of the 89 locales, so the narrow scope
costs nothing and keeps a token as short as "Alex" from reaching an
unrelated translation. It runs after the override merge, so the seven
hand-written translations still win and this is a no-op there.

The 34 locales that never define the key already resolve to the branded
en.json and are untouched.

Keeping the name in Latin across every script matches how the fork
already leaves "Noodle Gallery" in Latin inside Japanese and Arabic
strings, and avoids shipping 15 unproofread spellings of a real name.

Afrikaans gets an override rather than the swap: upstream's string is
half-untranslated ("Jou friend, Alex"), so a name swap alone would leave
"Jou friend, Pierre".

The substitution can only cover spellings it knows, and upstream gains
locales continuously, so verify-branding.sh now asserts the OUTCOME —
every locale defining the key must name the fork's author. A future
transliteration the list has never seen survives the swap without that
name and fails the build instead of shipping. test-i18n-branding.sh
covers each substitution class, the ordering invariant, and a fixture
proving the swap cannot escape its key.
Deeds67 added a commit that referenced this pull request Aug 10, 2026
* fix(branding): brand en_GB, which shipped upstream's wording under our name

en_GB is a partial Weblate locale (2,278 of en's 3,123 keys) that svelte-i18n
resolves on top of en, so every key it omits already rendered branded English.
The keys it does define came from upstream and only ever had the brand name
swapped by patch_i18n step 3 — so the sidebar CTA read "Buy Noodle Gallery"
instead of "Support Noodle Gallery", and the release note signed off "Your
friend, Alex". Both are branded and grammatical, so every existing leak scan
was blind to them: neither string contains the upstream name.

Add branding/i18n/overrides-en_GB.json for the seven keys carrying upstream
wording, and leave the six genuinely dialectal ones ("bin", "optimisations",
"customised") to Weblate.

The regression test now asserts a structural invariant rather than these two
strings: for every key in overrides-en.json, branded en_GB must equal branded
en unless the key is on an explicit UK-variant waiver list. A Weblate sync that
retranslates one of them fails the test instead of shipping.

Found on the personal instance while verifying an RC build.

* fix(branding): translate the fork's own wording into every shipped locale

Some overrides don't rename Immich, they say something different from it: the
fork sells nothing, is written by someone else, and has no link proxy. patch_i18n
step 3 can only rename, so any locale without its own override kept saying
upstream's thing under our brand — "Kup Noodle Gallery", "Il tuo amico, Alex",
"Noodle Gallery Photo Server <noreply@example.com>".

Add overrides-{nl,it,es,pl}.json and fill the gaps in de/fr, covering the ten
keys where the fork's English changed the meaning rather than the name.

Also point the Ctrl+Shift+M link at the instance the user is on. It was
hardcoded to https://my.immich.app, a proxy that redirects a visitor to whichever
instance THEY configured there — so for a Noodle Gallery user the copied link
landed on an Immich-branded setup page, and the link they shared named immich.
Every user here has exactly one instance, so link to it directly. The keys keep
their upstream names to hold the rebase diff to a single line.

The test derives which overrides changed meaning and fails if FORK_VOICE_KEYS
misses one, then requires every shipped locale to translate each of them. Adding
a semantic override without translating it now fails CI.

* fix(branding): stop offering to sell the fork, in all 89 locales

The CTA is the one string where upstream's meaning is not merely off-brand but
false: the fork sells nothing, so every locale that carried its own translation
of "Buy Immich" offered a purchase that does not exist — "Kup Noodle Gallery",
"Comprar Noodle Gallery", "Immichを購入" rebranded. 50 locales were affected;
the other 39 never define the key and already inherited the branded English
through svelte-i18n's fallbackLocale.

Each value is that locale's own Weblate translation of purchase_panel_title
("Support the project") with the object replaced by the brand name, so the verb
and register come from a human translator working in this same panel rather than
from me. Six locales have no such translation (gsw, is, kn, mn, ms, sq,
yue_Hant); those follow the nearest related locale. Where the brand slot would
need inflection the name is left uninflected, as UIs conventionally do.

Overrides are written only for keys a locale actually defines, so no locale
loses its English fallback for a key it never translated.

Unlike the fork-voice keys, the test enforces this across ALL 89 locales: a
wrong CTA is worse than an untranslated one, and a Weblate sync that reintroduces
a purchase verb now fails CI.

Incidentally fixes ar, whose "شراء immich" spelled the name lowercase and so
was never caught by the case-sensitive rebrand.

* fix(branding): rebrand lowercase mentions without breaking real identifiers

The rebrand matched only the capitalised name so that docs.immich.app and the
app.immich:// scheme would survive it. The cost was that a translator who
lowercased the brand in prose leaked it, in 14 user-facing strings across 9
locales — including "Witamy w immich" on the Polish welcome screen. Every leak
scan was blind to them too: they grep the capitalised form.

Shield the real identifiers first, then the bare lowercase mention can be
rewritten with no exception carved out for it. Sentinels are private-use code
points, which cannot occur in a translation, so shielding can never collide with
real text; the replacement stays a literal split/join, so introducing this could
not reintroduce the regex hazard that idiom exists to avoid.

Georgian falls out correctly: "immich-ის" rebrands to "Noodle Gallery-ის",
because a case suffix is not an identifier and nothing shields it. Verified
tiles.immich.cloud (55), smtp.immich.app (56) and <noreply@immich.app> survive
byte-for-byte.

The identifier list is a shared global the test sources, and the test asserts
over the SHIELDED text — so a Weblate sync that introduces a new identifier form
fails loudly instead of being quietly rewritten into a hostname that never
existed. That is also why the Tamil typo diles.immich.cloud is shielded rather
than corrected.
Deeds67 added a commit that referenced this pull request Aug 10, 2026
…958)

The in-app release announcement is signed by a person, not the product, so
the brand swap in patch_i18n could never fix it: upstream signs it "Your
friend, Alex" and a rename only ever produces "Your friend, Alex" again.
PR #957 hand-translated the sign-off for the seven locales the product
ships in; the other 47 that define the key still named upstream's author,
in 15 different transliterations ("Твой друг Алекс", "صديقك، أليكس",
"당신의 친구, Alex가").

Swap it at build time instead of enumerating locales. config.json gains
the fork's author name and every known spelling of upstream's, ordered so
no entry is a substring of a later one; swap_author_name() applies them
with a literal split/join, scoped to the sign-off key alone — the name
appears in no other key in any of the 89 locales, so the narrow scope
costs nothing and keeps a token as short as "Alex" from reaching an
unrelated translation. It runs after the override merge, so the seven
hand-written translations still win and this is a no-op there.

The 34 locales that never define the key already resolve to the branded
en.json and are untouched.

Keeping the name in Latin across every script matches how the fork
already leaves "Noodle Gallery" in Latin inside Japanese and Arabic
strings, and avoids shipping 15 unproofread spellings of a real name.

Afrikaans gets an override rather than the swap: upstream's string is
half-untranslated ("Jou friend, Alex"), so a name swap alone would leave
"Jou friend, Pierre".

The substitution can only cover spellings it knows, and upstream gains
locales continuously, so verify-branding.sh now asserts the OUTCOME —
every locale defining the key must name the fork's author. A future
transliteration the list has never seen survives the swap without that
name and fails the build instead of shipping. test-i18n-branding.sh
covers each substitution class, the ordering invariant, and a fixture
proving the swap cannot escape its key.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:fix Bug fix for changelog 🖥️web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant