-
-
Notifications
You must be signed in to change notification settings - Fork 957
Update translation workflows #15112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update translation workflows #15112
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,7 +36,10 @@ jobs: | |
| - name: Install gettext | ||
| run: sudo apt-get update && sudo apt-get install -y gettext | ||
| - name: Install Python dependencies | ||
| run: uv sync | ||
| # --all-packages installs the python_packages/* members; without them the | ||
| # Makefile's importability guard skips their message extraction and their | ||
| # CSVs never reach Crowdin. | ||
| run: uv sync --all-packages | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Resolved — addressed in the current code. blocking: the diagnosis in the comment is right, but if ! python -c "import $$module" >/dev/null 2>&1; then \not Fix in the Makefile: |
||
| - name: Install JavaScript dependencies | ||
| run: pnpm install --frozen-lockfile | ||
| - name: Extract and upload strings to Crowdin | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,41 +4,149 @@ | |
|
|
||
| This script should be run after downloading translations from Crowdin to clean up | ||
| any languages that were downloaded but are not officially supported by Kolibri. | ||
| A `languages_mapping` in crowdin.yml only renames codes, it does not filter, so | ||
| Crowdin delivers every language enabled on the project into every tree we sync. | ||
| """ | ||
|
|
||
| import glob | ||
| import logging | ||
| import os | ||
| import re | ||
| import shutil | ||
|
|
||
| from utils import available_languages | ||
| from utils import LOCALE_DATA_FOLDER | ||
| from utils import to_locale | ||
| from build_tools.i18n.generate_mapping import get_android_language_mapping | ||
| from build_tools.i18n.generate_mapping import get_installer_language_mapping | ||
| from build_tools.i18n.generate_mapping import get_language_mapping | ||
|
|
||
| REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) | ||
|
|
||
| def cleanup_unsupported_languages(): | ||
| # `values-` prefixes every Android resource qualifier, not just locales, so match the | ||
| # shape of a locale one — `fr`, `fr-rFR` or BCP-47 `b+es+419` — and leave the rest | ||
| # (values-night, values-v21, values-sw600dp) alone. `car` and `tv` are the UI-mode | ||
| # qualifiers short enough to also be shaped like a language code (both are real ISO 639 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: The denial is still correct and still necessary — |
||
| # codes), so they are denied outright: in a res/ directory they are never a locale. | ||
| ANDROID_LOCALE_QUALIFIER = re.compile( | ||
| r"^values-(?!(?:car|tv)$)(b\+[a-z]{2,3}(?:\+[A-Za-z0-9]+)*|[a-z]{2,3}(?:-r[A-Z]{2})?)$" | ||
| ) | ||
|
|
||
| # Non-hidden build output that recursive discovery would otherwise walk into: p4a | ||
| # unpacks a full CPython under android_root/, and an unpacked Kolibri tar or a | ||
| # PyInstaller/sdist staging tree holds catalogs we do not own. glob already skips | ||
| # dot-prefixed directories, so .venv/ needs no entry. | ||
| EXCLUDED_DIR_NAMES = frozenset( | ||
| { | ||
| "node_modules", | ||
| "site-packages", | ||
| "android_root", | ||
| "build", | ||
| "dist", | ||
| "tar", | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def _supported_locale_names(): | ||
| """Every locale directory name any of our sync targets may legitimately use. | ||
|
|
||
| A directory's path does not reveal which convention its tree follows — the | ||
| desktop-app installer keys folders by hyphen-lowercase intl codes (es-es) while | ||
| everything gettext-based uses Django codes (es_ES) — so plain locale directories | ||
| are pruned against the union. Only unsupported *languages* are being removed | ||
| here; a code in the wrong convention for its tree is a different problem. | ||
| """ | ||
| Remove locale directories that are not in our supported languages list. | ||
| return set(get_language_mapping().values()) | set( | ||
| get_installer_language_mapping().values() | ||
| ) | ||
|
|
||
|
|
||
| def prune_locale_dirs(directory, supported_names, pattern=None): | ||
| """Remove immediate subdirectories of `directory` for unsupported languages. | ||
|
|
||
| `pattern` is a compiled regex whose first group extracts the locale name from a | ||
| subdirectory name; a name it does not match is not a locale directory and is left | ||
| alone. Without one, the subdirectory name is the locale name. Returns the sorted | ||
| names of what was removed. | ||
| """ | ||
| # Get the set of supported locale directory names | ||
| supported_locales = set() | ||
| for lang_obj in available_languages(): | ||
| intl_code = lang_obj["intl_code"] | ||
| locale_dir = to_locale(intl_code) | ||
| supported_locales.add(locale_dir) | ||
|
|
||
| # Check all directories in the locale folder | ||
| removed_locales = [] | ||
| for item in os.listdir(LOCALE_DATA_FOLDER): | ||
| item_path = os.path.join(LOCALE_DATA_FOLDER, item) | ||
| if not os.path.isdir(directory): | ||
| return [] | ||
|
|
||
| removed = [] | ||
| for item in sorted(os.listdir(directory)): | ||
| item_path = os.path.join(directory, item) | ||
|
|
||
| # Skip if not a directory or if it's a special file | ||
| if not os.path.isdir(item_path): | ||
| continue | ||
|
|
||
| # If this directory is not in our supported list, remove it | ||
| if item not in supported_locales: | ||
| if pattern is None: | ||
| locale_name = item | ||
| else: | ||
| match = pattern.match(item) | ||
| if match is None: | ||
| continue | ||
| locale_name = match.group(1) | ||
|
|
||
| if locale_name not in supported_names: | ||
| shutil.rmtree(item_path) | ||
| removed_locales.append(item) | ||
| removed.append(item) | ||
|
|
||
| return removed | ||
|
|
||
|
|
||
| def _is_excluded(path, repo_root): | ||
| # Relative to the root: a checkout under ~/build/kolibri must not exclude everything. | ||
| parts = os.path.relpath(path, repo_root).split(os.sep) | ||
| return any( | ||
| part in EXCLUDED_DIR_NAMES or part.endswith(".egg-info") for part in parts | ||
| ) | ||
|
|
||
|
|
||
| def _locale_roots(repo_root): | ||
| """Directories named locale/ or locales/, holding one subdirectory per language.""" | ||
| yield os.path.join(repo_root, "kolibri", "locale") | ||
|
|
||
| for top in ("python_packages", "platforms"): | ||
| for name in ("locale", "locales"): | ||
| for path in glob.glob( | ||
| os.path.join(repo_root, top, "**", name), recursive=True | ||
| ): | ||
| # Vendored dependencies and staged build output ship their own | ||
| # catalogs; not ours to prune. | ||
| if _is_excluded(path, repo_root): | ||
| continue | ||
| yield path | ||
|
|
||
|
|
||
| def _android_res_roots(repo_root): | ||
| """Android res/ directories, identified by their untranslated values/ folder.""" | ||
| for path in glob.glob( | ||
| os.path.join(repo_root, "platforms", "**", "res", "values"), recursive=True | ||
| ): | ||
| if _is_excluded(path, repo_root): | ||
| continue | ||
| yield os.path.dirname(path) | ||
|
|
||
|
|
||
| def cleanup_targets(repo_root=REPO_ROOT): | ||
| """Yield (directory, supported_names, pattern) for every tree Crowdin writes to.""" | ||
| supported = _supported_locale_names() | ||
| for directory in _locale_roots(repo_root): | ||
| yield directory, supported, None | ||
|
|
||
| android_supported = set(get_android_language_mapping().values()) | ||
| for directory in _android_res_roots(repo_root): | ||
| yield directory, android_supported, ANDROID_LOCALE_QUALIFIER | ||
|
|
||
|
|
||
| def cleanup_unsupported_languages(repo_root=REPO_ROOT): | ||
| """ | ||
| Remove locale directories that are not in our supported languages list. | ||
| """ | ||
| removed_locales = [] | ||
| for directory, supported_names, pattern in cleanup_targets(repo_root): | ||
| for name in prune_locale_dirs(directory, supported_names, pattern): | ||
| removed_locales.append( | ||
| os.path.join(os.path.relpath(directory, repo_root), name) | ||
| ) | ||
|
|
||
| if removed_locales: | ||
| logging.info( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ Resolved — addressed in the current code.
blocking:
--all-packagescannot make that guard pass, so the comment asserts a behaviour that doesn't occur. The guard runs a bare interpreter:# Makefile:260 (i18n-extract-frontend) and Makefile:316 (i18n-download-translations) if ! python -c "import $$module" >/dev/null 2>&1; then \Bare
python, notuv run python— which every other Python recipe in this Makefile uses (205, 244, 248, 275, 285, 306).uv syncinstalls into.venv/and nothing here puts it onPATH:setup-uv@v8.2.0is invoked withoutactivate-environment, and no step writesVIRTUAL_ENV/$GITHUB_PATH. So the guard's interpreter is the runner's system Python, which never sees the workspace members, and the plugin CSVs still don't reach Crowdin.The fix is in the Makefile: change both guards to
uv run python -c "import $$module"(keeping--all-packageshere). Settingactivate-environment: trueon thesetup-uvstep would also work, but the Makefile change makes the recipes correct when run locally too.