Skip to content

Fix for issue #875: Custom translations for one language showing for other languages#876

Open
BrDewit wants to merge 5 commits intokeycloakify:mainfrom
BrDewit:fix-875-custom-translation-bug
Open

Fix for issue #875: Custom translations for one language showing for other languages#876
BrDewit wants to merge 5 commits intokeycloakify:mainfrom
BrDewit:fix-875-custom-translation-bug

Conversation

@BrDewit
Copy link
Copy Markdown

@BrDewit BrDewit commented Jul 3, 2025

In issue #875 I outlined an issue where creating a custom translation for one language, caused all other languages to display that custom translation, instead of only the specified language displaying the custom translation.

The cause of the issue was due to the fact that the code fell back on the custom translations of other languages when creating the translation function of a specific language. If there was no custom translation for the language at all, for any key, the custom translation of either the default language, or if that didn't exist, the first specified custom translation, was used.
If any custom translation existed for a certain language, the issue would not occur as the messagesByLanguageTag_themeDefined[currentLanguage.languageTag] call on line 223 of the getI18n.tsx file does not return undefined in this case, and it would not fall back on another language.

I've removed the fallback to other languages in this part of the code, as the custom translation is supposed to only be applied to the language it's specified for. The resolveMsg and resolveMsgAdvanced functions handle falling back to other languages if a certain key does not exist.

I've tested the effects of the changes manually by testing multiple configurations of different languages and translations, but I've not implemented an automated test.

The change is only breaking if the user of the library used a custom translation of an existing key in 1 language to translate that same key across multiple/all other languages.

Summary by CodeRabbit

  • New Features

    • Improved translation fallback on the login screens: theme-specific text now falls back to a secondary language when missing, reducing missing or incorrect text for users.
  • Refactor

    • Streamlined message resolution to more reliably select theme and fallback translations.
  • Bug Fixes

    • Fewer missing-translation issues and more consistent localized UI.

…tions to be shown for languages that don't have a custom translation for a certain key
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jul 3, 2025

Walkthrough

Adds a two-tier fallback resolution for theme-defined translations: look up theme-defined messages for the current language, then the default message set for current language, then theme-defined for a fallback language, then the default set for the fallback language; introduces helper getThemeDefinedMessage and a new messages_themeDefined_fallback parameter.

Changes

Cohort / File(s) Change Summary
i18n — getI18n implementation
src/login/i18n/noJsx/getI18n.tsx
Adds messages_themeDefined_fallback parameter, introduces getThemeDefinedMessage helper, and changes lookup order to a two-tier fallback (current language theme-defined → current defaults → fallback theme-defined → fallback defaults).

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant getI18n
  participant messages_fromKcServer
  participant messages_themeDefined_current
  participant messages_default_current
  participant messages_themeDefined_fallback
  participant messages_default_fallback

  Caller->>getI18n: request translation(key, themeName, currentLang, fallbackLang?)
  getI18n->>messages_fromKcServer: lookup(key)
  alt found in messages_fromKcServer
    messages_fromKcServer-->>getI18n: value
    getI18n-->>Caller: value
  else not found
    getI18n->>messages_themeDefined_current: getThemeDefinedMessage(key, currentLang, themeName)
    alt found in theme-defined current
      messages_themeDefined_current-->>getI18n: value (string or themed map -> selected by themeName)
      getI18n-->>Caller: value
    else
      getI18n->>messages_default_current: lookup(key, currentLang)
      alt found in default current
        messages_default_current-->>getI18n: value
        getI18n-->>Caller: value
      else
        getI18n->>messages_themeDefined_fallback: getThemeDefinedMessage(key, fallbackLang, themeName)
        alt found in theme-defined fallback
          messages_themeDefined_fallback-->>getI18n: value
          getI18n-->>Caller: value
        else
          messages_default_fallback-->>getI18n: lookup(key, fallbackLang)
          getI18n-->>Caller: value
        end
      end
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I hop through keys and language trees,
Two-tiered paths now sway the breeze.
If current theme lacks the song,
A gentle fallback hums along.
— Rabbit 🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@garronej
Copy link
Copy Markdown
Collaborator

garronej commented Jul 4, 2025

Hello @BrDewit,

Thank you for the well detailed PR.

That being said I think this changes, while fixing the issue of overwriting the curstom translation of an existing key in only a specific language this change also cause important regression when defining custom message key.

.withCustomTranslations({
    en: {
        myCustomMessage: "Hello world"
    }
})

With this PR applied, if the current language is en, msgStr("myCustomMessage") gives "Hello World" but if the current language is, let's say, fr then it gives "myCustomMessage".

Defining custom, non standard message keys is the primary usecase of withCustomTranslations.

What would be a reasonable approach is, if the key is a known one then do what you suggest otherwise keep the current behavior....

But even then, you usually never need to overwrite one of the default message keys, in your case you do just because the translation file are outdated and I have to update them.
But in the general case, if you have overwriten a default message it's probably because you've altered the sementic and want this new semantic to be always visible, even in the wrong language.

This is debatable though but as it stands the rule of withCustomTranslations is. If you want to use it, you must provide a translation for every language you wish to support or we will fall back to english or the first one.

@BrDewit
Copy link
Copy Markdown
Author

BrDewit commented Jul 4, 2025

Hi @garronej

That's a very reasonable remark, if they did not specify the custom message, you would indeed want to fall back to an existing translation as that's better than just having the key inserted in that position. Especially as that's probably a more common case than the one where a custom translation is given for an existing key. When I have time, I'll revise the changes to fix this regression.

… translations to be shown for languages that don't have a custom translation for a certain key"

This reverts commit 252cf5e.
…s in a provided language from custom translations in the fallback language, and resolve to these sets in different places of the translation resolution logic
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/login/i18n/noJsx/getI18n.tsx (1)

317-326: Fix fallback order to avoid cross-language custom translations for default keys (even before async load completes)

As written, when the current language is not the fallback language and before messages_defaultSet_currentLanguage is loaded, known/default keys will fall back to messages_themeDefined_fallback (custom translations from another language) instead of the default set fallback. This reintroduces the regression the PR aims to fix during the preload window.

Branch resolution based on whether the key is a known default key:

  • For default keys: never fall back to theme-defined messages from another language; fall back to the default set instead.
  • For unknown/custom keys: preserve cross-language theme-defined fallback.

Apply this diff inside resolveMsg:

-            const message =
-                id<Record<string, string | undefined>>(messages_fromKcServer)[key] ??
-                getThemeDefinedMessage(messages_themeDefined, key) ??
-                id<Record<string, string | undefined> | undefined>(messages_defaultSet_currentLanguage)?.[key] ??
-                getThemeDefinedMessage(messages_themeDefined_fallback, key) ??
-                id<Record<string, string | undefined>>(messages_defaultSet_fallbackLanguage)[key];
+            const isKnownDefaultKey = Object.prototype.hasOwnProperty.call(
+                id<Record<string, string>>(messages_defaultSet_fallbackLanguage),
+                key
+            );
+
+            const message = isKnownDefaultKey
+                ? (
+                    id<Record<string, string | undefined>>(messages_fromKcServer)[key] ??
+                    getThemeDefinedMessage(messages_themeDefined, key) ??
+                    id<Record<string, string | undefined> | undefined>(messages_defaultSet_currentLanguage)?.[key] ??
+                    id<Record<string, string | undefined>>(messages_defaultSet_fallbackLanguage)[key]
+                )
+                : (
+                    id<Record<string, string | undefined>>(messages_fromKcServer)[key] ??
+                    getThemeDefinedMessage(messages_themeDefined, key) ??
+                    getThemeDefinedMessage(messages_themeDefined_fallback, key)
+                );

This preserves the intended behavior for both categories of keys and eliminates the transient regression before the async default-set load completes.

🧹 Nitpick comments (3)
src/login/i18n/noJsx/getI18n.tsx (3)

222-231: Clarify and harden selection of messages_themeDefined_fallback (minor determinism nit)

Using the first key from messagesByLanguageTag_themeDefined as a tertiary fallback can be order-dependent. It’s fine, but to make the behavior fully deterministic and self-documenting, prefer trying known language tags in a stable order before falling back to “first available”.

             messages_themeDefined_fallback:
-                messagesByLanguageTag_themeDefined[id<string>(FALLBACK_LANGUAGE_TAG) as LanguageTag] ??
-                (() => {
-                    const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0];
-                    if (firstLanguageTag === undefined) {
-                        return undefined;
-                    }
-                    return messagesByLanguageTag_themeDefined[firstLanguageTag as LanguageTag];
-                })(),
+                messagesByLanguageTag_themeDefined[id<string>(FALLBACK_LANGUAGE_TAG) as LanguageTag] ??
+                // Try default-set languages in declared order for a stable pick:
+                (id<readonly string[]>(languageTags_defaultSet)
+                    .map(tag => messagesByLanguageTag_themeDefined[tag as LanguageTag])
+                    .find(Boolean) as typeof messages_themeDefined | undefined) ??
+                // Finally, first available user-provided language (if any):
+                Object.values(messagesByLanguageTag_themeDefined)[0],

355-372: Helper extraction LGTM; optional: consider soft-failing when theme variant missing

The helper is clean and typed appropriately. The assert on missing themeName in a map is strict; if you prefer graceful degradation, you could return undefined here to continue fallback rather than throwing. Keep as-is if the assert is intentional.


220-233: Add tests for the two key scenarios

Please add minimal tests to lock the intended behavior:

  • Known/default key with custom translation only in another language: never fall back to that custom translation (should use default-set).
  • Unknown/custom key with custom translation only in another language: do fall back to that custom translation.

I can draft these tests if you want.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 252cf5e and 6d04567.

📒 Files selected for processing (1)
  • src/login/i18n/noJsx/getI18n.tsx (4 hunks)
🔇 Additional comments (1)
src/login/i18n/noJsx/getI18n.tsx (1)

304-311: Signature extension looks good

Extending createI18nTranslationFunctionsFactory with messages_themeDefined_fallback is coherent with the new lookup path. The destructuring is correct and call site updated in this file.

@BrDewit
Copy link
Copy Markdown
Author

BrDewit commented Aug 19, 2025

@garronej excuse me for the delay, but I've finally made the change.

The translation logic will now first check in the custom translation of the provided language, then it will check the default set of the provided language, then it will check the custom translation of the fallback language, after which it will check the default set of the fallback language.

This way, my issue, where a custom translation in 1 language would override the default translation of that key in every language, in is fixed.
While the regression is also fixed, which would cause the key itself to be shown instead of a translation, when no custom translation of a non-default key was provided in the language, but it was in another.

The only breaking change is of course if someone used the behaviour I changed, as a feature, where they use one custom translation to override default keys for every language, but that behaviour is unintuitive. It seems reasonable that with withCustomTranslations you must provide a translation for the key in every language you wish to override.

@BrDewit
Copy link
Copy Markdown
Author

BrDewit commented Dec 29, 2025

Hi @garronej I recently remembered that my pull request was still open. I've updated my fork with the latest changes, and the changes I made still seem relevant. If you agree with the changes, the pull request can be merged. If not, let me know what needs to be changed!

Thanks in advance and happy holidays :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants