Skip to content

[PoC] [WIP] uses react-i18next#2738

Closed
dgdavid wants to merge 28 commits intoapi-v2from
i18next
Closed

[PoC] [WIP] uses react-i18next#2738
dgdavid wants to merge 28 commits intoapi-v2from
i18next

Conversation

@dgdavid
Copy link
Copy Markdown
Contributor

@dgdavid dgdavid commented Sep 15, 2025

Agama: switch to react-i18next

As you already know, Agama translations are a bit complex, mainly because they come from multiple sources: the web interface, backend services, and even external libraries such as libstorage-ng (which returns actions in the language set on the backend side).

Throughout development, we’ve found several i18n related issues which have been handled by increasing/polishing our custom, Cockpit-based translation code. However, this solution still doesn’t offer an easy way to address an old challenge related to the not yet implemented welcome screen: the ability to load and apply translations for the selected language without requiring a full app reload.

To tackle this, I’ve started exploring react-i18next as an alternative. It’s a mature, actively maintained library that could help us to reduce time invested on maintaining a custom i18n implementation, streamline translation handling, and enable dynamic language switching.

The good news is that it almost works out of the box. That said, to fully integrate it with the Agama translation stack, I’ll need help from someone with deeper knowledge of how translations are currently wired throughout the system.

Screencast.From.2025-09-15.11-32-45.mp4

For reference, please see below the relevant notes from my experiment:

  • react-i18next is based on i18next, which has a broad ecosystem of plugins and utilities available such as backends, language detectors, extractors, processors, and more. See: https://www.i18next.com/overview/plugins-and-utils

  • In the experiment, I used the settings you can check in the diff.

  • For testing, I generated locale JSON files using i18next-gettext-converter:

    i18next-conv -l es -s es.po -t web/public/po-es.json

  • Initially, I generated an English JSON file from agama.pot using msginit, before realizing that setting fallbackLng: false causes i18next to use keys as fallback:

    msginit --input=agama.pot --locale=en --no-translator --output-file=web/public/po-en.po

  • From what I understand, plural handling in i18next differs slightly from (n)gettext. The t() function doesn’t require a plural key by default—just a count variable. As a result, if a plural form is missing from the translation file, it will silently fall back to the singular form.

    That said, when using the i18next-parser:

    i18next 'src/**/*.{ts,tsx}'

    It correctly extracts and generates plural keys. For example:

    t("{{count}} languages supported", { count: 4 })

    Produces the following keys in the output JSON:

    {
      "{{count}} languages supported_one": "",
      "{{count}} languages supported_other": ""
    }
  • The caveat: developer-introduced texts alone won’t be enough to fully render the app in the default language (English), because plural forms are no longer present in the source code. Instead, they only would exist in the translation files.

    Is this a limitation, or could it be seen as a benefit? It might help ensure that even the default language is reviewed and localized properly, rather than assuming source strings are final. Check Missing plural keys in translation i18next/i18next#1690 and ngettext replacement ? i18next/i18next#1096

    Still, if this behavior becomes problematic, perhaps it could be improved or automated with a custom plugin: https://www.i18next.com/misc/creating-own-plugins)](https://www.i18next.com/misc/creating-own-plugins

  • For translations comming from backend, we can implement a good sync mechanism now that API is being rewritten. Something to manage them via query-cache or so, able to react to an onBackendLanguageChanged or event.

teclator and others added 28 commits September 3, 2025 16:59
Co-authored-by: José Iván López González <jlopez@suse.com>
By adding a new query for the system endpoint and consuming data from
there.

Previous "system" query has been moved to "hostname", which most
probably will dissapear once hostname is ported to the new API and, most
probably, served as part of the system.
Read the FIXME in the code to know more.
Apart from fixing some wrong keys after a backend update.
- Some code is commented in the supervisor until l10n is able to
  dispatch the required actions (e.g., apply config).
@dgdavid
Copy link
Copy Markdown
Contributor Author

dgdavid commented Sep 15, 2025

Note

The real-time language change shown in the screencast is intended for demo purposes only.

