Fixes: 1) None; Extra: 1) Removed the entire "geo" module, including migrations, model definitions, admin configurations, utilities, documentation, templates, translations, and related files. Moved functionality to "core".
150 lines
5.2 KiB
Python
150 lines
5.2 KiB
Python
import contextlib
|
|
import os
|
|
import re
|
|
from tempfile import NamedTemporaryFile
|
|
|
|
import polib
|
|
from django.apps import apps
|
|
from django.core.management.base import BaseCommand, CommandError
|
|
|
|
# Patterns to identify placeholders
|
|
PLACEHOLDER_REGEXES = [
|
|
re.compile(r"\{[^}]+\}"), # {name}, {type(instance)!s}, etc.
|
|
re.compile(r"%\([^)]+\)[sd]"), # %(verbose_name)s, %(count)d
|
|
]
|
|
|
|
def extract_placeholders(text: str) -> set[str]:
|
|
"""
|
|
Extract all placeholders from given text.
|
|
"""
|
|
phs: list[str] = []
|
|
for rx in PLACEHOLDER_REGEXES:
|
|
phs.extend(rx.findall(text))
|
|
return set(phs)
|
|
|
|
|
|
def load_po_sanitized(path: str) -> polib.POFile:
|
|
"""
|
|
Load a .po file via polib, sanitizing on parse errors.
|
|
Raises CommandError if still unparsable.
|
|
"""
|
|
try:
|
|
return polib.pofile(path)
|
|
except Exception:
|
|
# read raw text
|
|
try:
|
|
with open(path, encoding='utf-8') as f:
|
|
text = f.read()
|
|
except OSError as e:
|
|
raise CommandError(f"{path}: cannot read file ({e})")
|
|
# fix fuzzy flags and empty header entries
|
|
text = re.sub(r"^#,(?!\s)", "#, ", text, flags=re.MULTILINE)
|
|
parts = text.split("\n\n", 1)
|
|
header = parts[0]
|
|
rest = parts[1] if len(parts) > 1 else ''
|
|
rest = re.sub(r"^msgid \"\"\s*\nmsgstr \"\"\s*\n?", '', rest, flags=re.MULTILINE)
|
|
sanitized = header + "\n\n" + rest
|
|
tmp = NamedTemporaryFile(mode='w+', delete=False, suffix='.po', encoding='utf-8') # noqa: SIM115
|
|
try:
|
|
tmp.write(sanitized)
|
|
tmp.flush()
|
|
tmp.close()
|
|
return polib.pofile(tmp.name)
|
|
except Exception as e:
|
|
raise CommandError(f"{path}: syntax error after sanitization ({e})")
|
|
finally:
|
|
with contextlib.suppress(OSError):
|
|
os.unlink(tmp.name)
|
|
|
|
class Command(BaseCommand):
|
|
help = (
|
|
"Scan target-language .po files and report any placeholder mismatches, grouped by app."
|
|
)
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
'-l', '--language',
|
|
dest='target_languages',
|
|
action='append',
|
|
required=True,
|
|
metavar='LANG',
|
|
help='Locale code(s) to scan, e.g. de-DE, fr-FR'
|
|
)
|
|
parser.add_argument(
|
|
'-a', '--app',
|
|
dest='target_apps',
|
|
action='append',
|
|
required=True,
|
|
metavar='APP',
|
|
help='App label(s) to scan, e.g. core, payments'
|
|
)
|
|
parser.add_argument(
|
|
'-p', '--path',
|
|
dest='root_path',
|
|
required=False,
|
|
metavar='ROOT_PATH',
|
|
help='Root path prefix to adjust file links'
|
|
)
|
|
|
|
def handle(self, *args, **options) -> None:
|
|
langs: list[str] = options['target_languages']
|
|
apps_to_scan: set[str] = set(options['target_apps'])
|
|
root_path: str = options.get('root_path') or '/app/'
|
|
|
|
for app_conf in apps.get_app_configs():
|
|
if app_conf.label not in apps_to_scan:
|
|
continue
|
|
|
|
# Collect issues per app across all languages
|
|
app_issues: list[str] = []
|
|
|
|
for lang in langs:
|
|
loc = lang.replace('-', '_')
|
|
po_path = os.path.join(
|
|
app_conf.path, 'locale', loc, 'LC_MESSAGES', 'django.po'
|
|
)
|
|
if not os.path.exists(po_path):
|
|
continue
|
|
|
|
try:
|
|
po = load_po_sanitized(po_path)
|
|
except CommandError:
|
|
continue
|
|
|
|
# Collect lines for this language
|
|
lang_issues: list[str] = []
|
|
for entry in po:
|
|
if not entry.msgid or not entry.msgstr or entry.obsolete:
|
|
continue
|
|
src_ph = extract_placeholders(entry.msgid)
|
|
dst_ph = extract_placeholders(entry.msgstr)
|
|
missing = src_ph - dst_ph
|
|
extra = dst_ph - src_ph
|
|
if missing or extra:
|
|
line_no = entry.linenum or '?'
|
|
display = po_path.replace('/app/', root_path)
|
|
if '\\' in root_path:
|
|
display = display.replace('/', '\\')
|
|
lang_issues.append(
|
|
f" {display}:{line_no}: missing={sorted(missing)} extra={sorted(extra)}"
|
|
)
|
|
|
|
if lang_issues:
|
|
# Header for language with issues
|
|
app_issues.append(f" ► {lang}")
|
|
app_issues.extend(lang_issues)
|
|
|
|
# Output per app
|
|
if app_issues:
|
|
self.stdout.write(f"→ App: {app_conf.label}")
|
|
for line in app_issues:
|
|
self.stdout.write(line)
|
|
self.stdout.write("")
|
|
else:
|
|
# No issues in any language for this app
|
|
self.stdout.write(
|
|
self.style.SUCCESS(f"App {app_conf.label} has no placeholder issues.")
|
|
)
|
|
self.stdout.write("")
|
|
|
|
self.stdout.write(self.style.SUCCESS("Done scanning."))
|