begin plural string migration

This commit is contained in:
Damien Elmes 2020-11-17 23:24:19 +10:00
parent 2453e5c488
commit e527d31dfc
12 changed files with 166 additions and 36 deletions

View File

@ -24,7 +24,7 @@ import anki
import aqt import aqt
import aqt.forms import aqt.forms
from anki.httpclient import HttpClient from anki.httpclient import HttpClient
from anki.lang import _, ngettext, without_unicode_isolation from anki.lang import ngettext, without_unicode_isolation
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
@ -275,10 +275,7 @@ class AddonManager:
if conflicting: if conflicting:
addons = ", ".join(self.addonName(f) for f in conflicting) addons = ", ".join(self.addonName(f) for f in conflicting)
showInfo( showInfo(
_( tr(TR.ADDONS_THE_FOLLOWING_ADDONS_ARE_INCOMPATIBLE_WITH)
"The following add-ons are incompatible with %(name)s \
and have been disabled: %(found)s"
)
% dict(name=addon.human_name(), found=addons), % dict(name=addon.human_name(), found=addons),
textFormat="plain", textFormat="plain",
) )
@ -468,7 +465,9 @@ and have been disabled: %(found)s"
"manifest": tr(TR.ADDONS_INVALID_ADDON_MANIFEST), "manifest": tr(TR.ADDONS_INVALID_ADDON_MANIFEST),
} }
msg = messages.get(result.errmsg, _("Unknown error: {}".format(result.errmsg))) msg = messages.get(
result.errmsg, tr(TR.ADDONS_UNKNOWN_ERROR, val=result.errmsg)
)
if mode == "download": # preserve old format strings for i18n if mode == "download": # preserve old format strings for i18n
template = tr(TR.ADDONS_ERROR_DOWNLOADING_IDS_ERRORS) template = tr(TR.ADDONS_ERROR_DOWNLOADING_IDS_ERRORS)
@ -1400,13 +1399,9 @@ def installAddonPackages(
if warn: if warn:
names = ",<br>".join(f"<b>{os.path.basename(p)}</b>" for p in paths) names = ",<br>".join(f"<b>{os.path.basename(p)}</b>" for p in paths)
q = _( q = tr(TR.ADDONS_IMPORTANT_AS_ADDONS_ARE_PROGRAMS_DOWNLOADED) % dict(
"<b>Important</b>: As add-ons are programs downloaded from the internet, " names=names
"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"
) % dict(names=names)
if ( if (
not showInfo( not showInfo(
q, q,

View File

@ -59,3 +59,7 @@ addons-unable-to-update-or-delete-addon = Unable to update or delete add-on. Ple
addons-unknown-error = Unknown error: { $val } addons-unknown-error = Unknown error: { $val }
addons-view-addon-page = View Add-on Page addons-view-addon-page = View Add-on Page
addons-view-files = View Files addons-view-files = View Files
addons-delete-the-numd-selected-addon = { $count ->
[one] Delete the { $count } selected add-on?
*[other] Delete the { $count } selected add-ons?
}

View File

@ -61,3 +61,7 @@ qt-misc-would-you-like-to-download-it = Would you like to download it now?
qt-misc-your-collection-file-appears-to-be = Your collection file appears to be corrupt. This can happen when the file is copied or moved while Anki is open, or when the collection is stored on a network or cloud drive. If problems persist after restarting your computer, please open an automatic backup from the profile screen. qt-misc-your-collection-file-appears-to-be = Your collection file appears to be corrupt. This can happen when the file is copied or moved while Anki is open, or when the collection is stored on a network or cloud drive. If problems persist after restarting your computer, please open an automatic backup from the profile screen.
qt-misc-your-computers-storage-may-be-full = Your computer's storage may be full. Please delete some unneeded files, then try again. qt-misc-your-computers-storage-may-be-full = Your computer's storage may be full. Please delete some unneeded files, then try again.
qt-misc-your-firewall-or-antivirus-program-is = Your firewall or antivirus program is preventing Anki from creating a connection to itself. Please add an exception for Anki. qt-misc-your-firewall-or-antivirus-program-is = Your firewall or antivirus program is preventing Anki from creating a connection to itself. Please add an exception for Anki.
qt-misc-second = { $count ->
[one] { $count } second
*[other] { $count } seconds
}

View File

@ -170,15 +170,22 @@ text_remap = {
"studying": ["Space"], "studying": ["Space"],
"qt-misc": ["&Edit", "&Guide...", "&Help", "&Undo", "Unexpected response code: %s"], "qt-misc": ["&Edit", "&Guide...", "&Help", "&Undo", "Unexpected response code: %s"],
"adding": ["Added"], "adding": ["Added"],
"browsing": ["%d note"],
} }
blacklist = {"Anki", "%", "Dialog", "Center", "Left", "Right", "~", "&Cram..."} blacklist = {"Anki", "%", "Dialog", "Center", "Left", "Right", "~", "&Cram..."}
def determine_module(text, files): def determine_module(text, files):
if not files:
return None
if text in blacklist: if text in blacklist:
return None return None
if text.count("%") > 1:
print("skip", text)
return None
if "&" in text: if "&" in text:
return "qt-accel" return "qt-accel"
@ -189,6 +196,7 @@ def determine_module(text, files):
if len(files) == 1: if len(files) == 1:
return list(files)[0] return list(files)[0]
print(text, files)
assert False assert False
@ -225,8 +233,7 @@ seen_keys = set()
def migrate_entry(entry): def migrate_entry(entry):
if entry.msgid_plural: if not entry.msgid_plural:
# print("skip plural", entry.msgid)
return return
text = entry.msgid text = entry.msgid
@ -244,7 +251,7 @@ def migrate_entry(entry):
files2 = set() files2 = set()
for file in files: for file in files:
if file == "stats": if file in ("stats", "supermemo_xml"):
continue continue
file = module_map[file] file = module_map[file]
files2.add(file) files2.add(file)
@ -260,7 +267,7 @@ def migrate_entry(entry):
seen_keys.add(key) seen_keys.add(key)
modules.setdefault(module, []) modules.setdefault(module, [])
modules[module].append((key, text)) modules[module].append((key, [entry.msgid, entry.msgid_plural]))
return None return None
@ -309,8 +316,8 @@ for (module, items) in modules.items():
strings_by_module[module] = items strings_by_module[module] = items
for item in items: for item in items:
(key, text) = item (key, text) = item
assert text not in keys_by_text assert text[0] not in keys_by_text
keys_by_text[text] = (module, key) keys_by_text[text[0]] = (module, key)
with open("strings_by_module.json", "w") as file: with open("strings_by_module.json", "w") as file:
file.write(json.dumps(strings_by_module)) file.write(json.dumps(strings_by_module))

View File

@ -2,6 +2,9 @@
import json import json
import os import os
import re
from typing import List
from fluent.syntax import parse, serialize from fluent.syntax import parse, serialize
from fluent.syntax.ast import Message, TextElement, Identifier, Pattern, Junk from fluent.syntax.ast import Message, TextElement, Identifier, Pattern, Junk
@ -13,6 +16,8 @@ qt_modules = {"about", "qt-accel", "addons", "qt-misc"}
modules = json.load(open("strings_by_module.json")) modules = json.load(open("strings_by_module.json"))
translations = json.load(open("strings.json")) translations = json.load(open("strings.json"))
plurals = json.load(open("plurals.json"))
# # fixme: # # fixme:
# addons addons-downloaded-fnames Downloaded %(fname)s # addons addons-downloaded-fnames Downloaded %(fname)s
@ -33,17 +38,43 @@ translations = json.load(open("strings.json"))
# fixme: isolation chars # fixme: isolation chars
def transform_text(text: str) -> str: def plural_text(key, lang, translation):
# fixme: automatically remap to %s in a compat wrapper? manually fix? lang = re.sub("(_|-).*", "", lang)
text = (
text.replace("%d", "{ $val }") var = re.findall(r"{ (\$.*?) }", translation[0])
.replace("%s", "{ $val }") try:
.replace("{}", "{ $val }") var = var[0]
) except:
if "Clock drift" in text: print(key, lang, translation)
text = text.replace("\n", "<br>") raise
else:
text = text.replace("\n", " ") 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 transform_text(key: str, texts: List[str], lang: str) -> str:
texts = [
(
text.replace("%d", "{ $count }")
.replace("%s", "{ $count }")
.replace("%(num)d", "{ $count }")
)
for text in texts
]
text = plural_text(key, lang, texts)
print(text)
return text return text
@ -57,6 +88,14 @@ def check_parses(path: str):
raise Exception(f"file had junk! {path} {ent}") raise Exception(f"file had junk! {path} {ent}")
def munge_key(key):
if key == "browsing-note":
return "browsing-note-count"
if key == "card-templates-card":
return "card-templates-card-count"
return key
for module, items in modules.items(): for module, items in modules.items():
if module in qt_modules: if module in qt_modules:
folder = qt folder = qt
@ -68,8 +107,9 @@ for module, items in modules.items():
print(path) print(path)
with open(path, "a", encoding="utf8") as file: with open(path, "a", encoding="utf8") as file:
for (key, text) in items: for (key, text) in items:
text2 = transform_text(text) key = munge_key(key)
file.write(f"{key} = {text2}\n") text2 = transform_text(key, text, "en")
file.write(text2)
check_parses(path) check_parses(path)
@ -80,8 +120,12 @@ for module, items in modules.items():
out = [] out = []
for (key, text) in items: for (key, text) in items:
if text in map: if text[0] in map:
out.append((key, transform_text(map[text]))) forms = map[text[0]]
if isinstance(forms, str):
forms = [forms]
key = munge_key(key)
out.append(transform_text(key, forms, lang))
if out: if out:
path = os.path.join(folder, lang, module + ".ftl") path = os.path.join(folder, lang, module + ".ftl")
@ -91,7 +135,7 @@ for module, items in modules.items():
print(path) print(path)
with open(path, "a", encoding="utf8") as file: with open(path, "a", encoding="utf8") as file:
for (key, text) in out: for o in out:
file.write(f"{key} = {text}\n") file.write(o)
check_parses(path) check_parses(path)

View File

@ -102,3 +102,19 @@ browsing-treat-input-as-regular-expression = Treat input as regular expression
browsing-type-here-to-search = <type here to search; hit enter to show current deck> browsing-type-here-to-search = <type here to search; hit enter to show current deck>
browsing-whole-collection = Whole Collection browsing-whole-collection = Whole Collection
browsing-you-must-have-at-least-one = You must have at least one column. browsing-you-must-have-at-least-one = You must have at least one column.
browsing-group = { $count ->
[one] { $count } group
*[other] { $count } groups
}
browsing-note-count = { $count ->
[one] { $count } note
*[other] { $count } notes
}
browsing-note-deleted = { $count ->
[one] { $count } note deleted.
*[other] { $count } notes deleted.
}
browsing-selected = { $count ->
[one] { $count } selected
*[other] { $count } selected
}

View File

@ -40,3 +40,11 @@ card-templates-on = (on)
card-templates-remove-card-type = Remove Card Type... card-templates-remove-card-type = Remove Card Type...
card-templates-rename-card-type = Rename Card Type... card-templates-rename-card-type = Rename Card Type...
card-templates-reposition-card-type = Reposition Card Type... card-templates-reposition-card-type = Reposition Card Type...
card-templates-card-count = { $count ->
[one] { $count } card
*[other] { $count } cards
}
card-templates-this-will-create-card-proceed = { $count ->
[one] This will create { $count } card. Proceed?
*[other] This will create { $count } cards. Proceed?
}

View File

@ -31,3 +31,7 @@ decks-reschedule-cards-based-on-my-answers = Reschedule cards based on my answer
decks-study = Study decks-study = Study
decks-study-deck = Study Deck decks-study-deck = Study Deck
decks-the-provided-search-did-not-match = The provided search did not match any cards. Would you like to revise it? decks-the-provided-search-did-not-match = The provided search did not match any cards. Would you like to revise it?
decks-it-has-card = { $count ->
[one] It has { $count } card.
*[other] It has { $count } cards.
}

View File

@ -15,3 +15,15 @@ exporting-include-scheduling-information = Include scheduling information
exporting-include-tags = Include tags exporting-include-tags = Include tags
exporting-notes-in-plain-text = Notes in Plain Text exporting-notes-in-plain-text = Notes in Plain Text
exporting-selected-notes = Selected Notes exporting-selected-notes = Selected Notes
exporting-card-exported = { $count ->
[one] { $count } card exported.
*[other] { $count } cards exported.
}
exporting-exported-media-file = { $count ->
[one] Exported { $count } media file
*[other] Exported { $count } media files
}
exporting-note-exported = { $count ->
[one] { $count } note exported.
*[other] { $count } notes exported.
}

View File

@ -52,3 +52,23 @@ importing-unable-to-import-from-a-readonly = Unable to import from a read-only f
importing-unknown-file-format = Unknown file format. importing-unknown-file-format = Unknown file format.
importing-update-existing-notes-when-first-field = Update existing notes when first field matches importing-update-existing-notes-when-first-field = Update existing notes when first field matches
importing-updated = Updated importing-updated = Updated
importing-note-added = { $count ->
[one] { $count } note added
*[other] { $count } notes added
}
importing-note-imported = { $count ->
[one] { $count } note imported.
*[other] { $count } notes imported.
}
importing-note-unchanged = { $count ->
[one] { $count } note unchanged
*[other] { $count } notes unchanged
}
importing-note-updated = { $count ->
[one] { $count } note updated
*[other] { $count } notes updated
}
importing-processed-media-file = { $count ->
[one] Processed { $count } media file
*[other] Processed { $count } media files
}

View File

@ -138,3 +138,7 @@ scheduling-steps-must-be-numbers = Steps must be numbers.
scheduling-tag-only = Tag Only scheduling-tag-only = Tag Only
scheduling-the-default-configuration-cant-be-removed = The default configuration can't be removed. scheduling-the-default-configuration-cant-be-removed = The default configuration can't be removed.
scheduling-your-changes-will-affect-multiple-decks = Your changes will affect multiple decks. If you wish to change only the current deck, please add a new options group first. scheduling-your-changes-will-affect-multiple-decks = Your changes will affect multiple decks. If you wish to change only the current deck, please add a new options group first.
scheduling-deck-updated = { $count ->
[one] { $count } deck updated.
*[other] { $count } decks updated.
}

View File

@ -42,3 +42,15 @@ studying-type-answer-unknown-field = Type answer: unknown field { $val }
studying-unbury = Unbury studying-unbury = Unbury
studying-what-would-you-like-to-unbury = What would you like to unbury? studying-what-would-you-like-to-unbury = What would you like to unbury?
studying-you-havent-recorded-your-voice-yet = You haven't recorded your voice yet. studying-you-havent-recorded-your-voice-yet = You haven't recorded your voice yet.
studying-card-studied-in = { $count ->
[one] { $count } card studied in
*[other] { $count } cards studied in
}
studying-minute = { $count ->
[one] { $count } minute.
*[other] { $count } minutes.
}
studying-note-and-its-card-deleted = { $count ->
[one] Note and its { $count } card deleted.
*[other] Note and its { $count } cards deleted.
}