Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release-notes-generator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
fetch-tags: true

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.12'

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ After the translation process is executed, the action produces an output called
- **Module Information:**
Each module found in your Android project is listed along with its language-specific resources.
- **Strings Translations:**
For each module and language, a table displays every key that was translated, including the original text and the resulting translation.
For each module, a single table displays every key that was translated, pairing the original text with side-by-side columns for each language.
- **Plural Resources Translations:**
If plural resources were translated, the report shows each plural resource name along with a table listing each plural quantity (e.g., `one`, `few`, `many`) and their corresponding translations.
If plural resources were translated, the report shows each plural name followed by a table listing every quantity with the source text and a column for each language.

This output is particularly useful for manual review and validation. In the provided advanced workflow, the report is automatically inserted into the body of a pull request, allowing maintainers to inspect the changes before merging.

Expand Down
168 changes: 128 additions & 40 deletions app/AndroidResourceTranslator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,10 +1122,12 @@ def _translate_missing_plurals(
)

# Add to results
source_values = chunk_dict.get(plural_name, {})
results.append(
{
"plural_name": plural_name,
"translations": res.plurals[plural_name],
"source": dict(source_values) if source_values else {},
"translations": dict(res.plurals[plural_name]),
}
)

Expand Down Expand Up @@ -1485,55 +1487,141 @@ def create_translation_report(translation_log):
"""
Generate a Markdown formatted translation report as a string.
"""
report = "# Translation Report\n\n"
report_lines: List[str] = ["# Translation Report", ""]
has_translations = False

def _format_cell(value: Any) -> str:
if value is None:
return ""
if not isinstance(value, str):
value = str(value)
return value.replace("\n", " ").replace("|", "\\|").strip()

for module, languages in translation_log.items():
module_has_translations = False
module_report = f"## Module: {module}\n\n"
languages_report = ""
module_strings: Dict[str, Dict[str, Any]] = {}
module_plurals: Dict[str, Dict[str, Any]] = {}
string_languages: Set[str] = set()
plural_languages: Set[str] = set()

for lang, details in languages.items():
has_string_translations = bool(details.get("strings"))
has_plural_translations = bool(details.get("plurals"))

if not (has_string_translations or has_plural_translations):
if not details:
continue

module_has_translations = True
has_translations = True
# Get the language name from the code (will return lang code if name not found)
lang_name = get_language_name(lang)
languages_report += f"### Language: {lang_name}\n\n"

if has_string_translations:
languages_report += "| Key | Source Text | Translated Text |\n"
languages_report += "| --- | ----------- | --------------- |\n"
for entry in details["strings"]:
key = entry["key"]
source = entry["source"].replace("\n", " ")
translation = entry["translation"].replace("\n", " ")
languages_report += f"| {key} | {source} | {translation} |\n"
languages_report += "\n"

if has_plural_translations:
languages_report += "#### Plural Resources\n\n"
for plural in details["plurals"]:
plural_name = plural["plural_name"]
languages_report += f"**{plural_name}**\n\n"
languages_report += "| Quantity | Translated Text |\n"
languages_report += "| -------- | --------------- |\n"
for qty, text in plural["translations"].items():
languages_report += f"| {qty} | {text} |\n"
languages_report += "\n"

if module_has_translations:
report += module_report + languages_report
string_entries = details.get("strings", [])
if string_entries:
string_languages.add(lang)
for entry in string_entries:
key = entry.get("key")
if not key:
continue
bucket = module_strings.setdefault(
key,
{
"source": entry.get("source", ""),
"translations": {},
},
)
if not bucket["source"] and entry.get("source"):
bucket["source"] = entry["source"]
bucket["translations"][lang] = entry.get("translation", "")

plural_entries = details.get("plurals", [])
if plural_entries:
plural_languages.add(lang)
for entry in plural_entries:
plural_name = entry.get("plural_name")
if not plural_name:
continue
bucket = module_plurals.setdefault(
plural_name,
{"source": {}, "translations": {}},
)
source_map = entry.get("source") or {}
if source_map:
if not bucket["source"]:
bucket["source"] = dict(source_map)
else:
for qty, text in source_map.items():
bucket["source"].setdefault(qty, text)
translations_map = entry.get("translations") or {}
lang_map = bucket["translations"].setdefault(lang, {})
lang_map.update(translations_map)

if not module_strings and not module_plurals:
continue

has_translations = True
report_lines.append(f"## Module: {module}")
report_lines.append("")

if module_strings:
report_lines.append("### Strings")
report_lines.append("")

string_lang_list = sorted(
string_languages, key=lambda code: get_language_name(code).lower()
)
headers = ["Key", "Source Text"] + [
get_language_name(code) for code in string_lang_list
]
separator = ["---"] * len(headers)
report_lines.append("| " + " | ".join(headers) + " |")
report_lines.append("| " + " | ".join(separator) + " |")

for key in sorted(module_strings.keys()):
entry = module_strings[key]
row_cells = [
_format_cell(key),
_format_cell(entry.get("source", "")),
]
for lang_code in string_lang_list:
row_cells.append(
_format_cell(entry["translations"].get(lang_code, ""))
)
report_lines.append("| " + " | ".join(row_cells) + " |")
report_lines.append("")

if module_plurals:
report_lines.append("### Plural Resources")
report_lines.append("")
plural_lang_list = sorted(
plural_languages, key=lambda code: get_language_name(code).lower()
)
for plural_name in sorted(module_plurals.keys()):
entry = module_plurals[plural_name]
report_lines.append(f"#### {plural_name}")
report_lines.append("")
headers = ["Quantity", "Source Text"] + [
get_language_name(code) for code in plural_lang_list
]
separator = ["---"] * len(headers)
report_lines.append("| " + " | ".join(headers) + " |")
report_lines.append("| " + " | ".join(separator) + " |")

quantities: Set[str] = set(entry.get("source", {}).keys())
for lang_code in plural_lang_list:
quantities.update(
entry["translations"].get(lang_code, {}).keys()
)

for quantity in sorted(quantities):
source_text = entry.get("source", {}).get(quantity, "")
row_cells = [
_format_cell(quantity),
_format_cell(source_text),
]
for lang_code in plural_lang_list:
lang_text = entry["translations"].get(lang_code, {}).get(
quantity, ""
)
row_cells.append(_format_cell(lang_text))
report_lines.append("| " + " | ".join(row_cells) + " |")
report_lines.append("")

if not has_translations:
report += "No translations were performed."
report_lines.append("No translations were performed.")

return report
return "\n".join(report_lines).rstrip() + "\n"


# ------------------------------------------------------------------------------
Expand Down
33 changes: 20 additions & 13 deletions app/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ def test_create_translation_report_with_data(self):
"source": "Hello World",
"translation": "Hola Mundo",
},
{"key": "goodbye", "source": "Goodbye", "translation": "Adiós"},
{
"key": "goodbye",
"source": "Goodbye",
"translation": "Adiós",
},
],
"plurals": [
{
"plural_name": "days",
"source": {"one": "%d day", "other": "%d days"},
"translations": {"one": "%d día", "other": "%d días"},
}
],
Expand All @@ -74,18 +79,20 @@ def test_create_translation_report_with_data(self):
self.assertIn("# Translation Report", report)
self.assertIn("## Module: test_module", report)

# Check Spanish section
self.assertIn("### Language: Spanish", report)
self.assertIn("| Key | Source Text | Translated Text |", report)
self.assertIn("| hello | Hello World | Hola Mundo |", report)
self.assertIn("| goodbye | Goodbye | Adiós |", report)
self.assertIn("**days**", report)
self.assertIn("| one | %d día |", report)
self.assertIn("| other | %d días |", report)

# Check French section
self.assertIn("### Language: French", report)
self.assertIn("| hello | Hello World | Bonjour le monde |", report)
# Check consolidated strings table
self.assertIn("### Strings", report)
self.assertIn("| Key | Source Text | French | Spanish |", report)
self.assertIn("| goodbye | Goodbye | | Adiós |", report)
self.assertIn(
"| hello | Hello World | Bonjour le monde | Hola Mundo |", report
)

# Check plural table
self.assertIn("### Plural Resources", report)
self.assertIn("#### days", report)
self.assertIn("| Quantity | Source Text | Spanish |", report)
self.assertIn("| one | %d day | %d día |", report)
self.assertIn("| other | %d days | %d días |", report)

@patch("AndroidResourceTranslator.AndroidResourceFile.parse_file")
def test_check_missing_translations_none_missing(self, mock_parse_file):
Expand Down