Implement Interface Internationalization Support #41

Closed
opened 2026-07-03 07:55:54 -07:00 by serrebi · 3 comments
Owner

It is required to enable interface internationalization (i18n) for BlindRSS to broaden its usability for users from different countries. Currently, the application’s UI is fixed in English, limiting accessibility for non‑English‑speaking users.

Given that a related component of the BlindRSS ecosystem (VLC) already leverages the gettext framework for localization, it is proposed to adopt the same solution for the main BlindRSS interface. gettext is a widely used, mature system with accessible tooling for translators and a straightforward implementation model: original English strings serve as translation keys, so migration mainly involves wrapping static UI strings in gettext calls and setting up the necessary infrastructure.

Additionally, language preferences should not be strictly tied to the system locale. Users may prefer a different interface language than the OS default. Therefore, the application must provide an explicit language selection mechanism in the menu, while keeping “System Default” as the out‑of‑the‑box option.


Implementation Overview for gettext in Existing Codebases

It is proposed to follow this practical approach to integrate gettext into a project where UI element labels are currently hardcoded:

Identify and Wrap Static Strings
  • Locate all user‑facing strings in the codebase (menu items, dialog titles, button labels, status messages, error texts).
  • Replace direct string literals with gettext function calls (e.g., gettext("Save") or the conventional shorthand _("Save")).
  • Ensure that plural forms are handled using ngettext() where appropriate (e.g., “1 item”, “2 items”).
Extract Strings into a POT File
  • Use xgettext to scan the source code and generate a Portable Object Template (.pot) file containing all translatable strings.
  • This file serves as the master list of keys for all translations.
Create PO Files for Target Languages
  • For each supported language, create a .po file (e.g., ru.po, de.po) based on the .pot template.
  • Translators fill in the translations for each msgid.
  • Tools like Poedit provide a graphical interface to simplify this process.
Compile to MO Files
  • Convert .po files into binary .mo files using msgfmt. These are the files loaded at runtime for fast lookup.
  • Place .mo files in a standard directory structure (e.g., /usr/share/locale/<lang>/LC_MESSAGES/blindrss.mo).
Initialize gettext at Application Startup
  • Call bindtextdomain() to specify where locale files are stored.
  • Call textdomain() to set the domain name (e.g., "blindrss").
  • Optionally call bind_textdomain_codeset() to define the character encoding (usually UTF‑8).
Handle Language Switching at Runtime
  • Maintain a runtime variable for the currently selected UI language.
  • When the user changes the language via the menu, update this variable and reload the appropriate locale domain.
  • In environments where gettext does not support dynamic reloading, consider restarting the UI components or the application itself after a language change.

Proposed UI Changes

It is proposed to add a language selection option in the application menu with the following behavior:

  • A new submenu (e.g., “Language” under “Settings” or “Help”) lists available languages plus a special entry labeled “As in System”.

  • “As in System” is selected by default, meaning the app uses the OS locale setting.

  • Selecting a specific language overrides the system setting for the current session (or permanently, depending on implementation).

  • The UI immediately reflects the chosen language wherever gettext strings are used.


Expected Behavior

After implementation, the following behavior is expected:

Default Behavior
  • On first launch, the interface language matches the system locale (via the “As in System” option).
  • All user‑visible strings are rendered according to the selected locale using gettext.
User-Initiated Language Change
  • When a user selects a specific language from the menu, the interface updates to that language.
  • If “As in System” is reselected, the app reverts to using the OS locale.
  • Language preference persists across sessions (stored in configuration).

Technical Consistency

  • Hardcoded English strings no longer appear directly in the UI; all are accessed via gettext functions.
  • New strings added to the UI in the future must follow the same pattern to maintain i18n readiness.

Additional Notes

  • Testing must include verification of correct rendering in multiple languages, paying special attention to string length variations (some translations may be significantly longer than English).
  • Pluralization rules differ between languages; ngettext() must be used where count‑based messages appear.
  • Documentation for translators (including how to contribute .po files) should be included in the project repository to encourage community contributions.
