Skip to content

Commit dd4789a

Browse files
Fix translation references to unverified translations (#155314)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
1 parent ca49b6e commit dd4789a

File tree

4 files changed

+82
-47
lines changed

4 files changed

+82
-47
lines changed

script/translations/develop.py

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import argparse
44
import json
55
from pathlib import Path
6-
import re
76
import sys
87

98
from . import download, upload
109
from .const import INTEGRATIONS_DIR
11-
from .util import flatten_translations, get_base_arg_parser
10+
from .util import flatten_translations, get_base_arg_parser, substitute_references
1211

1312

1413
def valid_integration(integration):
@@ -31,42 +30,6 @@ def get_arguments() -> argparse.Namespace:
3130
return parser.parse_args()
3231

3332

34-
def substitute_translation_references(integration_strings, flattened_translations):
35-
"""Recursively processes all translation strings for the integration."""
36-
result = {}
37-
for key, value in integration_strings.items():
38-
if isinstance(value, dict):
39-
sub_dict = substitute_translation_references(value, flattened_translations)
40-
result[key] = sub_dict
41-
elif isinstance(value, str):
42-
result[key] = substitute_reference(value, flattened_translations)
43-
44-
return result
45-
46-
47-
def substitute_reference(value, flattened_translations):
48-
"""Substitute localization key references in a translation string."""
49-
matches = re.findall(r"\[\%key:([a-z0-9_]+(?:::(?:[a-z0-9-_])+)+)\%\]", value)
50-
if not matches:
51-
return value
52-
53-
new = value
54-
for key in matches:
55-
if key in flattened_translations:
56-
new = new.replace(
57-
f"[%key:{key}%]",
58-
# New value can also be a substitution reference
59-
substitute_reference(
60-
flattened_translations[key], flattened_translations
61-
),
62-
)
63-
else:
64-
print(f"Invalid substitution key '{key}' found in string '{value}'")
65-
sys.exit(1)
66-
67-
return new
68-
69-
7033
def run_single(translations, flattened_translations, integration):
7134
"""Run the script for a single integration."""
7235
print(f"Generating translations for {integration}")
@@ -77,8 +40,8 @@ def run_single(translations, flattened_translations, integration):
7740

7841
integration_strings = translations["component"][integration]
7942

80-
translations["component"][integration] = substitute_translation_references(
81-
integration_strings, flattened_translations
43+
translations["component"][integration] = substitute_references(
44+
integration_strings, flattened_translations, fail_on_missing=True
8245
)
8346

8447
if download.DOWNLOAD_DIR.is_dir():

script/translations/download.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010

1111
from .const import CLI_2_DOCKER_IMAGE, CORE_PROJECT_ID, INTEGRATIONS_DIR
1212
from .error import ExitApp
13-
from .util import get_lokalise_token, load_json_from_path
13+
from .util import (
14+
flatten_translations,
15+
get_lokalise_token,
16+
load_json_from_path,
17+
substitute_references,
18+
)
1419

1520
DOWNLOAD_DIR = Path("build/translations-download").absolute()
1621

1722

18-
def run_download_docker():
23+
def run_download_docker() -> None:
1924
"""Run the Docker image to download the translations."""
2025
print("Running Docker to download latest translations.")
2126
result = subprocess.run(
@@ -39,6 +44,7 @@ def run_download_docker():
3944
"--replace-breaks=false",
4045
"--filter-data",
4146
"nonfuzzy",
47+
"--disable-references",
4248
"--export-empty-as",
4349
"skip",
4450
"--format",
@@ -76,9 +82,12 @@ def filter_translations(translations: dict[str, Any], strings: dict[str, Any]) -
7682
continue
7783

7884

79-
def save_language_translations(lang, translations):
85+
def save_language_translations(lang: str, translations: dict[str, Any]) -> None:
8086
"""Save translations for a single language."""
8187
components = translations.get("component", {})
88+
89+
flattened_translations = flatten_translations(translations)
90+
8291
for component, component_translations in components.items():
8392
# Remove legacy platform translations
8493
component_translations.pop("platform", None)
@@ -104,26 +113,30 @@ def save_language_translations(lang, translations):
104113
path = component_path / "translations" / f"{lang}.json"
105114
path.parent.mkdir(parents=True, exist_ok=True)
106115

116+
component_translations = substitute_references(
117+
component_translations, flattened_translations, fail_on_missing=False
118+
)
119+
107120
filter_translations(component_translations, strings)
108121

109122
save_json(path, component_translations)
110123

111124

112-
def save_integrations_translations():
125+
def save_integrations_translations() -> None:
113126
"""Save integrations translations."""
114127
for lang_file in DOWNLOAD_DIR.glob("*.json"):
115128
lang = lang_file.stem
116129
translations = load_json_from_path(lang_file)
117130
save_language_translations(lang, translations)
118131

119132

120-
def delete_old_translations():
133+
def delete_old_translations() -> None:
121134
"""Delete old translations."""
122135
for fil in INTEGRATIONS_DIR.glob("*/translations/*"):
123136
fil.unlink()
124137

125138

126-
def run():
139+
def run() -> None:
127140
"""Run the script."""
128141
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
129142

script/translations/error.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ def __init__(self, reason, exit_code=1):
1212
self.exit_code = exit_code
1313

1414

15+
class MissingReference(Exception):
16+
"""Exception to indicate a missing reference in translations."""
17+
18+
def __init__(self, reference_key: str):
19+
"""Initialize the missing reference exception."""
20+
self.reference_key = reference_key
21+
super().__init__(f"Missing reference key: {reference_key}")
22+
23+
1524
class JSONDecodeErrorWithPath(json.JSONDecodeError):
1625
"""Subclass of JSONDecodeError with additional properties.
1726

script/translations/util.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
import json
55
import os
66
import pathlib
7+
import re
78
import subprocess
89
from typing import Any
910

10-
from .error import ExitApp, JSONDecodeErrorWithPath
11+
from .error import ExitApp, JSONDecodeErrorWithPath, MissingReference
1112

1213

1314
def get_base_arg_parser() -> argparse.ArgumentParser:
@@ -89,3 +90,52 @@ def flatten_translations(translations):
8990
key_stack.pop()
9091

9192
return flattened_translations
93+
94+
95+
def substitute_reference(value: str, flattened_translations: dict[str, str]) -> str:
96+
"""Substitute localization key references in a translation string."""
97+
matches = re.findall(r"\[\%key:([a-z0-9_]+(?:::(?:[a-z0-9-_])+)+)\%\]", value)
98+
if not matches:
99+
return value
100+
101+
new = value
102+
for key in matches:
103+
if key in flattened_translations:
104+
# New value can also be a substitution reference
105+
substituted = substitute_reference(
106+
flattened_translations[key], flattened_translations
107+
)
108+
new = new.replace(f"[%key:{key}%]", substituted)
109+
else:
110+
raise MissingReference(key)
111+
112+
return new
113+
114+
115+
def substitute_references(
116+
translations: dict[str, Any],
117+
substitutions: dict[str, str],
118+
*,
119+
fail_on_missing: bool,
120+
) -> dict[str, Any]:
121+
"""Recursively substitute references for all translation strings."""
122+
result = {}
123+
for key, value in translations.items():
124+
if isinstance(value, dict):
125+
sub_dict = substitute_references(
126+
value, substitutions, fail_on_missing=fail_on_missing
127+
)
128+
if sub_dict:
129+
result[key] = sub_dict
130+
elif isinstance(value, str):
131+
try:
132+
substituted = substitute_reference(value, substitutions)
133+
except MissingReference as err:
134+
if fail_on_missing:
135+
raise ExitApp(
136+
f"Missing reference '{err.reference_key}' in translation for key '{key}'"
137+
) from err
138+
continue
139+
result[key] = substituted
140+
141+
return result

0 commit comments

Comments
 (0)