diff --git a/qt/po/scripts/extract-po-string.py b/qt/po/scripts/extract-po-string.py deleted file mode 100644 index 488c2d97a..000000000 --- a/qt/po/scripts/extract-po-string.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: UTF-8 -*- -import os -import json -import re -import sys -import polib -import shutil -from fluent.syntax import parse, serialize -from fluent.syntax.ast import Message, TextElement, Identifier, Pattern, Junk - -# extract a translated string from strings.json and insert it into ftl -# eg: -# $ python extract-po-string.py strings.json /path/to/templates/media-check.ftl delete-unused "Delete Unused Media" "" -# $ python extract-po-string.py strings.json /path/to/templates/media-check.ftl delete-unused "%(a)s %(b)s" "%(a)s=$val1,%(b)s=$val2" - -json_filename, ftl_filename, key, msgid_substring, repls = sys.argv[1:] - -# split up replacements -replacements = [] -for repl in repls.split(","): - if not repl: - continue - replacements.append(repl.split("=")) - -# add file as prefix to key -prefix = os.path.splitext(os.path.basename(ftl_filename))[0] -key = f"{prefix}-{key}" - -strings = json.load(open(json_filename, "r")) - -msgids = [] -if msgid_substring in strings["en"]: - # is the ID an exact match? - msgids.append(msgid_substring) -else: - for id in strings["en"].keys(): - if msgid_substring in id: - msgids.append(id) - -msgid = None -if len(msgids) == 0: - print("no IDs matched") - sys.exit(1) -elif len(msgids) == 1: - msgid = msgids[0] -else: - for c, id in enumerate(msgids): - print(f"* {c}: {id}") - msgid = msgids[int(input("number to use? "))] - - -def transform_entry(entry): - if isinstance(entry, str): - return transform_string(entry) - else: - return [transform_string(e) for e in entry] - - -def transform_string(msg): - for (old, new) in replacements: - msg = msg.replace(old, f"{new}") - # strip leading/trailing whitespace - return msg.strip() - - -to_insert = [] -for lang in strings.keys(): - entry = strings[lang].get(msgid) - if not entry: - continue - entry = transform_entry(entry) - if entry: - print(f"{lang} had translation {entry}") - to_insert.append((lang, entry)) - -plurals = json.load(open("plurals.json")) - - -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 - - -# 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) - -print() -input("proceed? ctrl+c to abort") - -i18ndir = os.path.join(os.path.dirname(ftl_filename), "..") - -# for each language's translation -for lang, translation in to_insert: - if lang == "en": - # template - ftl_path = ftl_filename - else: - # translation - ftl_path = ftl_filename.replace("templates", lang) - ftl_dir = os.path.dirname(ftl_path) - - if not os.path.exists(ftl_dir): - os.mkdir(ftl_dir) - - add_message(ftl_path, key, translation) - -# copy file from repo into src -srcdir = os.path.join(i18ndir, "..", "..") -src_filename = os.path.join(srcdir, os.path.basename(ftl_filename)) -shutil.copy(ftl_filename, src_filename) - -print("done") diff --git a/qt/po/scripts/extract_po_string.py b/qt/po/scripts/extract_po_string.py new file mode 100644 index 000000000..a10300d38 --- /dev/null +++ b/qt/po/scripts/extract_po_string.py @@ -0,0 +1,238 @@ +#!/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")) +plurals = json.load(open("plurals.json")) +# i18ndir = os.path.join(ftl_dir, "..") + + +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) + + +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() + # 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 new file mode 100644 index 000000000..e7ca71fea --- /dev/null +++ b/qt/po/scripts/extract_po_string_diag.py @@ -0,0 +1,86 @@ +# -*- 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 new file mode 100644 index 000000000..c3c9f10d3 --- /dev/null +++ b/qt/po/scripts/extract_po_string_diag.ui @@ -0,0 +1,109 @@ + + + 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 + + + +