Ideally, we would retain this behavior, as it improves usability, especially on the mentioned welcome screen (#1941, #2503), by allowing users to view options and buttons in their selected language.

However, the final implementation should meet the following requirements:

  • The system must not trigger a backend language change until the user selects [Accept] or [Continue].
  • If the user selects [Cancel], the interface language should revert to the previously selected language.

@dgdavid
Copy link
Copy Markdown
Contributor Author

dgdavid commented Sep 15, 2025

Initially, I generated an English JSON file from agama.pot using msginit, before realizing that setting fallbackLng: false causes i18next to use keys as fallback:

But, In fact, I think we shouldn't use fallbackLng: false and miss the opportunity of build good fallback languages, especially on language detection, https://www.i18next.com/principles/fallback

@dgdavid dgdavid changed the title [PoC] [WIP] uses react-i18n [PoC] [WIP] uses react-i18next Sep 15, 2025
Copy link
Copy Markdown
Contributor

@lslezak lslezak left a comment

Choose a reason for hiding this comment

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

In general it looks good, just some minor comments.

// replaced with the product name and the text in the square brackets [] is
// used for the link to show the license, please keep the brackets.
_("I have read and accept the [license] for %s"),
t("I have read and accept the [license] for {{product.name}}", { product: nextProduct }),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd keep the old way and avoid using the i18next interpolations. There are several advantages for the old approach:

  • Gettext adds the c-format flag for such messages and then it checks that all translations also contain the %s placeholder. And that check is also done by Weblate, the translators cannot even save a translation without the proper number of %s placeholders. That avoids bugs in translations from the very beginning.
  • The %s is much simpler format and avoids typos or copy&paste errors. What happens if a translator by mistake writes {{product,name}} or {{produtc.name}} in a translation? With simple %s you cannot do much mistakes and if you do then gettext will complain.

import LanguageDetector from "i18next-browser-languagedetector";
import HTTPApi from "i18next-http-backend";

i18next
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I do not like when some initialization code is executed directly at import. You cannot write a test for such functionality.

I'd wrap it into an init() function or something like that and call that function explicitly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have not a clear idea nor strong opinion here. It was just copy/pasted form the upstream documentation for doing a quick PoC

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK, np as it is still a PoC...

@lslezak
Copy link
Copy Markdown
Contributor

lslezak commented Sep 26, 2025

Regarding translations, I'd keep the current _() translation function and just just change its implementation to call t() from i18next.

The same with plural forms, let's keep using the n_() function and change its implementation. That would allow using the xgettext extractor and using the PO files in Weblate as we do now.

@dgdavid
Copy link
Copy Markdown
Contributor Author

dgdavid commented Sep 29, 2025

Regarding translations, I'd keep the current _() translation function and just just change its implementation to call t() from i18next.

If I'm not wrong, that is not possible: "t" is a hook. And that is why it works and immediately change the texts as soon a new translation file is loaded.

The same with plural forms, let's keep using the n_() function and change its implementation. That would allow using the xgettext extractor and using the PO files in Weblate as we do now.

I'm not sure we should stick with xgettext if it goes against a more React-friendly way of handling translations. For better or worse, React is the framework we're using for the web UI, so it makes sense to align with its ecosystem as much as possible. As mentioned in the PR, there are plugins available that can handle translation extraction.

That said, you're the expert in i18n here. Let me just kindly ask you take a moment to think twice about how we might combine the best of both worlds, xgettext and i18next, even if that means reworking or even sacrifice some of what’s already been done. The goal is to enable more dynamic, reactive translations, and we expect changes along the way.

@lslezak
Copy link
Copy Markdown
Contributor

lslezak commented Sep 29, 2025

If I'm not wrong, that is not possible: "t" is a hook. And that is why it works and immediately change the texts as soon a new translation file is loaded.

Um, that should be possible, t() is just a wrapper around the translate() function which should just return a string in the end... (Hopefully I haven't overlooked something.)

I'm not sure we should stick with xgettext if it goes against a more React-friendly way of handling translations. For better or worse, React is the framework we're using for the web UI, so it makes sense to align with its ecosystem as much as possible. As mentioned in the PR, there are plugins available that can handle translation extraction.

I'm fine with changing the tooling if it better matches the used framework. I just do not want to loose the features provided by the current translation stack. I found out that Weblate has native support for the i18next JSON files so that should not be a problem.

However, I can still see benefits of using the GNU gettext and the PO files:

  • The checks for the %s format tags as mentioned above
  • Comments for translators which can describe the context of the message or the %s placeholder details, that significantly reduces risk of wrong or confusing translations
  • Location in the source code, in Weblate you can just click a link displayed next to the message to see where exactly the text is located in the Agama sources. That allows the translators to see the context and some related texts around, that can help with writing good translations.

Another reason for gettext and the PO files is that it is unified for all Agama components, it is also used by the Ruby backend. Using the same approach and the same rules everywhere reduces confusion and mistakes on the translators side.

@dgdavid
Copy link
Copy Markdown
Contributor Author

dgdavid commented Sep 29, 2025

First of all, thanks for the explanation.

If I'm not wrong, that is not possible: "t" is a hook. And that is why it works and immediately change the texts as soon a new translation file is loaded.

Um, that should be possible, t() is just a wrapper around the translate() function which should just return a string in the end... (Hopefully I haven't overlooked something.)

Maybe it was me who overlooked something. BtW, I meant t comes from a hook.

I'm not sure we should stick with xgettext if it goes against a more React-friendly way of handling translations. For better or worse, React is the framework we're using for the web UI, so it makes sense to align with its ecosystem as much as possible. As mentioned in the PR, there are plugins available that can handle translation extraction.

I'm fine with changing the tooling if it better matches the used framework.

I just do not want to loose the features provided by the current translation stack.

Don't get me wrong. Me neither. Just betting for find the best of two world but avoiding to fight against the framework.

I found out that Weblate has native support for the i18next JSON files so that should not be a problem.

Cool.

However, I can still see benefits of using the GNU gettext and the PO files:

  • The checks for the %s format tags as mentioned above
  • Comments for translators which can describe the context of the message or the %s placeholder details, that significantly reduces risk of wrong or confusing translations
  • Location in the source code, in Weblate you can just click a link displayed next to the message to see where exactly the text is located in the Agama sources. That allows the translators to see the context and some related texts around, that can help with writing good translations.

Another reason for gettext and the PO files is that it is unified for all Agama components, it is also used by the Ruby backend. Using the same approach and the same rules everywhere reduces confusion and mistakes on the translators side.

I see. Let's try put all the pieces together and see what we can do. We can talk about it tomorrow or whenever this task can be moved ahead.

Thanks.

@lslezak
Copy link
Copy Markdown
Contributor

lslezak commented Sep 30, 2025

Yes, probably using i18next + GNU gettext and PO files is the best solution...

Base automatically changed from agama-new-api to api-v2 October 3, 2025 12:27
@dgdavid
Copy link
Copy Markdown
Contributor Author

dgdavid commented Mar 2, 2026

Closing this PR as the situation has significantly improved. Agama can now translate the interface without requiring a full refresh. This has been addressed in the latest updates: #3142 and #3197.

@dgdavid dgdavid closed this Mar 2, 2026
@dgdavid dgdavid deleted the i18next branch March 17, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants