Skip to content

Conversation

@Dimi20cen
Copy link
Contributor

@Dimi20cen Dimi20cen commented Aug 18, 2025

Summary

This PR restores multi-language support to the Windows installer, addressing a regression from our legacy version. It implements a modern, industry-standard translation workflow using .po files and ISO locale codes.

All user-facing strings have been externalized from kolibri.iss into language files. Additionally, all installer related files (the main script, translations, and dependencies) have been moved into a new installer/ directory for better organization.

Changes

  • Externalized Strings & ISO Standard: User-facing text has been moved from kolibri.iss into separate .isl files. The system now uses ISO locale codes (e.g., es_ES, de_DE) instead of English language names.
  • Centralized Language Definitions: Added definitions.py to map ISO codes to their corresponding Inno Setup language names and Microsoft Language IDs. This automates the configuration of [LangOptions] and removes the need for manual ID lookup.
  • Conversion Scripts:
    • isl_to_po.py: Converts Inno Setup files to .po. When scaffolding a new language, it automatically detects and merges standard Inno Setup translations (e.g., "Next >", "Cancel") so translators only need to focus on Kolibri-specific strings.
    • po_to_isl.py: Compiles translated .po files back into .isl format for the installer, automatically injecting the correct Language IDs from the definitions file.
  • Automated Build Process: The build-installer-windows command automatically compiles all .po files found in installer/translations/locale/ into the required .isl format before building the executable.
  • New Makefile Targets:
    • new-language: Scaffolds a new language (e.g., make new-language LANG=es_ES), creating a .po file pre-filled with standard translations.
    • translations-export-source: Updates the source messages.po from the master en.isl.
    • translations-compile: Converts all localized .po files into .isl files ready for packaging.

References

Fixes #181

Translation Workflow

Prerequisites

  1. Install dependencies again, (make dependencies).
  2. Run
make get-whl whl="https://github.com/learningequality/kolibri/releases/download/v0.18.1/kolibri-0.18.1-py2.py3-none-any.whl"
  1. Run make pyinstaller to build the application.
  2. Have Inno Setup installed.
  3. Have polib installed, pip install polib.

1. Adding a New Language (Developer Task)

