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:
@@ -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
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):
|
||||
|
||||
@@ -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
+3684
-4321
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user