1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-07 05:58:28 -04:00

i18n: term overhaul

- Delete ~160-ish useless terms
- Add context to more terms
- Add a mechanism to duplicate the same terms with different contexts
- sync
This commit is contained in:
bbedward
2026-07-16 12:15:20 -04:00
parent 6c45413d7a
commit 3c0f2cbc48
83 changed files with 19570 additions and 20947 deletions
@@ -4,6 +4,9 @@
While term_freeze.json exists, any I18n.tr()/qsTr() term not in it fails the check.
Existing terms may be reused or moved freely.
The freeze is term-granular: adding a new real context to a frozen term via
I18n.tr(term, ctx, true) does not trip it (same English string, new slot).
--update re-snapshot the current terms into term_freeze.json
(delete term_freeze.json to lift the freeze)
"""
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Block near-variant translation terms (case/trailing-punctuation duplicates).
Unlike a term freeze, new terms are always allowed. This only fails when two
terms collapse to the same string after lowercasing and stripping trailing
".:?! " punctuation - e.g. "Signal" vs "Signal:", "Reset to Default?" vs
"Reset to default". Build punctuation outside tr() instead:
I18n.tr("Signal") + ":"
"Term" vs "Term..." pairs are NOT flagged: a trailing ellipsis is meaningful
(progress states, opens-a-dialog affordances).
"""
import sys
from collections import defaultdict
from extract_translations import extract_qstr_strings
from pathlib import Path
ROOT_DIR = Path(__file__).parent.parent
# Intentional pairs that survive normalization on purpose.
ALLOWED = [
{"PIN", "Pin"}, # WPS PIN acronym vs the verb "pin"
{"Device", "device"}, # label vs inline generic-noun fallback
{"Until %1", "until %1"}, # sentence-initial vs mid-sentence position
]
def normalize(term):
t = term.replace("", "...")
ellipsis = t.rstrip().endswith("...")
t = t.lower().rstrip(".:?! ")
return t + "..." if ellipsis else t
def main():
translations = extract_qstr_strings(ROOT_DIR)
groups = defaultdict(set)
for term in translations:
groups[normalize(term)].add(term)
failures = []
for variants in groups.values():
if len(variants) < 2 or any(variants == a for a in ALLOWED):
continue
# term vs term+"..." is allowed; flag only same-ellipsis-class variants
plain = {v for v in variants if not v.replace("", "...").rstrip().endswith("...")}
dotted = variants - plain
for cls in (plain, dotted):
if len(cls) > 1:
failures.append(sorted(cls))
if not failures:
print(f"No term variants ({len(translations)} terms checked)")
return 0
print("Near-variant terms found - reuse one exact term (or add to ALLOWED "
"in check_term_variants.py if genuinely intentional):", file=sys.stderr)
for variants in sorted(failures):
print(f" {variants}", file=sys.stderr)
for v in variants:
for occ in translations[v]["occurrences"][:3]:
print(f" {v!r}: {occ['file']}:{occ['line']}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
+13188 -4427
View File
File diff suppressed because it is too large Load Diff
@@ -435,7 +435,7 @@ def parse_tabs_from_sidebar(sidebar_file):
with open(sidebar_file, "r", encoding="utf-8") as f:
content = f.read()
pattern = r'"text"\s*:\s*I18n\.tr\("([^"]+)"(?:,\s*"[^"]+")?\).*?"icon"\s*:\s*"([^"]+)".*?"tabIndex"\s*:\s*(\d+)'
pattern = r'"text"\s*:\s*I18n\.tr\("([^"]+)"(?:,\s*"[^"]+"(?:,\s*true)?)?\).*?"icon"\s*:\s*"([^"]+)".*?"tabIndex"\s*:\s*(\d+)'
tabs = []
for match in re.finditer(pattern, content, re.DOTALL):
+82 -41
View File
@@ -18,11 +18,29 @@ def spans_overlap(a, b):
def extract_qstr_strings(root_dir):
translations = defaultdict(lambda: {'contexts': set(), 'occurrences': []})
translations = defaultdict(lambda: {
'contexts': set(),
'real_contexts': defaultdict(list),
'occurrences': [],
'plain_occurrences': []
})
qstr_patterns = [
(re.compile(r'qsTr\(\s*"((?:\\.|[^"\\])*)"\s*\)'), '"'),
(re.compile(r"qsTr\(\s*'((?:\\.|[^'\\])*)'\s*\)"), "'")
]
# I18n.tr(term, context, true) -- the literal `true` flag uploads the
# context as a real POEditor context, giving (term, context) its own
# translation slot. Must be on one line with a literal `true`.
i18n_real_context_patterns = [
(
re.compile(r'I18n\.tr\(\s*"((?:\\.|[^"\\])*)"\s*,\s*"((?:\\.|[^"\\])*)"\s*,\s*true\s*\)'),
'"'
),
(
re.compile(r"I18n\.tr\(\s*'((?:\\.|[^'\\])*)'\s*,\s*'((?:\\.|[^'\\])*)'\s*,\s*true\s*\)"),
"'"
)
]
i18n_context_patterns = [
(
re.compile(r'I18n\.tr\(\s*"((?:\\.|[^"\\])*)"\s*,\s*"((?:\\.|[^"\\])*)"\s*\)'),
@@ -46,75 +64,96 @@ def extract_qstr_strings(root_dir):
for pattern, quote in qstr_patterns:
for match in pattern.finditer(line):
term = decode_string_literal(match.group(1), quote)
translations[term]['occurrences'].append({
'file': str(relative_path),
'line': line_num
})
occ = {'file': str(relative_path), 'line': line_num}
translations[term]['occurrences'].append(occ)
translations[term]['plain_occurrences'].append(occ)
real_spans = []
for pattern, quote in i18n_real_context_patterns:
for match in pattern.finditer(line):
term = decode_string_literal(match.group(1), quote)
context = decode_string_literal(match.group(2), quote)
occ = {'file': str(relative_path), 'line': line_num}
translations[term]['real_contexts'][context].append(occ)
translations[term]['occurrences'].append(occ)
real_spans.append(match.span())
context_spans = []
for pattern, quote in i18n_context_patterns:
for match in pattern.finditer(line):
if any(spans_overlap(match.span(), span) for span in real_spans):
continue
term = decode_string_literal(match.group(1), quote)
context = decode_string_literal(match.group(2), quote)
occ = {'file': str(relative_path), 'line': line_num}
translations[term]['contexts'].add(context)
translations[term]['occurrences'].append({
'file': str(relative_path),
'line': line_num
})
translations[term]['occurrences'].append(occ)
translations[term]['plain_occurrences'].append(occ)
context_spans.append(match.span())
for pattern, quote in i18n_simple_patterns:
for match in pattern.finditer(line):
if any(spans_overlap(match.span(), span) for span in context_spans):
if any(spans_overlap(match.span(), span) for span in real_spans + context_spans):
continue
term = decode_string_literal(match.group(1), quote)
translations[term]['occurrences'].append({
'file': str(relative_path),
'line': line_num
})
occ = {'file': str(relative_path), 'line': line_num}
translations[term]['occurrences'].append(occ)
translations[term]['plain_occurrences'].append(occ)
return translations
def area_tags(occurrences):
tags = set()
for occ in occurrences:
path = occ['file']
if path.startswith('dms-plugins/'):
tags.add('plugin-' + path.split('/')[1].lower())
elif path.startswith(('Modules/Settings/', 'Modals/Settings/')):
tags.add('settings')
else:
tags.add('shell')
return sorted(tags)
def create_poeditor_json(translations):
poeditor_data = []
for term, data in sorted(translations.items()):
references = []
if data['plain_occurrences']:
references = [f"{occ['file']}:{occ['line']}" for occ in data['plain_occurrences']]
contexts = sorted(data['contexts']) if data['contexts'] else []
comment = " | ".join(contexts) if contexts else ""
for occ in data['occurrences']:
ref = f"{occ['file']}:{occ['line']}"
references.append(ref)
poeditor_data.append({
"term": term,
"context": term,
"reference": ", ".join(references),
"comment": comment,
"tags": area_tags(data['plain_occurrences'])
})
contexts = sorted(data['contexts']) if data['contexts'] else []
comment = " | ".join(contexts) if contexts else ""
entry = {
"term": term,
"context": term,
"reference": ", ".join(references),
"comment": comment
}
poeditor_data.append(entry)
for context in sorted(data['real_contexts']):
references = [f"{occ['file']}:{occ['line']}" for occ in data['real_contexts'][context]]
poeditor_data.append({
"term": term,
"context": context,
"reference": ", ".join(references),
"comment": "",
"tags": area_tags(data['real_contexts'][context])
})
return poeditor_data
def create_template_json(translations):
template_data = []
for term, data in sorted(translations.items()):
contexts = sorted(data['contexts']) if data['contexts'] else []
context_str = " | ".join(contexts) if contexts else ""
entry = {
"term": term,
return [
{
"term": entry["term"],
"translation": "",
"context": context_str,
"context": entry["context"],
"reference": "",
"comment": ""
"comment": entry["comment"]
}
template_data.append(entry)
return template_data
for entry in create_poeditor_json(translations)
]
def main():
script_dir = Path(__file__).parent
@@ -142,6 +181,8 @@ def main():
print(f" - Unique strings: {len(translations)}")
print(f" - Total occurrences: {sum(len(data['occurrences']) for data in translations.values())}")
print(f" - Strings with contexts: {sum(1 for data in translations.values() if data['contexts'])}")
print(f" - Real-context entries: {sum(len(data['real_contexts']) for data in translations.values())}")
print(f" - POEditor entries: {len(poeditor_data)}")
print(f" - Source file: {en_json_path}")
print(f" - Template file: {template_json_path}")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff