You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
5.4 KiB
201 lines
5.4 KiB
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
from collections import Counter
|
|
|
|
|
|
def _unesc(s: str) -> str:
|
|
if "\\" in s:
|
|
return s.encode("utf-8").decode("unicode_escape")
|
|
return s
|
|
|
|
|
|
def _skip_ws_and_comments(text: str, i: int) -> int:
|
|
n = len(text)
|
|
while i < n:
|
|
ch = text[i]
|
|
if ch.isspace():
|
|
i += 1
|
|
continue
|
|
if text.startswith("//", i):
|
|
j = text.find("\n", i + 2)
|
|
i = n if j == -1 else j + 1
|
|
continue
|
|
if text.startswith("/*", i):
|
|
j = text.find("*/", i + 2)
|
|
i = n if j == -1 else j + 2
|
|
continue
|
|
break
|
|
return i
|
|
|
|
|
|
def _parse_string_literal(text: str, i: int) -> tuple[str, int]:
|
|
quote = text[i]
|
|
i += 1
|
|
n = len(text)
|
|
buf: list[str] = []
|
|
while i < n:
|
|
ch = text[i]
|
|
if ch == "\\" and i + 1 < n:
|
|
buf.append(ch)
|
|
buf.append(text[i + 1])
|
|
i += 2
|
|
continue
|
|
if ch == quote:
|
|
i += 1
|
|
break
|
|
buf.append(ch)
|
|
i += 1
|
|
raw = "".join(buf)
|
|
return _unesc(raw), i
|
|
|
|
|
|
def _parse_dart_map(file_path: str) -> dict[str, str]:
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
text = f.read()
|
|
start = text.find("{")
|
|
end = text.rfind("}")
|
|
if start == -1 or end == -1 or end <= start:
|
|
return {}
|
|
text = text[start + 1 : end]
|
|
out: dict[str, str] = {}
|
|
i = 0
|
|
n = len(text)
|
|
while i < n:
|
|
i = _skip_ws_and_comments(text, i)
|
|
if i >= n:
|
|
break
|
|
if text[i] not in ("'", '"'):
|
|
i += 1
|
|
continue
|
|
key, i = _parse_string_literal(text, i)
|
|
i = _skip_ws_and_comments(text, i)
|
|
if i >= n or text[i] != ":":
|
|
continue
|
|
i += 1
|
|
i = _skip_ws_and_comments(text, i)
|
|
if i >= n or text[i] not in ("'", '"'):
|
|
continue
|
|
parts: list[str] = []
|
|
while i < n and text[i] in ("'", '"'):
|
|
part, i = _parse_string_literal(text, i)
|
|
parts.append(part)
|
|
i = _skip_ws_and_comments(text, i)
|
|
out[key] = "".join(parts)
|
|
return out
|
|
|
|
|
|
def _ascii_letter_ratio(s: str) -> float:
|
|
if not s:
|
|
return 0.0
|
|
letters = sum(1 for ch in s if ("a" <= ch.lower() <= "z"))
|
|
return letters / max(1, len(s))
|
|
|
|
|
|
def _looks_english_sentence(s: str) -> bool:
|
|
if len(s.strip()) < 4:
|
|
return False
|
|
ratio = _ascii_letter_ratio(s)
|
|
if ratio < 0.35:
|
|
return False
|
|
s_low = s.lower()
|
|
common = [
|
|
"please",
|
|
"failed",
|
|
"success",
|
|
"error",
|
|
"connect",
|
|
"connection",
|
|
"device",
|
|
"network",
|
|
"download",
|
|
"upload",
|
|
"translate",
|
|
"translation",
|
|
"record",
|
|
"permission",
|
|
"enable",
|
|
"disable",
|
|
"check",
|
|
"try again",
|
|
]
|
|
return any(w in s_low for w in common)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--dir",
|
|
default=os.path.join("lib", "core", "translations", "language"),
|
|
help="Directory containing language dart files",
|
|
)
|
|
parser.add_argument(
|
|
"--baseline",
|
|
default="en_us.dart",
|
|
help="Baseline dart file to compare values against",
|
|
)
|
|
parser.add_argument(
|
|
"--allowlist",
|
|
default="",
|
|
help="Path to JSON array of keys allowed to keep identical baseline values",
|
|
)
|
|
parser.add_argument(
|
|
"--mode",
|
|
choices=["exact", "englishy"],
|
|
default="englishy",
|
|
help="exact: value equals baseline; englishy: value equals baseline and looks English",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
lang_dir = args.dir
|
|
dart_files = sorted(glob.glob(os.path.join(lang_dir, "*.dart")))
|
|
if not dart_files:
|
|
raise SystemExit(f"No dart files found in {lang_dir}")
|
|
|
|
allowlist: set[str] = set()
|
|
if args.allowlist:
|
|
with open(args.allowlist, "r", encoding="utf-8") as f:
|
|
allowlist = set(json.load(f))
|
|
|
|
parsed = {os.path.basename(p): _parse_dart_map(p) for p in dart_files}
|
|
if args.baseline not in parsed:
|
|
raise SystemExit(f"baseline not found: {args.baseline}")
|
|
|
|
baseline = parsed[args.baseline]
|
|
|
|
result: dict[str, dict] = {"baseline": args.baseline, "mode": args.mode, "files": {}}
|
|
global_counter: Counter[str] = Counter()
|
|
|
|
for fn, kv in parsed.items():
|
|
if fn == args.baseline:
|
|
continue
|
|
issues: list[dict] = []
|
|
for k, v in kv.items():
|
|
if k in allowlist:
|
|
continue
|
|
bv = baseline.get(k)
|
|
if bv is None:
|
|
continue
|
|
if v != bv:
|
|
continue
|
|
if args.mode == "englishy" and not _looks_english_sentence(v):
|
|
continue
|
|
issues.append({"key": k, "value": v})
|
|
global_counter[k] += 1
|
|
result["files"][fn] = {"count": len(issues), "items": issues}
|
|
|
|
result["summary"] = {
|
|
"file_count": len(dart_files),
|
|
"non_baseline_file_count": len(dart_files) - 1,
|
|
"files_with_issues": sum(1 for f in result["files"].values() if f["count"] > 0),
|
|
"total_issues": sum(f["count"] for f in result["files"].values()),
|
|
"most_common_keys": [{"key": k, "count": c} for k, c in global_counter.most_common(50)],
|
|
}
|
|
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|
|
|