diff --git a/.gitignore b/.gitignore index 89aa3d91c..48fe9de45 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,3 @@ build pyenv .mypy_cache __pycache__ -i18n diff --git a/qt/.gitignore b/qt/.gitignore index 8c4b83451..85e37d52a 100644 --- a/qt/.gitignore +++ b/qt/.gitignore @@ -22,3 +22,4 @@ aqt_data/web/webview.js dist aqt.egg-info build +i18n/anki.pot diff --git a/qt/i18n/.gitignore b/qt/i18n/.gitignore new file mode 100644 index 000000000..24e5b0a1a --- /dev/null +++ b/qt/i18n/.gitignore @@ -0,0 +1 @@ +.build diff --git a/qt/i18n/Makefile b/qt/i18n/Makefile new file mode 100644 index 000000000..c2d6607a9 --- /dev/null +++ b/qt/i18n/Makefile @@ -0,0 +1,21 @@ +SHELL := bash +.SHELLFLAGS := -eu -o pipefail -c +.DELETE_ON_ERROR: +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules + +$(shell mkdir -p .build) + +.PHONY: develop +develop: .build/develop + +.PHONY: clean +clean: + +.PHONY: build +build: + +.build/develop: $(wildcard translations/anki.pot/*) + ./build-mo-files + ./copy-qt-files + @touch $@ diff --git a/qt/i18n/build-mo-files b/qt/i18n/build-mo-files new file mode 100755 index 000000000..5de6492b6 --- /dev/null +++ b/qt/i18n/build-mo-files @@ -0,0 +1,17 @@ +#!/bin/bash +# +# build mo files +# + +targetDir="../anki-qt/aqt_data/locale" +mkdir -p $targetDir + +echo "Compiling *.po..." +for file in translations/anki.pot/* +do + outdir=$(echo $file | \ + perl -pe "s%translations/anki.pot/(.*)%$targetDir/\1/LC_MESSAGES%") + outfile="$outdir/anki.mo" + mkdir -p $outdir + msgfmt $file --output-file=$outfile +done diff --git a/qt/i18n/check-po-files.py b/qt/i18n/check-po-files.py new file mode 100644 index 000000000..68f397da6 --- /dev/null +++ b/qt/i18n/check-po-files.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# +# locate any translations that have invalid format strings +# + +import os, re, sys +po_dir = "translations/anki.pot" + +msg_re = re.compile(r"^(msgid|msgid_plural|msgstr|)(\[[\d]\])? \"(.*)\"$") +cont_re = re.compile(r"^\"(.*)\"$") +pct_re = re.compile(r"%(?:\([^\)]+?\))?[#+\-\d.]*[a-zA-Z]") +path_re = re.compile(r"#: (.+)$") + +def check_reps(path, t1, strs): + allZero = True + for s in strs: + if s: + allZero = False + if allZero: + return + + orig = t1 or "" + for s in strs: + if not reps_match(orig, s): + return "{}\n{}\n{}\n".format(path, orig, strs) + +def reps_match(t1, t2): + for char in "{}": + if t1.count(char) != t2.count(char): + return False + + t1 = t1.replace("%%", "") + t2 = t2.replace("%%", "") + + if t1.count("%"): + matches = set(pct_re.findall(t1)) + matches2 = set(pct_re.findall(t2)) + return matches == matches2 + + return True + + +def fix_po(path): + last_msgid = None + last_msgstr = None + last_path = None + lines = [] + state = "outside" + problems = [] + strs = [] + + for line in open(path): + lines.append(line) + + # comment? + m = path_re.match(line) + if m: + last_path = m.group(1) + + # starting new id/str? + m = msg_re.match(line) + if m: + label, num, text = m.group(1), m.group(2), m.group(3) + if label == "msgid": + last_msgid = text + state = "id" + strs = [] + elif label == "msgid_plural": + continue + else: + state = "str" + strs.append(text) + + continue + + # continuing previous id/str? + m = cont_re.match(line) + if m: + if state == "id": + last_msgid += m.group(1) + elif state == "str": + strs[-1] += m.group(1) + else: + assert 0 + + continue + + state = "outside" + if last_msgid: + p = check_reps(last_path, last_msgid, strs) + if p: + problems.append(p) +# print("{0}\nProblems in {1}:\n{0}\n{2}".format("*"*60, path, "\n".join(problems))) +# return 1 + last_msgid = None + + if problems: + print("{0}\nProblems in {1}:\n{0}\n{2}".format("*"*60, path, "\n".join(problems))) + + return len(problems) + +problems = 0 +for po in os.listdir(po_dir): + path = os.path.join(po_dir, po) + problems += fix_po(path) + +if problems: + sys.exit(1) diff --git a/qt/i18n/copy-qt-files b/qt/i18n/copy-qt-files new file mode 100755 index 000000000..8ad7e21b4 --- /dev/null +++ b/qt/i18n/copy-qt-files @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +out=../anki-qt/aqt_data/locale +mkdir -p $out + +qtTranslations=$(python -c "from PyQt5.QtCore import *; print(QLibraryInfo.location(QLibraryInfo.TranslationsPath))") +rsync -av $qtTranslations/qt* $out diff --git a/qt/i18n/update-crowdin b/qt/i18n/update-crowdin new file mode 100755 index 000000000..fc9b8d341 --- /dev/null +++ b/qt/i18n/update-crowdin @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +proj=anki + +if [ "$key" = "" ]; then + echo "key not defined" + exit 1 +fi + +./update-pot + +curl \ + -F "files[/anki.pot]=@anki.pot" \ + https://api.crowdin.com/api/project/$proj/update-file?key=$key diff --git a/qt/i18n/update-from-crowdin b/qt/i18n/update-from-crowdin new file mode 100755 index 000000000..af1495d1d --- /dev/null +++ b/qt/i18n/update-from-crowdin @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e + +proj=anki + +if [ "$key" = "" ]; then + echo "key not defined" + exit 1 +fi + +# fetch translations from crowdin +if [ ! -f all.zip ]; then + curl https://api.crowdin.com/api/project/$proj/export?key=$key + curl -o all.zip https://api.crowdin.com/api/project/$proj/download/all.zip?key=$key +fi + +# unzip +unzip -o all.zip + +# make sure translations are valid +python check-po-files.py + +rm all.zip + +# send translations to github +git add translations +git commit -m update || true +git push diff --git a/qt/i18n/update-pot b/qt/i18n/update-pot new file mode 100755 index 000000000..a90c8c7b0 --- /dev/null +++ b/qt/i18n/update-pot @@ -0,0 +1,16 @@ +#!/bin/bash +# +# update translation files +# + +all=all.files +echo "Updating anki.pot..." +for i in ../anki-lib-python/anki/{*.py,importing/*.py,template/*.py}; do + echo $i >> $all +done +for i in ../anki-qt/aqt/{*.py,forms/*.py}; do + echo $i >> $all +done + +xgettext -cT: -s --no-wrap --files-from=$all --output=anki.pot +rm $all