This process scaffolds the files for a new language using its ISO locale code.

  1. Register the Language:
    Open installer/translations/definitions.py and add the new ISO code to the LANG_DEFINITIONS dictionary. You must map it to the corresponding Inno Setup language name and Microsoft Language ID.

      "es_ES": {"inno_name": "Spanish", "id": "$040A"},

    This command runs the create_new_language.py script, which creates installer/translations/Spanish.isl. The file will be pre-filled with standard Inno Setup translations if available, otherwise it will be a copy of the English template with empty values for translation.

  2. Scaffold the PO File:
    Run the make target with the ISO code.

    make new-language LANG=es_ES
    • This checks your local Inno Setup installation for a standard translation file (e.g., Spanish.isl).
    • It creates installer/translations/locale/es_ES/messages.po.
    • Note: If a standard Inno file was found, standard strings (like "Next >" or "Cancel") will be pre-filled in the PO file, so translators only need to focus on Kolibri-specific strings.
  3. Enable the Language in the Installer Script:
    Open installer/kolibri.iss and add a new entry in the [Languages] section. Point to the .isl file (note: this file doesn't exist yet; it will be generated during the build).

    [Languages]
    Name: "en"; MessagesFile: "translations\en.isl"
    Name: "de_DE"; MessagesFile: "translations\locale\de_DE\de_DE.isl"
    ; Add the new line for Spanish
    Name: "es_ES"; MessagesFile: "translations\locale\es_ES\es_ES.isl"
    
  4. Commit Changes: Commit definitions.py, kolibri.iss, and the new messages.po file.

2. Syncing with Crowdin (Developer Task)

  1. Update Source Strings:
    If en.isl has changed, regenerate the source PO file:

    make translations-export-source

    Upload installer/translations/locale/en/messages.po to Crowdin as the Source File.

  2. Upload Existing Translations:
    If you just scaffolded a new language (Step 1) and it contains pre-filled standard translations, upload installer/translations/locale/es_ES/messages.po to Crowdin as Existing Translations. This ensures translators don't re-do work Inno Setup has already done.

3. Translating (Translator Task)

Translators work on Crowdin to translate the custom Kolibri strings (and any standard strings that were missing).

4. Integrating Translations (Developer Task)

  1. Download Translations:
    Download the completed .po file for the language from Crowdin.

  2. Update Repository:
    Overwrite the local file at installer/translations/locale/es_ES/messages.po with the version from Crowdin. Commit the change.

5. Building the Installer (Developer Task)

The build process automatically converts the PO files into the ISL format required by Inno Setup.

  1. Run the Build:

    make build-installer-windows
  2. What Happens Behind the Scenes:

    • The translations-compile target runs automatically.
    • It iterates through every folder in installer/translations/locale/.
    • It converts messages.po into a compiled .isl file (e.g., es_ES.isl).
    • It automatically injects the LanguageID and LanguageName from definitions.py into the .isl file.
  3. Verify:
    Run the generated installer in dist-installer/. You should see the language selection dialog, and selecting the new language should switch the UI text accordingly.

@rtibbles rtibbles self-assigned this Aug 26, 2025
@Dimi20cen Dimi20cen force-pushed the feat/windows/inno_setup_localization branch from 12267e8 to c22e54e Compare August 30, 2025 07:41
@Dimi20cen Dimi20cen force-pushed the feat/windows/inno_setup_localization branch 3 times, most recently from 224a5ef to 8cda0e6 Compare October 15, 2025 13:01
@Dimi20cen Dimi20cen force-pushed the feat/windows/inno_setup_localization branch from c22cad8 to 462d25d Compare October 26, 2025 17:58
Copy link
Member

@rtibbles rtibbles left a comment

Choose a reason for hiding this comment

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

I think this has most of the right pieces in place! Just a couple of clarifications that came to mind - and I'm not entirely sure we need all of the code here with the right workflow in place.

@@ -0,0 +1,173 @@
"""
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need this script. The workflow we would most likely use is to take the English isl file, convert it to a po file then upload it to Crowdin (the translation platform that we use).

We would then download the translated po files for each language, and convert those to isl files for each language. I think your two conversion scripts should handle all that is needed in that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

take the English isl file, convert it to a po file then upload it to Crowdin

If we don't use create_new_language.py, then the translator would have to re-translate even the standard strings that InnoSetup already provides (for languages like spanish).

Instead if we use create_new_language.py, then only the custom strings would need to be translated.

Do I miss something?

Copy link
Member

Choose a reason for hiding this comment

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

Crowdin lets you upload existing translations for a source as well - so if we have the original inno translations, we can convert them to a po file for upload.

My basic thought is that I'd like us to specify which languages we want to support, and then conditionally upload already existing translations for those languages for which translations already exist. It is also possible that I'm just misreading the workflow you're intending.

@@ -0,0 +1,305 @@
; Master English messages for Kolibri Installer
Copy link
Member

Choose a reason for hiding this comment

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

Might be easier to use the locale code as the filename en.isl rather than the language name? Especially as we have the language name encoded in here anyway!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently, create_new_language.py uses the language name (e.g. Spanish) to find the packaged translation in C:\Program Files (x86)\Inno Setup 6\Languages.

We could modify the create_new_language.py script to take a locale code as input, and then use an internal dictionary to look up the Inno Setup Filename it needs to find.

Something like that

LOCALE_TO_INNO_NAME = {
    "de": "German",
    "es": "Spanish",
    "fr": "French",
    "it": "Italian",
    # ...
}

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that works for me - we end up doing this kind of mapping quite a lot for translations, because every translation system uses something different. We use the locale (e.g. es_ES) canonically, because it is the most specific, whereas just saying "Spanish" is very ambiguous - is that Spanish from Spain, Spanish from Argentina, Spanish from Latin America?

So, essentially for each locale code that we want to support, we would also make a note of which Inno Setup language name we would pull translations from.


[LangOptions]
languagename=English
languageid=$0409
Copy link
Member

Choose a reason for hiding this comment

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

Where do we find out these values?

Copy link
Contributor Author

@Dimi20cen Dimi20cen Oct 31, 2025

Choose a reason for hiding this comment

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

They can be found here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c

I was thinking of adding this to the readme, when this PR is complete

Copy link
Member

Choose a reason for hiding this comment

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

That would be great - I suppose we could even add this information to the locale lookup object that we're defining above:

LOCALE_INFO = {
    "de": {
        "inno_setup_name": "German",
        "ms_languageid": "0x0007",
    },
    # ...
}

If we wanted to get really fancy, we could fetch the page and parse the HTML table - but that seems very unneccessary!

[LangOptions]
languagename=English
languageid=$0409
languagecodepage=0
Copy link
Member

Choose a reason for hiding this comment

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

What does languagecodepage mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It specifies the Windows ANSI code page that the language file text is encoded in. It tells Inno Setup how to correctly read and display the localized strings if the file isn’t in Unicode (UTF-8).

However how that the files are UTF-8, its not needed.
We can either remove it when creating a new language or maybe we can leave it as is

Copy link
Member

Choose a reason for hiding this comment

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

I think if we can be sure to encode in UTF-8 that seems like the best solution!

@@ -0,0 +1,147 @@
"""
Copy link
Member

Choose a reason for hiding this comment

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

I think the main distinction to keep in mind is that we treat English as the "source", and all other languages as the "translations".

So, the workflow is, we upload every message we need in English in a po file - but then for some languages we will essentially have 'pre-filled' translations based on what InnoSetup already provides (either from its built in translations or community supported translations).

This commit refactors the Windows installer to support multiple languages and improves the build system's organization.

User-facing strings are now externalized from `kolibri.iss` into `.isl` language files, enabling a language selection dialog on startup. All installer-related files (the script, translations, and dependencies) have been consolidated into the `installer/` directory.

Major changes:
-   Added new translation management scripts (`create_new_language.py`, `update_from_inno_default.py`) to streamline localization.
-   Introduced `new-language` and `update-translations` Makefile targets to automate the translation workflow.
-   Updated the `Makefile` and PyInstaller spec to use the new `installer/` directory structure.
This commit introduces a translation workflow for the Windows installer using `.po` files. This replaces the previous method of directly editing `.isl` files, making the localization process more accessible and manageable for translators.

- PO/ISL Conversion: Added `isl_to_po.py` and `po_to_isl.py` scripts to convert between Inno Setup's `.isl` format and the standard `.po` format.
- Makefile Integration:
    - Created a `translations-export` target to generate `.po` files from existing `.isl` files for translators, using the `isl_to_po.py` script.
    - Added a `translations-compile` target to convert all `.po` files back into `.isl` format, using the `po_to_isl.py` script.
    - The `build-installer-windows` process now automatically runs `translations-compile`, ensuring the latest translations are always included in the build.
- Initial German PO File: Added `German.po` to demonstrate the new workflow.
- Improved Scaffolding: The `create_new_language.py` script has been updated to better handle the creation of new language files, ensuring custom messages are correctly scaffolded for translation.
@Dimi20cen Dimi20cen force-pushed the feat/windows/inno_setup_localization branch from 462d25d to e0ed8f3 Compare November 27, 2025 09:12
…tion config

Changes:
- Renamed translation files and directories to use ISO codes (e.g., `es_ES` instead of `Spanish`), removing ambiguity.
- Added `definitions.py` as a single source of truth for Microsoft Language IDs and Inno Setup names, removing the need to manually configure `[LangOptions]`.
- Workflow Refactor:
    - Removed `create_new_language.py`, its logic is now integrated into `isl_to_po.py` via the `new-language` target.
    - Updated `po_to_isl.py` to automatically add the correct Language ID during compilation.
    - Updated `Makefile` and `kolibri.iss` to support the new `translations/locale/<code >/` directory structure.
@Dimi20cen Dimi20cen force-pushed the feat/windows/inno_setup_localization branch from ef5d319 to a1039f6 Compare November 27, 2025 09:39
Copy link
Member

@rtibbles rtibbles left a comment

Choose a reason for hiding this comment

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

This is looking pretty good to me! I'll do a quick test locally to make sure I can follow the workflow.

@rtibbles
Copy link
Member

rtibbles commented Dec 4, 2025

Naturally, when I said I would do this... I forgot the fact that I don't have a Windows dev machine, so there are significant parts of the workflow that I can't actually do!

@Dimi20cen as a final step here, could you add all the supported languages for Kolibri - you can see the full list of codes here: https://github.com/learningequality/kolibri/blob/develop/kolibri/utils/i18n.py#L47

If you need any more information about the languages, then this JSON file should have it: https://github.com/learningequality/kolibri/blob/develop/kolibri/locale/language_info.json

Feel free to just focus on the intl-codes for now - I can add the extra mapping for going into the crowdin language code space!

- Update `definitions.py` with native display names, custom fonts, and RTL flags.
- Refactor locale codes (e.g., `de_DE` -> `de`) for consistency with kolbri intl-codes
- Update `po_to_isl.py` to generate `LangOptions` based on new metadata.
- Update `kolibri.iss` with the revised language list.
Copy link
Member

@rtibbles rtibbles left a comment

Choose a reason for hiding this comment

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

I think this is looking good, I'll have to run this on my windows machine to be absolutely sure, but I think it might be simpler just to merge this and iterate from there!

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.

Add Localization Support to the Inno Setup Installer

2 participants