It is required to enable interface internationalization (i18n) for BlindRSS to broaden its usability for users from different countries. Currently, the application’s UI is fixed in English, limiting accessibility for non‑English‑speaking users. Given that a related component of the BlindRSS ecosystem (VLC) already leverages the gettext framework for localization, it is proposed to adopt the same solution for the main BlindRSS interface. gettext is a widely used, mature system with accessible tooling for translators and a straightforward implementation model: original English strings serve as translation keys, so migration mainly involves wrapping static UI strings in gettext calls and setting up the necessary infrastructure. Additionally, language preferences should not be strictly tied to the system locale. Users may prefer a different interface language than the OS default. Therefore, the application must provide an explicit language selection mechanism in the menu, while keeping “System Default” as the out‑of‑the‑box option. --- #### Implementation Overview for gettext in Existing Codebases It is proposed to follow this practical approach to integrate gettext into a project where UI element labels are currently hardcoded: ##### Identify and Wrap Static Strings - Locate all user‑facing strings in the codebase (menu items, dialog titles, button labels, status messages, error texts). - Replace direct string literals with gettext function calls (e.g., `gettext("Save")` or the conventional shorthand `_("Save")`). - Ensure that plural forms are handled using `ngettext()` where appropriate (e.g., “1 item”, “2 items”). ##### Extract Strings into a POT File - Use `xgettext` to scan the source code and generate a Portable Object Template (.pot) file containing all translatable strings. - This file serves as the master list of keys for all translations. ##### Create PO Files for Target Languages - For each supported language, create a .po file (e.g., `ru.po`, `de.po`) based on the .pot template. - Translators fill in the translations for each msgid. - Tools like Poedit provide a graphical interface to simplify this process. ##### Compile to MO Files - Convert .po files into binary .mo files using `msgfmt`. These are the files loaded at runtime for fast lookup. - Place .mo files in a standard directory structure (e.g., `/usr/share/locale/<lang>/LC_MESSAGES/blindrss.mo`). ##### Initialize gettext at Application Startup - Call `bindtextdomain()` to specify where locale files are stored. - Call `textdomain()` to set the domain name (e.g., `"blindrss"`). - Optionally call `bind_textdomain_codeset()` to define the character encoding (usually UTF‑8). ##### Handle Language Switching at Runtime - Maintain a runtime variable for the currently selected UI language. - When the user changes the language via the menu, update this variable and reload the appropriate locale domain. - In environments where gettext does not support dynamic reloading, consider restarting the UI components or the application itself after a language change. --- #### Proposed UI Changes It is proposed to add a language selection option in the application menu with the following behavior: - A new submenu (e.g., “Language” under “Settings” or “Help”) lists available languages plus a special entry labeled “As in System”. - “As in System” is selected by default, meaning the app uses the OS locale setting. - Selecting a specific language overrides the system setting for the current session (or permanently, depending on implementation). - The UI immediately reflects the chosen language wherever gettext strings are used. --- ##### Expected Behavior After implementation, the following behavior is expected: ##### Default Behavior - On first launch, the interface language matches the system locale (via the “As in System” option). - All user‑visible strings are rendered according to the selected locale using gettext. ##### User-Initiated Language Change - When a user selects a specific language from the menu, the interface updates to that language. - If “As in System” is reselected, the app reverts to using the OS locale. - Language preference persists across sessions (stored in configuration). #### Technical Consistency - Hardcoded English strings no longer appear directly in the UI; all are accessed via gettext functions. - New strings added to the UI in the future must follow the same pattern to maintain i18n readiness. --- ### Additional Notes - Testing must include verification of correct rendering in multiple languages, paying special attention to string length variations (some translations may be significantly longer than English). - Pluralization rules differ between languages; `ngettext()` must be used where count‑based messages appear. - Documentation for translators (including how to contribute .po files) should be included in the project repository to encourage community contributions. <!-- forgejo-github-sync: issue github=serrebidev/BlindRSS#44 -->
Author
Owner

The gettext infrastructure you proposed landed in v1.78.0.

What's in place:

  • gettext throughout, with English source strings as the message keys — so untranslated strings (and installs with no catalog) fall back to English exactly as before.
  • Language selection: Settings → General → Interface language (restart required). "Automatic" follows the OS locale, including the Windows UI language; the dropdown lists only languages that ship a compiled catalog. Also settable via the "language" key in config.json.
  • Translator workflow: locale/blindrss.pot is the template (regenerated by python tools/extract_strings.py); the standard msginit / msgmerge / msgfmt toolchain produces locale/<lang>/LC_MESSAGES/blindrss.mo, which the app and both build specs pick up automatically. locale/README.md documents the process, including placeholder and menu-access-key (&) conventions.
  • Coverage so far: all menus, the feed tree labels, context menus, article list columns, tray icon menu and status labels, and confirmation prompts (~134 messages). Remaining dialog strings (Settings tabs, player window, etc.) will be wrapped incrementally in follow-up releases — the infrastructure makes each addition a one-line change.

