strip out unused gettext refs

This commit is contained in:
Damien Elmes 2020-11-18 13:22:51 +10:00
parent ab69ca31ec
commit 1c5f94d46f
23 changed files with 18 additions and 2020 deletions

View file

@ -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

View file

@ -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.

View file

@ -14,7 +14,7 @@ Install Homebrew from <https://brew.sh/>
Then install deps:
```
$ brew install rsync gettext bazelisk
$ brew install rsync bazelisk
```
**Install Python 3.8**:

View file

@ -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**:

View file

@ -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

View file

@ -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)

View file

@ -1,7 +1,6 @@
filegroup(
name = "data",
srcs = [
"//qt/aqt/data/locale",
"//qt/aqt/data/web",
],
visibility = ["//qt:__subpackages__"],

View file

@ -1,6 +0,0 @@
load("//qt/po:gettext.bzl", "compile_all_po_files")
compile_all_po_files(
name = "locale",
visibility = ["//qt:__subpackages__"],
)

View file

@ -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

1
qt/po/.gitignore vendored
View file

@ -1 +0,0 @@
strings*.json

View file

@ -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__"],
)

View file

@ -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,
)

View file

@ -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"]
}

View file

@ -1 +0,0 @@
polib

View file

@ -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))

View file

@ -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_())

View file

@ -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"))

View file

@ -1,109 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>879</width>
<height>721</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Fluent key suffix</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="filenames"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Search:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="replacementsTemplateButton">
<property name="text">
<string>Template</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Target file</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Replacements</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="replacements"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="key"/>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="search"/>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Matches</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="searchMatches"/>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Preview</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="preview"/>
</item>
<item>
<widget class="QPushButton" name="addButton">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>search</tabstop>
<tabstop>filenames</tabstop>
<tabstop>key</tabstop>
<tabstop>replacements</tabstop>
<tabstop>replacementsTemplateButton</tabstop>
<tabstop>searchMatches</tabstop>
<tabstop>preview</tabstop>
<tabstop>addButton</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View file

@ -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)

View file

@ -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 <i>%(id)s</i>: %(error)s
# addons addons-error-installing-bases-errors Error installing <i>%(base)s</i>: %(error)s
# addons addons-important-as-addons-are-programs-downloaded <b>Important</b>: As add-ons are programs downloaded from the internet, they are potentially malicious.<b>You should only install add-ons you trust.</b><br><br>Are you sure you want to proceed with the installation of the following Anki add-on(s)?<br><br>%(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:<p>%(cont)s
# media media-recordingtime Recording...<br>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", "<br>")
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)

View file

@ -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",
)

2
run
View file

@ -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 $*
}