diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 564fb6e66..c303ec09a 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -139,7 +139,7 @@ jobs: set -x sudo apt update - sudo apt install portaudio19-dev gettext + sudo apt install portaudio19-dev curl -L https://github.com/bazelbuild/bazelisk/releases/download/v1.7.4/bazelisk-linux-amd64 -o ./bazel && \ chmod +x ./bazel diff --git a/docs/linux.md b/docs/linux.md index 3e7c4ba11..794b2790b 100644 --- a/docs/linux.md +++ b/docs/linux.md @@ -7,7 +7,7 @@ These instructions are written for Debian/Ubuntu; adjust for your distribution. **Ensure some basic tools are installed**: ``` -$ sudo apt install bash grep findutils curl gcc g++ git gettext +$ sudo apt install bash grep findutils curl gcc g++ git ``` The 'find' utility is 'findutils' on Debian. diff --git a/docs/mac.md b/docs/mac.md index ddb2bb249..2a6aa9d43 100644 --- a/docs/mac.md +++ b/docs/mac.md @@ -14,7 +14,7 @@ Install Homebrew from Then install deps: ``` -$ brew install rsync gettext bazelisk +$ brew install rsync bazelisk ``` **Install Python 3.8**: diff --git a/docs/windows.md b/docs/windows.md index ba1de3388..aeebb229e 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -31,7 +31,7 @@ Install [msys2](https://www.msys2.org/) into the default folder location. After installation completes, run msys2, and run the following command: ``` -$ pacman -S git gettext +$ pacman -S git ``` **Bazelisk**: diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index ee3ea14ac..74496d1f5 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -1,13 +1,10 @@ -# -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -# Please leave the coding line in this file to prevent xgettext complaining. from __future__ import annotations -import gettext import re -from typing import Optional, Union +from typing import Optional import anki @@ -142,11 +139,6 @@ def lang_to_disk_lang(lang: str) -> str: # the currently set interface language currentLang = "en" -# the current gettext translation catalog -current_catalog: Optional[ - Union[gettext.NullTranslations, gettext.GNUTranslations] -] = None - # the current Fluent translation instance current_i18n: Optional[anki.rsbackend.RustBackend] = None @@ -155,10 +147,13 @@ locale_folder = "" def _(str: str) -> str: - if current_catalog: - return current_catalog.gettext(str) - else: - return str + print(f"gettext _() is deprecated: {str}") + return str + + +def ngettext(single: str, plural: str, n: int) -> str: + print(f"ngettext() is deprecated: {plural}") + return plural def tr_legacyglobal(*args, **kwargs) -> str: @@ -169,26 +164,10 @@ def tr_legacyglobal(*args, **kwargs) -> str: return "tr_legacyglobal() called without active backend" -def ngettext(single: str, plural: str, n: int) -> str: - if current_catalog: - return current_catalog.ngettext(single, plural, n) - elif n == 1: - return single - return plural - - def set_lang(lang: str, locale_dir: str) -> None: - global currentLang, current_catalog, current_i18n, locale_folder - gettext_dir = locale_dir - ftl_dir = locale_dir - + global currentLang, current_i18n, locale_folder currentLang = lang - current_catalog = gettext.translation( - "anki", gettext_dir, languages=[lang], fallback=True - ) - - current_i18n = anki.rsbackend.RustBackend(ftl_folder=ftl_dir, langs=[lang]) - + current_i18n = anki.rsbackend.RustBackend(ftl_folder=locale_folder, langs=[lang]) locale_folder = locale_dir diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index ec5d5d523..2317b4c67 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -4,7 +4,6 @@ import argparse import builtins import getpass -import gettext import locale import os import sys @@ -167,7 +166,7 @@ dialogs = DialogManager() # Language handling ########################################################################## # Qt requires its translator to be installed before any GUI widgets are -# loaded, and we need the Qt language to match the gettext language or +# loaded, and we need the Qt language to match the i18n language or # translated shortcuts will not work. # A reference to the Qt translator needs to be held to prevent it from @@ -202,7 +201,7 @@ def setupLangAndBackend( lang = force or pm.meta["defaultLang"] lang = anki.lang.lang_to_disk_lang(lang) - # load gettext catalog + # set active language ldir = locale_dir() anki.lang.set_lang(lang, ldir) diff --git a/qt/aqt/data/BUILD.bazel b/qt/aqt/data/BUILD.bazel index 4512cbe44..98324bb90 100644 --- a/qt/aqt/data/BUILD.bazel +++ b/qt/aqt/data/BUILD.bazel @@ -1,7 +1,6 @@ filegroup( name = "data", srcs = [ - "//qt/aqt/data/locale", "//qt/aqt/data/web", ], visibility = ["//qt:__subpackages__"], diff --git a/qt/aqt/data/locale/BUILD.bazel b/qt/aqt/data/locale/BUILD.bazel deleted file mode 100644 index 1fb916e8d..000000000 --- a/qt/aqt/data/locale/BUILD.bazel +++ /dev/null @@ -1,6 +0,0 @@ -load("//qt/po:gettext.bzl", "compile_all_po_files") - -compile_all_po_files( - name = "locale", - visibility = ["//qt:__subpackages__"], -) diff --git a/qt/aqt/pinnedmodules.py b/qt/aqt/pinnedmodules.py index eee5660b0..70c7b86a4 100644 --- a/qt/aqt/pinnedmodules.py +++ b/qt/aqt/pinnedmodules.py @@ -9,6 +9,7 @@ # included implicitly in the past, and relied upon by some add-ons import cgi import decimal +import gettext # useful for add-ons import logging diff --git a/qt/po/.gitignore b/qt/po/.gitignore deleted file mode 100644 index 334750928..000000000 --- a/qt/po/.gitignore +++ /dev/null @@ -1 +0,0 @@ -strings*.json diff --git a/qt/po/BUILD.bazel b/qt/po/BUILD.bazel deleted file mode 100644 index cdca3e8a3..000000000 --- a/qt/po/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -load(":gettext.bzl", "build_template", "update_all_po_files") - -build_template( - name = "pot", - srcs = [ - "//pylib/anki:py_source_files", - "//qt/aqt:py_source_files", - "//qt/aqt/forms", - ], - pot_file = "anki.pot", -) - -update_all_po_files( - name = "po_files", - pot_file = "anki.pot", - visibility = ["//qt/aqt:__subpackages__"], -) diff --git a/qt/po/gettext.bzl b/qt/po/gettext.bzl deleted file mode 100644 index e5279b05b..000000000 --- a/qt/po/gettext.bzl +++ /dev/null @@ -1,148 +0,0 @@ -_langs = [ - "af", - "ar", - "bg", - "ca", - "cs", - "da", - "de", - "el", - "en-GB", - "eo", - "es", - "et", - "eu", - "fa", - "fi", - "fr", - "ga-IE", - "gl", - "he", - "hi-IN", - "hr", - "hu", - "hy-AM", - "it", - "ja", - "jbo", - "kab", - # "km", - "ko", - "la", - "mn", - "mr", - "ms", - "nb-NO", - "nl", - "nn-NO", - "oc", - "or", - "pl", - "pt-BR", - "pt-PT", - "ro", - "ru", - "sk", - "sl", - "sr", - "sv-SE", - "th", - "tr", - "uk", - # "ur", - "vi", - "zh-CN", - "zh-TW", -] - -# homebrew gettext is not on path by default -_pathfix = """export PATH="$$PATH":/usr/local/opt/gettext/bin\n""" - -def update_po(name, po_file_in, po_file_out, pot_file, visibility): - "Merge old .po and latest strings from .pot into new .po" - native.genrule( - name = name, - srcs = [po_file_in, pot_file], - outs = [po_file_out], - cmd = _pathfix + """\ -msgmerge -q -F --no-wrap $(location {po_file_in}) $(location {pot_file}) > $(location {po_file_out}) -""".format( - po_file_in = po_file_in, - po_file_out = po_file_out, - pot_file = pot_file, - ), - message = "Updating translation", - visibility = visibility, - ) - -def compile_po(name, po_file, mo_file): - "Build .mo file from an updated .po file." - native.genrule( - name = name, - srcs = [po_file], - outs = [mo_file], - cmd = _pathfix + """\ -cat $(location {po_file}) | msgfmt - --output-file=$(location {mo_file}) -""".format( - po_file = po_file, - mo_file = mo_file, - ), - message = "Compiling translation", - ) - -def build_template(name, pot_file, srcs): - "Build .pot file from Python files." - native.genrule( - name = name, - srcs = srcs, - outs = [pot_file], - cmd = _pathfix + """\ -all=all.files -for i in $(SRCS); do - echo $$i >> $$all -done -xgettext -cT: -s --no-wrap --files-from=$$all --output=$(OUTS) -rm $$all -""", - message = "Building .pot template", - ) - -def update_all_po_files(name, pot_file, visibility): - # merge external .po files with updated .pot - po_files = [] - for lang in _langs: - po_file_in = "@aqt_po//:desktop/{}/anki.po".format(lang) - po_file_out = "{}/anki.po".format(lang) - update_po( - name = lang + "_po", - po_file_in = po_file_in, - po_file_out = po_file_out, - pot_file = pot_file, - visibility = visibility, - ) - po_files.append(po_file_out) - - native.filegroup( - name = name, - srcs = po_files, - visibility = visibility, - ) - -def compile_all_po_files(name, visibility): - "Build all .mo files from .po files." - mo_files = [] - for lang in _langs: - po_file = "//qt/po:{}/anki.po".format(lang) - mo_file = "{}/LC_MESSAGES/anki.mo".format(lang) - compile_po( - name = lang + "_mo", - po_file = po_file, - mo_file = mo_file, - ) - mo_files.append(mo_file) - - native.filegroup( - name = name, - srcs = mo_files, - visibility = visibility, - ) diff --git a/qt/po/plurals.json b/qt/po/plurals.json deleted file mode 100644 index bac5eda6a..000000000 --- a/qt/po/plurals.json +++ /dev/null @@ -1,889 +0,0 @@ -{ - "af": [ - "one", - "other" - ], - "ak": [ - "one", - "other" - ], - "am": [ - "one", - "other" - ], - "an": [ - "one", - "other" - ], - "ar": [ - "zero", - "one", - "two", - "few", - "many", - "other" - ], - "ars": [ - "zero", - "one", - "two", - "few", - "many", - "other" - ], - "as": [ - "one", - "other" - ], - "asa": [ - "one", - "other" - ], - "ast": [ - "one", - "other" - ], - "az": [ - "one", - "other" - ], - "be": [ - "one", - "few", - "many", - "other" - ], - "bem": [ - "one", - "other" - ], - "bez": [ - "one", - "other" - ], - "bg": [ - "one", - "other" - ], - "bho": [ - "one", - "other" - ], - "bm": [ - "other" - ], - "bn": [ - "one", - "other" - ], - "bo": [ - "other" - ], - "br": [ - "one", - "two", - "few", - "many", - "other" - ], - "brx": [ - "one", - "other" - ], - "bs": [ - "one", - "few", - "other" - ], - "ca": [ - "one", - "other" - ], - "ce": [ - "one", - "other" - ], - "ceb": [ - "one", - "other" - ], - "cgg": [ - "one", - "other" - ], - "chr": [ - "one", - "other" - ], - "ckb": [ - "one", - "other" - ], - "cs": [ - "one", - "few", - "many", - "other" - ], - "cy": [ - "zero", - "one", - "two", - "few", - "many", - "other" - ], - "da": [ - "one", - "other" - ], - "de": [ - "one", - "other" - ], - "dsb": [ - "one", - "two", - "few", - "other" - ], - "dv": [ - "one", - "other" - ], - "dz": [ - "other" - ], - "ee": [ - "one", - "other" - ], - "el": [ - "one", - "other" - ], - "en": [ - "one", - "other" - ], - "eo": [ - "one", - "other" - ], - "es": [ - "one", - "other" - ], - "et": [ - "one", - "other" - ], - "eu": [ - "one", - "other" - ], - "fa": [ - "one", - "other" - ], - "ff": [ - "one", - "other" - ], - "fi": [ - "one", - "other" - ], - "fil": [ - "one", - "other" - ], - "fo": [ - "one", - "other" - ], - "fr": [ - "one", - "other" - ], - "fur": [ - "one", - "other" - ], - "fy": [ - "one", - "other" - ], - "ga": [ - "one", - "two", - "few", - "many", - "other" - ], - "gd": [ - "one", - "two", - "few", - "other" - ], - "gl": [ - "one", - "other" - ], - "gsw": [ - "one", - "other" - ], - "gu": [ - "one", - "other" - ], - "guw": [ - "one", - "other" - ], - "gv": [ - "one", - "two", - "few", - "many", - "other" - ], - "ha": [ - "one", - "other" - ], - "haw": [ - "one", - "other" - ], - "he": [ - "one", - "two", - "many", - "other" - ], - "hi": [ - "one", - "other" - ], - "hr": [ - "one", - "few", - "other" - ], - "hsb": [ - "one", - "two", - "few", - "other" - ], - "hu": [ - "one", - "other" - ], - "hy": [ - "one", - "other" - ], - "ia": [ - "one", - "other" - ], - "id": [ - "other" - ], - "ig": [ - "other" - ], - "ii": [ - "other" - ], - "in": [ - "other" - ], - "io": [ - "one", - "other" - ], - "is": [ - "one", - "other" - ], - "it": [ - "one", - "other" - ], - "iu": [ - "one", - "two", - "other" - ], - "iw": [ - "one", - "two", - "many", - "other" - ], - "ja": [ - "other" - ], - "jbo": [ - "other" - ], - "jgo": [ - "one", - "other" - ], - "ji": [ - "one", - "other" - ], - "jmc": [ - "one", - "other" - ], - "jv": [ - "other" - ], - "jw": [ - "other" - ], - "ka": [ - "one", - "other" - ], - "kab": [ - "one", - "other" - ], - "kaj": [ - "one", - "other" - ], - "kcg": [ - "one", - "other" - ], - "kde": [ - "other" - ], - "kea": [ - "other" - ], - "kk": [ - "one", - "other" - ], - "kkj": [ - "one", - "other" - ], - "kl": [ - "one", - "other" - ], - "km": [ - "other" - ], - "kn": [ - "one", - "other" - ], - "ko": [ - "other" - ], - "ks": [ - "one", - "other" - ], - "ksb": [ - "one", - "other" - ], - "ksh": [ - "zero", - "one", - "other" - ], - "ku": [ - "one", - "other" - ], - "kw": [ - "zero", - "one", - "two", - "few", - "many", - "other" - ], - "ky": [ - "one", - "other" - ], - "lag": [ - "zero", - "one", - "other" - ], - "lb": [ - "one", - "other" - ], - "lg": [ - "one", - "other" - ], - "lkt": [ - "other" - ], - "ln": [ - "one", - "other" - ], - "lo": [ - "other" - ], - "lt": [ - "one", - "few", - "many", - "other" - ], - "lv": [ - "zero", - "one", - "other" - ], - "mas": [ - "one", - "other" - ], - "mg": [ - "one", - "other" - ], - "mgo": [ - "one", - "other" - ], - "mk": [ - "one", - "other" - ], - "ml": [ - "one", - "other" - ], - "mn": [ - "one", - "other" - ], - "mo": [ - "one", - "few", - "other" - ], - "mr": [ - "one", - "other" - ], - "ms": [ - "other" - ], - "mt": [ - "one", - "few", - "many", - "other" - ], - "my": [ - "other" - ], - "nah": [ - "one", - "other" - ], - "naq": [ - "one", - "two", - "other" - ], - "nb": [ - "one", - "other" - ], - "nd": [ - "one", - "other" - ], - "ne": [ - "one", - "other" - ], - "nl": [ - "one", - "other" - ], - "nn": [ - "one", - "other" - ], - "nnh": [ - "one", - "other" - ], - "no": [ - "one", - "other" - ], - "nqo": [ - "other" - ], - "nr": [ - "one", - "other" - ], - "nso": [ - "one", - "other" - ], - "ny": [ - "one", - "other" - ], - "nyn": [ - "one", - "other" - ], - "om": [ - "one", - "other" - ], - "or": [ - "one", - "other" - ], - "os": [ - "one", - "other" - ], - "osa": [ - "other" - ], - "pa": [ - "one", - "other" - ], - "pap": [ - "one", - "other" - ], - "pl": [ - "one", - "few", - "many", - "other" - ], - "prg": [ - "zero", - "one", - "other" - ], - "ps": [ - "one", - "other" - ], - "pt": [ - "one", - "other" - ], - "pt-PT": [ - "one", - "other" - ], - "rm": [ - "one", - "other" - ], - "ro": [ - "one", - "few", - "other" - ], - "rof": [ - "one", - "other" - ], - "root": [ - "other" - ], - "ru": [ - "one", - "few", - "many", - "other" - ], - "rwk": [ - "one", - "other" - ], - "sah": [ - "other" - ], - "saq": [ - "one", - "other" - ], - "sc": [ - "one", - "other" - ], - "scn": [ - "one", - "other" - ], - "sd": [ - "one", - "other" - ], - "sdh": [ - "one", - "other" - ], - "se": [ - "one", - "two", - "other" - ], - "seh": [ - "one", - "other" - ], - "ses": [ - "other" - ], - "sg": [ - "other" - ], - "sh": [ - "one", - "few", - "other" - ], - "shi": [ - "one", - "few", - "other" - ], - "si": [ - "one", - "other" - ], - "sk": [ - "one", - "few", - "many", - "other" - ], - "sl": [ - "one", - "two", - "few", - "other" - ], - "sma": [ - "one", - "two", - "other" - ], - "smi": [ - "one", - "two", - "other" - ], - "smj": [ - "one", - "two", - "other" - ], - "smn": [ - "one", - "two", - "other" - ], - "sms": [ - "one", - "two", - "other" - ], - "sn": [ - "one", - "other" - ], - "so": [ - "one", - "other" - ], - "sq": [ - "one", - "other" - ], - "sr": [ - "one", - "few", - "other" - ], - "ss": [ - "one", - "other" - ], - "ssy": [ - "one", - "other" - ], - "st": [ - "one", - "other" - ], - "su": [ - "other" - ], - "sv": [ - "one", - "other" - ], - "sw": [ - "one", - "other" - ], - "syr": [ - "one", - "other" - ], - "ta": [ - "one", - "other" - ], - "te": [ - "one", - "other" - ], - "teo": [ - "one", - "other" - ], - "th": [ - "other" - ], - "ti": [ - "one", - "other" - ], - "tig": [ - "one", - "other" - ], - "tk": [ - "one", - "other" - ], - "tl": [ - "one", - "other" - ], - "tn": [ - "one", - "other" - ], - "to": [ - "other" - ], - "tr": [ - "one", - "other" - ], - "ts": [ - "one", - "other" - ], - "tzm": [ - "one", - "other" - ], - "ug": [ - "one", - "other" - ], - "uk": [ - "one", - "few", - "many", - "other" - ], - "ur": [ - "one", - "other" - ], - "uz": [ - "one", - "other" - ], - "ve": [ - "one", - "other" - ], - "vi": [ - "other" - ], - "vo": [ - "one", - "other" - ], - "vun": [ - "one", - "other" - ], - "wa": [ - "one", - "other" - ], - "wae": [ - "one", - "other" - ], - "wo": [ - "other" - ], - "xh": [ - "one", - "other" - ], - "xog": [ - "one", - "other" - ], - "yi": [ - "one", - "other" - ], - "yo": [ - "other" - ], - "yue": [ - "other" - ], - "zh": [ - "other" - ], - "zu": [ - "one", - "other" - ], - "oc": - ["one", "other"], - "la": - ["one", "other"] -} diff --git a/qt/po/requirements.txt b/qt/po/requirements.txt deleted file mode 100644 index 23e5826d6..000000000 --- a/qt/po/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -polib diff --git a/qt/po/scripts/extract-po-strings.py b/qt/po/scripts/extract-po-strings.py deleted file mode 100644 index 934ea4581..000000000 --- a/qt/po/scripts/extract-po-strings.py +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: UTF-8 -*- - -import os -import json -import polib -import pprint -import re - -# Read strings from all .po and .pot files and store them in a JSON file -# for quick access. - -# returns a string, an array of plurals, or None if there's no translation -def get_msgstr(entry): - # non-empty single string? - if entry.msgstr: - return entry.msgstr - # plural string and non-empty? - elif entry.msgstr_plural and entry.msgstr_plural[0]: - # convert the dict into a list in the correct order - plurals = list(entry.msgstr_plural.items()) - plurals.sort() - # update variables and discard keys - adjusted = [] - for _k, msg in plurals: - assert msg - adjusted.append(msg) - if len(adjusted) > 1 and adjusted[0]: - return adjusted - else: - if adjusted[0]: - return adjusted[0] - return None - - -module_map = { - "__init__": "qt-misc", - "about": "about", - "addcards": "adding", - "addfield": "fields", - "addmodel": "notetypes", - "addonconf": "addons", - "addons": "addons", - "anki2": "importing", - "browser": "browsing", - "browserdisp": "browsing", - "browseropts": "browsing", - "changemap": "browsing", - "changemodel": "browsing", - "clayout_top": "card-templates", - "clayout": "card-templates", - "collection": "collection", - "consts": "consts", - "csvfile": "importing", - "customstudy": "custom-study", - "dconf": "scheduling", - "debug": "qt-misc", - "deckbrowser": "decks", - "deckchooser": "qt-misc", - "deckconf": "scheduling", - "decks": "decks", - "dyndconf": "decks", - "dyndeckconf": "decks", - "editaddon": "addons", - "editcurrent": "editing", - "edithtml": "editing", - "editor": "editing", - "errors": "qt-misc", - "exporting": "exporting", - "fields": "fields", - "finddupes": "browsing", - "findreplace": "browsing", - "getaddons": "addons", - "importing": "importing", - "latex": "media", - "main": "qt-misc", - "mnemo": "importing", - "modelchooser": "qt-misc", - "modelopts": "notetypes", - "models": "notetypes", - "noteimp": "importing", - "overview": "studying", - "preferences": "preferences", - "previewer": "qt-misc", - "profiles": "profiles", - "progress": "qt-misc", - "reposition": "browsing", - "reschedule": "browsing", - "reviewer": "studying", - "schedv2": "scheduling", - "setgroup": "browsing", - "setlang": "preferences", - "sidebar": "browsing", - "sound": "media", - "studydeck": "decks", - "taglimit": "custom-study", - "template": "card-templates", - "toolbar": "qt-misc", - "update": "qt-misc", - "utils": "qt-misc", - "webview": "qt-misc", - "stats": "statistics", -} - -text_remap = { - "actions": [ - "Add", - "Cancel", - "Choose", - "Close", - "Copy", - "Decks", - "Delete", - "Export", - "Filter", - "Help", - "Import", - "Manage...", - "Name:", - "New name:", - "New", - "Options for %s", - "Options", - "Preview", - "Rebuild", - "Rename Deck", - "Rename", - "Replay Audio", - "Reposition", - "Save", - "Search", - "Shortcut key: %s", - "Suspend Card", - "Blue Flag", - "Green Flag", - "Orange Flag", - "Red Flag", - "Custom Study", - ], - "decks": [ - "Deck", - "New deck name:", - "Decreasing intervals", - "Increasing intervals", - "Latest added first", - "Most lapses", - "Oldest seen first", - "Order added", - "Order due", - "Random", - "Relative overdueness", - ], - "scheduling": [ - "days", - "Lapses", - "Reviews", - "At least one step is required.", - "Steps must be numbers.", - "Show new cards in order added", - "Show new cards in random order", - "Show new cards after reviews", - "Show new cards before reviews", - "Mix new cards and reviews", - "Learning", - "Review", - ], - "fields": ["Add Field"], - "editing": ["Tags", "Cards", "Fields", "LaTeX"], - "notetypes": ["Type", "Note Types"], - "studying": ["Space"], - "qt-misc": ["&Edit", "&Guide...", "&Help", "&Undo", "Unexpected response code: %s"], - "adding": ["Added"], -} - -blacklist = {"Anki", "%", "Dialog", "Center", "Left", "Right", "~", "&Cram..."} - - -def determine_module(text, files): - if text in blacklist: - return None - - if "&" in text: - return "qt-accel" - - for (module, texts) in text_remap.items(): - if text in texts: - return module - - if len(files) == 1: - return list(files)[0] - - assert False - - -modules = dict() - -remap_keys = { - "browsing-": "browsing-type-here-to-search", - "importing-": "importing-ignored", - "qt-misc-": "qt-misc-non-unicode-text", -} - - -def generate_key(module: str, text: str) -> str: - key = re.sub("<.*?>", "", text) - key = re.sub("%[dsf.0-9]+", "", key) - key = key.replace("+", "and") - key = re.sub("[^a-z0-9 ]", "", key.lower()) - words = key.split(" ") - if len(words) > 6: - words = words[:6] - key = "-".join(words) - key = re.sub("--+", "-", key) - key = re.sub("-$|^-", "", key) - - key = f"{module}-{key}" - - if key in remap_keys: - key = remap_keys[key] - - return key - - -seen_keys = set() - - -def migrate_entry(entry): - if entry.msgid_plural: - # print("skip plural", entry.msgid) - return - - entry.occurrences = [e for e in entry.occurrences if "aqt/stats.py" in e[0]] - if not entry.occurrences: - return None - - print(entry.occurrences) - text = entry.msgid - files = set( - [os.path.splitext(os.path.basename(e[0]))[0] for e in entry.occurrences] - ) - - files2 = set() - for file in files: - file = module_map[file] - files2.add(file) - module = determine_module(text, files2) - if not module: - return - - key = generate_key(module, text) - - if key in seen_keys: - key += "2" - assert key not in seen_keys - seen_keys.add(key) - - modules.setdefault(module, []) - modules[module].append((key, text)) - - return None - - -langs = {} - -# .pot first -base = "../../../anki-i18n/qtpo/desktop" -pot = os.path.join(base, "anki.pot") -pot_cat = polib.pofile(pot) - -migration_map = [] - -for entry in pot_cat: - if entry.msgid_plural: - msgstr = [entry.msgid, entry.msgid_plural] - else: - msgstr = entry.msgid - - langs.setdefault("en", {})[entry.msgid] = msgstr - - if d := migrate_entry(entry): - migration_map.append(d) - - -# then .po files -folders = (d for d in os.listdir(base) if d != "anki.pot") -for lang in folders: - po_path = os.path.join(base, lang, "anki.po") - cat = polib.pofile(po_path) - for entry in cat: - msgstr = get_msgstr(entry) - if not msgstr: - continue - langs.setdefault(lang, {})[entry.msgid] = msgstr - -with open("strings.json", "w") as file: - file.write(json.dumps(langs)) -print("wrote to strings.json") - -# old text -> (module, new key) -strings_by_module = {} -keys_by_text = {} -for (module, items) in modules.items(): - items.sort() - strings_by_module[module] = items - for item in items: - (key, text) = item - assert text not in keys_by_text - keys_by_text[text] = (module, key) - -with open("strings_by_module.json", "w") as file: - file.write(json.dumps(strings_by_module)) -with open("keys_by_text.json", "w") as file: - file.write(json.dumps(keys_by_text)) diff --git a/qt/po/scripts/extract_po_string.py b/qt/po/scripts/extract_po_string.py deleted file mode 100644 index adbea66b5..000000000 --- a/qt/po/scripts/extract_po_string.py +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: UTF-8 -*- - -import os -import json -import re -import sys -import polib -import shutil -import sys -import subprocess -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from extract_po_string_diag import Ui_Dialog -from fluent.syntax import parse, serialize -from fluent.syntax.ast import Message, TextElement, Identifier, Pattern, Junk - -# the templates folder inside the ftl repo -repo_templates_dir = sys.argv[1] -assert os.path.abspath(repo_templates_dir).endswith("templates") -strings = json.load(open("strings.json" if len(sys.argv) < 3 else sys.argv[2])) -plurals = json.load(open("plurals.json")) - - -def transform_entry(entry, replacements): - if isinstance(entry, str): - return transform_string(entry, replacements) - else: - return [transform_string(e, replacements) for e in entry] - - -def transform_string(msg, replacements): - try: - for (old, new) in replacements: - msg = msg.replace(old, f"{new}") - except ValueError: - pass - # strip leading/trailing whitespace - return msg.strip() - - -def plural_text(key, lang, translation): - lang = re.sub("(_|-).*", "", lang) - - # extract the variable - if there's more than one, use the first one - var = re.findall(r"{(\$.*?)}", translation[0]) - if not len(var) == 1: - print("multiple variables found, using first replacement") - var = replacements[0][1].replace("{", "").replace("}", "") - else: - var = var[0] - - buf = f"{key} = {{ {var} ->\n" - - # for each of the plural forms except the last - for idx, msg in enumerate(translation[:-1]): - plural_form = plurals[lang][idx] - buf += f" [{plural_form}] {msg}\n" - - # add the catchall - msg = translation[-1] - buf += f" *[other] {msg}\n" - buf += " }\n" - return buf - - -def key_from_search(search): - return search.replace(" ", "-").replace("'", "") - - -# add a non-pluralized message. works via fluent.syntax, so can automatically -# indent, etc -def add_simple_message(fname, key, message): - orig = "" - if os.path.exists(fname): - with open(fname) as file: - orig = file.read() - - obj = parse(orig) - for ent in obj.body: - if isinstance(ent, Junk): - raise Exception(f"file had junk! {fname} {ent}") - obj.body.append(Message(Identifier(key), Pattern([TextElement(message)]))) - - modified = serialize(obj, with_junk=True) - # escape leading dots - modified = re.sub(r"(?ms)^( +)\.", '\\1{"."}', modified) - - # ensure the resulting serialized file is valid by parsing again - obj = parse(modified) - for ent in obj.body: - if isinstance(ent, Junk): - raise Exception(f"introduced junk! {fname} {ent}") - - # it's ok, write it out - with open(fname, "w") as file: - file.write(modified) - - -def add_message(fname, key, translation): - # simple, non-plural form? - if isinstance(translation, str): - add_simple_message(fname, key, translation) - else: - text = plural_text(key, lang, translation) - open(fname, "a").write(text) - - -def key_already_used(key: str) -> bool: - return not subprocess.call(["grep", "-r", f"{key} =", repo_templates_dir]) - - -class Window(QDialog, Ui_Dialog): - def __init__(self): - QDialog.__init__(self) - self.setupUi(self) - - self.matched_strings = [] - - self.files = sorted(os.listdir(repo_templates_dir)) - self.filenames.addItems(self.files) - - self.search.textChanged.connect(self.on_search) - self.replacements.textChanged.connect(self.update_preview) - self.key.textEdited.connect(self.update_preview) - self.filenames.currentIndexChanged.connect(self.update_preview) - self.searchMatches.currentItemChanged.connect(self.update_preview) - self.replacementsTemplateButton.clicked.connect(self.on_template) - self.addButton.clicked.connect(self.on_add) - - def on_template(self): - self.replacements.setText("%d={ $value }") - # qt macos bug - self.replacements.repaint() - - def on_search(self): - msgid_substring = self.search.text() - self.key.setText(key_from_search(msgid_substring)) - - msgids = [] - exact_idx = None - for n, id in enumerate(strings["en"].keys()): - if msgid_substring.lower() in id.lower(): - msgids.append(id) - - # is the ID an exact match? - if msgid_substring in strings["en"]: - exact_idx = n - - self.matched_strings = msgids - self.searchMatches.clear() - self.searchMatches.addItems(self.matched_strings) - if exact_idx is not None: - self.searchMatches.setCurrentRow(exact_idx) - elif self.matched_strings: - self.searchMatches.setCurrentRow(0) - - self.update_preview() - - def update_preview(self): - self.preview.clear() - if not self.matched_strings: - return - - strings = self.get_adjusted_strings() - key = self.get_key() - self.preview.setPlainText( - f"Key: {key}\n\n" - + "\n".join([f"{lang}: {value}" for (lang, value) in strings]) - ) - - # returns list of (lang, entry) - def get_adjusted_strings(self): - msgid = self.matched_strings[self.searchMatches.currentRow()] - - # split up replacements - replacements = [] - for repl in self.replacements.text().split(","): - if not repl: - continue - replacements.append(repl.split("=")) - - to_insert = [] - for lang in strings.keys(): - entry = strings[lang].get(msgid) - if not entry: - continue - entry = transform_entry(entry, replacements) - if entry: - to_insert.append((lang, entry)) - - return to_insert - - def get_key(self): - # add file as prefix to key - prefix = os.path.splitext(self.filenames.currentText())[0] - return f"{prefix}-{self.key.text()}" - - def on_add(self): - to_insert = self.get_adjusted_strings() - key = self.get_key() - if key_already_used(key): - QMessageBox.warning(None, "Error", "Duplicate Key") - return - - # for each language's translation - for lang, translation in to_insert: - ftl_path = self.filename_for_lang(lang) - add_message(ftl_path, key, translation) - - if lang == "en": - # copy file from repo into src - srcdir = os.path.join(repo_templates_dir, "..", "..", "..") - src_filename = os.path.join(srcdir, os.path.basename(ftl_path)) - shutil.copy(ftl_path, src_filename) - - subprocess.check_call( - f"cd {repo_templates_dir} && git add .. && git commit -m 'add {key}'", - shell=True, - ) - - self.preview.setPlainText(f"Added {key}.") - self.preview.repaint() - - def filename_for_lang(self, lang): - fname = self.filenames.currentText() - if lang == "en": - return os.path.join(repo_templates_dir, fname) - else: - ftl_dir = os.path.join(repo_templates_dir, "..", lang) - if not os.path.exists(ftl_dir): - os.mkdir(ftl_dir) - return os.path.join(ftl_dir, fname) - - -print("Remember to pull-i18n before making changes.") -if subprocess.check_output(f"git status --porcelain {repo_templates_dir}", shell=True): - print("Repo has uncommitted changes.") - sys.exit(1) - -app = QApplication(sys.argv) -window = Window() -window.show() -sys.exit(app.exec_()) diff --git a/qt/po/scripts/extract_po_string_diag.py b/qt/po/scripts/extract_po_string_diag.py deleted file mode 100644 index e7ca71fea..000000000 --- a/qt/po/scripts/extract_po_string_diag.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'extract_po_string_diag.ui' -# -# Created by: PyQt5 UI code generator 5.15.0 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(879, 721) - self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") - self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setObjectName("gridLayout") - self.label_2 = QtWidgets.QLabel(Dialog) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) - self.filenames = QtWidgets.QComboBox(Dialog) - self.filenames.setObjectName("filenames") - self.gridLayout.addWidget(self.filenames, 1, 1, 1, 1) - self.label_4 = QtWidgets.QLabel(Dialog) - self.label_4.setObjectName("label_4") - self.gridLayout.addWidget(self.label_4, 0, 0, 1, 1) - self.replacementsTemplateButton = QtWidgets.QPushButton(Dialog) - self.replacementsTemplateButton.setObjectName("replacementsTemplateButton") - self.gridLayout.addWidget(self.replacementsTemplateButton, 4, 1, 1, 1) - self.label = QtWidgets.QLabel(Dialog) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 1, 0, 1, 1) - self.label_3 = QtWidgets.QLabel(Dialog) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1) - self.replacements = QtWidgets.QLineEdit(Dialog) - self.replacements.setObjectName("replacements") - self.gridLayout.addWidget(self.replacements, 3, 1, 1, 1) - self.key = QtWidgets.QLineEdit(Dialog) - self.key.setObjectName("key") - self.gridLayout.addWidget(self.key, 2, 1, 1, 1) - self.search = QtWidgets.QLineEdit(Dialog) - self.search.setObjectName("search") - self.gridLayout.addWidget(self.search, 0, 1, 1, 1) - self.verticalLayout.addLayout(self.gridLayout) - self.label_5 = QtWidgets.QLabel(Dialog) - self.label_5.setObjectName("label_5") - self.verticalLayout.addWidget(self.label_5) - self.searchMatches = QtWidgets.QListWidget(Dialog) - self.searchMatches.setObjectName("searchMatches") - self.verticalLayout.addWidget(self.searchMatches) - self.label_6 = QtWidgets.QLabel(Dialog) - self.label_6.setObjectName("label_6") - self.verticalLayout.addWidget(self.label_6) - self.preview = QtWidgets.QTextEdit(Dialog) - self.preview.setObjectName("preview") - self.verticalLayout.addWidget(self.preview) - self.addButton = QtWidgets.QPushButton(Dialog) - self.addButton.setObjectName("addButton") - self.verticalLayout.addWidget(self.addButton) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - Dialog.setTabOrder(self.search, self.filenames) - Dialog.setTabOrder(self.filenames, self.key) - Dialog.setTabOrder(self.key, self.replacements) - Dialog.setTabOrder(self.replacements, self.replacementsTemplateButton) - Dialog.setTabOrder(self.replacementsTemplateButton, self.searchMatches) - Dialog.setTabOrder(self.searchMatches, self.preview) - Dialog.setTabOrder(self.preview, self.addButton) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.label_2.setText(_translate("Dialog", "Fluent key suffix")) - self.label_4.setText(_translate("Dialog", "Search:")) - self.replacementsTemplateButton.setText(_translate("Dialog", "Template")) - self.label.setText(_translate("Dialog", "Target file")) - self.label_3.setText(_translate("Dialog", "Replacements")) - self.label_5.setText(_translate("Dialog", "Matches")) - self.label_6.setText(_translate("Dialog", "Preview")) - self.addButton.setText(_translate("Dialog", "Add")) diff --git a/qt/po/scripts/extract_po_string_diag.ui b/qt/po/scripts/extract_po_string_diag.ui deleted file mode 100644 index c3c9f10d3..000000000 --- a/qt/po/scripts/extract_po_string_diag.ui +++ /dev/null @@ -1,109 +0,0 @@ - - - Dialog - - - - 0 - 0 - 879 - 721 - - - - Dialog - - - - - - - - Fluent key suffix - - - - - - - - - - Search: - - - - - - - Template - - - - - - - Target file - - - - - - - Replacements - - - - - - - - - - - - - - - - - - Matches - - - - - - - - - - Preview - - - - - - - - - - Add - - - - - - - search - filenames - key - replacements - replacementsTemplateButton - searchMatches - preview - addButton - - - - diff --git a/qt/po/scripts/rewrite-refs.py b/qt/po/scripts/rewrite-refs.py deleted file mode 100644 index 5746be453..000000000 --- a/qt/po/scripts/rewrite-refs.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python - -import glob, re, json, stringcase - -files = ( - # glob.glob("../../pylib/**/*.py", recursive=True) + - glob.glob("../../qt/**/*.py", recursive=True) -) -string_re = re.compile(r'_\(\s*(".*?")\s*\)', re.DOTALL) - -map = json.load(open("keys_by_text.json")) - -# unused or missing strings -blacklist = { - "Label1", - "After pressing OK, you can choose which tags to include.", - "Filter/Cram", - # previewer.py needs updating to fix these - "Shortcut key: R", - "Shortcut key: B", -} - - -def repl(m): - # the argument may consistent of multiple strings that need merging together - text = eval("(" + m.group(1) + ")") - print(f"text is `{text}`") - - if text in blacklist: - return m.group(0) - - (module, key) = map[text] - screaming = stringcase.constcase(key) - print(screaming) - - if "%d" in text or "%s" in text: - # replace { $val } with %s for compat with old code - return f'tr(TR.{screaming}, val="%s")' - - return f"tr(TR.{screaming})" - - -for file in files: - buf = open(file).read() - buf2 = string_re.sub(repl, buf) - if buf != buf2: - lines = buf2.split("\n") - lines.insert(3, "from aqt.utils import tr, TR") - buf2 = "\n".join(lines) - open(file, "w").write(buf2) diff --git a/qt/po/scripts/write-ftls.py b/qt/po/scripts/write-ftls.py deleted file mode 100644 index 2df0acc1e..000000000 --- a/qt/po/scripts/write-ftls.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python - -import json -import os - -from fluent.syntax import parse, serialize -from fluent.syntax.ast import Message, TextElement, Identifier, Pattern, Junk - -core = "../../../anki-i18n/core/core" -qt = "../../../anki-i18n/qtftl/desktop" - -qt_modules = {"about", "qt-accel", "addons", "qt-misc"} - -modules = json.load(open("strings_by_module.json")) -translations = json.load(open("strings.json")) - -# # fixme: -# addons addons-downloaded-fnames Downloaded %(fname)s -# addons addons-downloading-adbd-kb02fkb Downloading %(a)d/%(b)d (%(kb)0.2fKB)... -# addons addons-error-downloading-ids-errors Error downloading %(id)s: %(error)s -# addons addons-error-installing-bases-errors Error installing %(base)s: %(error)s -# addons addons-important-as-addons-are-programs-downloaded Important: As add-ons are programs downloaded from the internet, they are potentially malicious.You should only install add-ons you trust.