If you (or anyone reading) want to contribute a translation — Russian would be a great first catalog — the README has the exact commands, and Poedit works well with NVDA.

Thanks for the suggestion!

The gettext infrastructure you proposed landed in [v1.78.0](https://github.com/serrebidev/BlindRSS/releases/tag/v1.78.0). What's in place: - **gettext throughout**, with English source strings as the message keys — so untranslated strings (and installs with no catalog) fall back to English exactly as before. - **Language selection**: `Settings → General → Interface language` (restart required). "Automatic" follows the OS locale, including the Windows UI language; the dropdown lists only languages that ship a compiled catalog. Also settable via the `"language"` key in config.json. - **Translator workflow**: `locale/blindrss.pot` is the template (regenerated by `python tools/extract_strings.py`); the standard `msginit` / `msgmerge` / `msgfmt` toolchain produces `locale/<lang>/LC_MESSAGES/blindrss.mo`, which the app and both build specs pick up automatically. `locale/README.md` documents the process, including placeholder and menu-access-key (`&`) conventions. - **Coverage so far**: all menus, the feed tree labels, context menus, article list columns, tray icon menu and status labels, and confirmation prompts (~134 messages). Remaining dialog strings (Settings tabs, player window, etc.) will be wrapped incrementally in follow-up releases — the infrastructure makes each addition a one-line change. If you (or anyone reading) want to contribute a translation — Russian would be a great first catalog — the README has the exact commands, and Poedit works well with NVDA. Thanks for the suggestion! <!-- forgejo-github-sync: comment github=serrebidev/BlindRSS#44/4877905359 -->
Author
Owner

I propose refining the localization workflow with gettext to align it with best practices and reduce the burden on translators.

Suggested changes:

  1. Add *.mo to .gitignore so binary MO files aren’t tracked in the repo.
  2. Automate the compilation of .po to .mo files in the build script. This should be a build-time step, not a manual task for translators or developers.
  3. Update the instructions in locale/README.md to reflect this new workflow.

Rationale:

  • .mo files are binary artifacts. It’s a standard practice not to store generated binaries in version control unless absolutely necessary.
  • The human-editable .po files are the true source of translations. Translators should only need to work with and commit .po files.
  • Compiling .mo from .po is a deterministic build step; automating it makes the process more reliable and less error‑prone.

Let me know if there are any specific constraints or environment considerations I should be aware of.

I propose refining the localization workflow with gettext to align it with best practices and reduce the burden on translators. Suggested changes: 1. Add *.mo to .gitignore so binary MO files aren’t tracked in the repo. 2. Automate the compilation of .po to .mo files in the build script. This should be a build-time step, not a manual task for translators or developers. 3. Update the instructions in locale/README.md to reflect this new workflow. Rationale: * .mo files are binary artifacts. It’s a standard practice not to store generated binaries in version control unless absolutely necessary. * The human-editable .po files are the true source of translations. Translators should only need to work with and commit .po files. * Compiling .mo from .po is a deterministic build step; automating it makes the process more reliable and less error‑prone. Let me know if there are any specific constraints or environment considerations I should be aware of. <!-- forgejo-github-sync: comment github=serrebidev/BlindRSS#44/4879698209 -->
Author
Owner

Implemented in v1.78.3. Generated *.mo files are now ignored, build.bat/build.sh compile locale//LC_MESSAGES/blindrss.po catalogs with tools/compile_translations.py before PyInstaller runs, and locale/README.md documents the new translator workflow where .po files are the committed source.

Implemented in v1.78.3. Generated *.mo files are now ignored, build.bat/build.sh compile locale/<lang>/LC_MESSAGES/blindrss.po catalogs with tools/compile_translations.py before PyInstaller runs, and locale/README.md documents the new translator workflow where .po files are the committed source. <!-- forgejo-github-sync: comment github=serrebidev/BlindRSS#44/4879771295 -->
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
serrebi/BlindRSS#41
No description provided.