Skip to content

Instantly share code, notes, and snippets.

@gwennlbh
Last active October 4, 2025 18:02
Show Gist options
  • Save gwennlbh/c5c64b3fe9c6aa99916f545cd1a6baa2 to your computer and use it in GitHub Desktop.
Save gwennlbh/c5c64b3fe9c6aa99916f545cd1a6baa2 to your computer and use it in GitHub Desktop.
Paraglide to Wuchale migration scripts
from pathlib import Path
import json
import re
src = Path(__file__).parent.parent
english = json.loads(Path(src.parent / "messages/en.json").read_text(encoding="utf8"))
french = json.loads(Path(src.parent / "messages/fr.json").read_text(encoding="utf8"))
del english["$schema"]
del french["$schema"]
def named_to_positional(text_with_placeholders: str) -> str:
pattern = re.compile(r"\{(?P<name>[a-z0-9_]+)\}")
names = []
def replacer(match: re.Match) -> str:
name = match.group("name")
if name not in names:
names.append(name)
return "{" + str(names.index(name)) + "}"
result = pattern.sub(replacer, text_with_placeholders)
if result != text_with_placeholders:
print(f"Converted '{text_with_placeholders}' to '{result}'")
return result
french_to_english = {
named_to_positional(french[key]): named_to_positional(english[key])
for key in french.keys()
if key in english
}
pofile = Path(src / "locales/en.po")
def red(text: str) -> str:
return f"\033[91m{text}\033[0m"
def try_surrounding_with_tags(in_french: str) -> str | None:
prefixed = french_to_english.get(in_french.removeprefix("<0/> "))
if prefixed:
return f"<0/> {prefixed}"
suffixed = french_to_english.get(in_french.removesuffix(" <0/>"))
if suffixed:
return f"{suffixed} <0/>"
inside = french_to_english.get(in_french.removeprefix("<0>").removesuffix("</0>"))
if inside:
return f"<0>{inside}</0>"
wrapped = french_to_english.get(
in_french.removeprefix("<0/> ").removesuffix(" <1/>")
)
if wrapped:
return f"<0/> {wrapped} <1/>"
return None
lines = list(pofile.read_text(encoding="utf8").splitlines())
for i, line in enumerate(lines):
match = re.match(r'msgid "(?P<key>.+)"', line)
if not match:
continue
key = match.group("key")
message = french_to_english.get(key) or try_surrounding_with_tags(key)
if not message:
print(red(f"Not found: {key}"))
continue
if lines[i + 1] != 'msgstr ""':
continue
print(f"Found {key} in {pofile.relative_to(src.parent)}:{i+1} -> {line}")
pofile.write_text(
pofile.read_text(encoding="utf8").replace(
f'msgid "{key}"\nmsgstr ""',
f'msgid "{key}"\nmsgstr "{message.replace('"', r'\"')}"',
),
encoding="utf8",
)
import json
from pathlib import Path
import re
here = Path(__file__).parent
src = here.parent
messages: dict[str, str] = json.loads(
Path(src.parent / "messages/fr.json").read_text(encoding="utf8")
)
paraglide_call_pattern = re.compile(r"\bm\.(?P<key>[a-z0-9_]+)\(\)")
def process_directory(directory: Path):
for file in directory.glob("*"):
if file.is_dir():
process_directory(file)
continue
if not file.suffix in {".js", ".svelte"}:
continue
text = file.read_text(encoding="utf8")
if "m." not in text:
continue
lines = list(text.splitlines())
print(f"\nProcessing {file}")
for i, line in enumerate(lines):
for call in paraglide_call_pattern.finditer(line):
print(
f'Found {call.group("key")} in {file.relative_to(src.parent)}:{i+1} -> {line.replace(call.group(0), bold(call.group(0)))}'
)
message = messages.get(call.group("key"))
if not message:
raise ValueError(f"Key {call.group('key')} not found in messages")
lines[i] = line.replace(
call.group(0), f"'{message.replace("'", r"\'")}'"
)
file.write_text("\n".join(lines), encoding="utf8")
# ANSI bold for terminal
def bold(text: str) -> str:
return f"\033[1m{text}\033[0m"
if __name__ == "__main__":
process_directory(src)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment