fix(branding): brand the fork's own wording across every locale - #957
Merged
Conversation
…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.
…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
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.
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.
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_i18ncan 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_GBis the case that surfaced it — a partial Weblate locale (2,278 of en's 3,123 keys) resolved on top ofen, 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. Notablyversion_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 fixesar, whoseشراء immichwas lowercase and so never caught by the case-sensitive rebrand.4.
my.immich.app—getMyImmichLink()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 onpage.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:
en_GBmust equal brandedenunless waived as a UK variant;FORK_VOICE_KEYSis derived from the files and fails if the list misses one, then every shipped locale must translate each;Each was watched failing first (12, 55 and 94 failures respectively) before the fix.
gallery-branding-check.sh(the CI job): passCaveats
immichleaks survive branding, includingpl: welcome_to_immich= "Witamy w immich". The rebrand is case-sensitive by design and the leak scan greps capital-I only.version_announcement_closingstill says "Your friend, Alex" in the other 82 locales.