Are you sure you want to proceed with the installation of the following Anki add-on(s)?

%(names)s -# addons addons-installed-names Installed %(name)s -# addons addons-the-following-addons-are-incompatible-with The following add-ons are incompatible with %(name)s and have been disabled: %(found)s -# card-templates card-templates-delete-the-as-card-type-and Delete the '%(a)s' card type, and its %(b)s? -# browsing browsing-found-as-across-bs Found %(a)s across %(b)s. -# browsing browsing-nd-names %(n)d: %(name)s -# importing importing-rows-had-num1d-fields-expected-num2d '%(row)s' had %(num1)d fields, expected %(num2)d -# about about-written-by-damien-elmes-with-patches Written by Damien Elmes, with patches, translation, testing and design from:

%(cont)s -# media media-recordingtime Recording...
Time: %0.1f -# addons-unknown-error = Unknown error: {} - -# fixme: isolation chars - - -def transform_text(text: str) -> str: - # fixme: automatically remap to %s in a compat wrapper? manually fix? - text = ( - text.replace("%d", "{ $val }") - .replace("%s", "{ $val }") - .replace("{}", "{ $val }") - ) - if "Clock drift" in text: - text = text.replace("\n", "
") - else: - text = text.replace("\n", " ") - return text - - -def check_parses(path: str): - # make sure the file parses - with open(path) as file: - orig = file.read() - obj = parse(orig) - for ent in obj.body: - if isinstance(ent, Junk): - raise Exception(f"file had junk! {path} {ent}") - - -for module, items in modules.items(): - if module in qt_modules: - folder = qt - else: - folder = core - - if not module.startswith("statistics"): - continue - - # templates - path = os.path.join(folder, "templates", module + ".ftl") - print(path) - with open(path, "a", encoding="utf8") as file: - for (key, text) in items: - text2 = transform_text(text) - file.write(f"{key} = {text2}\n") - - check_parses(path) - - # translations - for (lang, map) in translations.items(): - if lang == "en": - continue - - out = [] - for (key, text) in items: - if text in map: - out.append((key, transform_text(map[text]))) - - if out: - path = os.path.join(folder, lang, module + ".ftl") - dir = os.path.dirname(path) - if not os.path.exists(dir): - os.mkdir(dir) - - print(path) - with open(path, "a", encoding="utf8") as file: - for (key, text) in out: - file.write(f"{key} = {text}\n") - - check_parses(path) diff --git a/repos.bzl b/repos.bzl index 3de5f93b1..37f03f868 100644 --- a/repos.bzl +++ b/repos.bzl @@ -126,9 +126,6 @@ def register_repos(): qtftl_i18n_commit = "9909cfa4386288e686b2336b3b1048b7ee1bb194" qtftl_i18n_shallow_since = "1605664969 +1000" - qtpo_i18n_commit = "872d7f0f6bde52577e8fc795dd85699b0eeb97d5" - qtpo_i18n_shallow_since = "1605564627 +0000" - i18n_build_content = """ filegroup( name = "files", @@ -166,15 +163,3 @@ exports_files(["l10n.toml"]) shallow_since = qtftl_i18n_shallow_since, remote = "https://github.com/ankitects/anki-desktop-ftl", ) - - new_git_repository( - name = "aqt_po", - build_file_content = """ -exports_files(glob(["**/*.pot", "**/*.po"]), -visibility = ["//visibility:public"], -) - """, - commit = qtpo_i18n_commit, - shallow_since = qtpo_i18n_shallow_since, - remote = "https://github.com/ankitects/anki-desktop-i18n", - ) diff --git a/run b/run index 422e9fcab..c5cdaa5be 100755 --- a/run +++ b/run @@ -11,7 +11,7 @@ run_mac() { # so we need to copy the files into a working folder before running on a Mac. workspace=$(dirname $0) bazel build //qt:runanki && \ - rsync -avPL --exclude=anki/external --exclude=__pycache__ --delete \ + rsync -aiL --no-times --exclude=anki/external --exclude=__pycache__ --delete \ $workspace/bazel-bin/qt/runanki* $workspace/bazel-copy/ && \ $workspace/bazel-copy/runanki $* } diff --git a/qt/po/scripts/duplicate-string.py b/scripts/duplicate-string.py similarity index 100% rename from qt/po/scripts/duplicate-string.py rename to scripts/duplicate-string.py