Core: Share translation request normalization (#470)

This commit is contained in:
Minseok Song
2026-06-30 23:51:00 +09:00
committed by GitHub
parent 6de824d486
commit e392b70f6f
4 changed files with 224 additions and 99 deletions
+38 -69
View File
@@ -1,4 +1,3 @@
import importlib.resources
import logging
import os
from contextlib import contextmanager
@@ -7,7 +6,6 @@ from pathlib import Path
from typing import Iterable, Mapping
import click
import yaml
from PIL import Image
from co_op_translator.config.base_config import Config
@@ -25,6 +23,10 @@ from co_op_translator.core.llm.jupyter_notebook_translator import (
from co_op_translator.core.llm.markdown_translator import MarkdownTranslator
from co_op_translator.core.project.language_migrator import LanguageFolderMigrator
from co_op_translator.core.project.project_translator import ProjectTranslator
from co_op_translator.core.project.translation.request import (
build_translation_request,
resolve_translation_types,
)
from co_op_translator.core.vision.image_translator import ImageTranslator
from co_op_translator.glossary import glossary_terms_scope
from co_op_translator.utils.common.file_utils import (
@@ -33,7 +35,6 @@ from co_op_translator.utils.common.file_utils import (
update_readme_languages_table,
update_readme_other_courses,
)
from co_op_translator.utils.common.lang_utils import normalize_language_codes
from co_op_translator.utils.common.logging_utils import setup_logging
from co_op_translator.utils.common.metadata_utils import (
normalize_language_codes_in_lang_metadata,
@@ -304,15 +305,13 @@ def run_translation(
) -> None:
Config.check_configuration()
translation_types: list[str] = []
if markdown:
translation_types.append("markdown")
if images:
translation_types.append("images")
if notebook:
translation_types.append("notebook")
if not translation_types:
translation_types = ["markdown", "notebook", "images"]
translation_types = list(
resolve_translation_types(
markdown=markdown,
images=images,
notebook=notebook,
)
)
if "images" in translation_types:
cv_available = VisionConfig.check_configuration()
@@ -359,43 +358,16 @@ def run_translation(
logger.info("Auto-confirming 'all' languages in non-interactive mode.")
click.echo("Auto-confirming translation for all languages...")
try:
with importlib.resources.path(
"co_op_translator.fonts", "font_language_mappings.yml"
) as mappings_path:
with open(mappings_path, "r", encoding="utf-8") as file:
font_mappings = yaml.safe_load(file)
if not font_mappings:
raise RuntimeError("Empty font mappings file")
language_codes = " ".join(
[
lang_code
for lang_code in font_mappings
if isinstance(font_mappings[lang_code], dict)
]
)
if not language_codes:
raise RuntimeError(
"No valid language codes found in font mappings"
)
logging.debug(
f"Loaded language codes from font mapping: {language_codes}"
)
except (FileNotFoundError, yaml.YAMLError) as e:
raise RuntimeError(f"Failed to load font mappings: {str(e)}") from e
if all_languages_selected:
try:
lang_list = Config.get_language_codes()
except Exception:
lang_list = [
code.strip() for code in language_codes.split() if code.strip()
]
else:
lang_list = [
code.strip() for code in language_codes.split() if code.strip()
]
lang_list = normalize_language_codes(lang_list) if lang_list else []
request = build_translation_request(
language_codes=language_codes,
root_dir=root_dir,
markdown=markdown,
images=images,
notebook=notebook,
)
language_codes = request.language_codes
lang_list = request.language_list_values()
translation_types = request.translation_types_list()
if update:
click.echo(
@@ -590,18 +562,17 @@ def run_translation(
lang_subdir: str | None,
repo_url: str | None,
) -> dict[str, int]:
translation_types: list[str] = []
if markdown:
translation_types.append("markdown")
if images:
translation_types.append("images")
if notebook:
translation_types.append("notebook")
if not translation_types:
translation_types = ["markdown", "notebook", "images"]
request = build_translation_request(
language_codes=language_codes,
root_dir=root_dir,
markdown=markdown,
images=images,
notebook=notebook,
)
translation_types = request.translation_types_list()
translator = ProjectTranslator(
language_codes,
request.language_codes,
root_dir,
translation_types=translation_types,
add_disclaimer=add_disclaimer,
@@ -610,7 +581,7 @@ def run_translation(
lang_subdir=lang_subdir,
)
virtual_file_contents = compute_pretranslation_virtual_inputs(
Path(root_dir).resolve(),
request.root_path,
translation_types,
repo_url=repo_url,
)
@@ -665,15 +636,13 @@ def run_translation(
"total": 0,
"words": 0,
}
translation_types_for_summary: list[str] = []
if markdown:
translation_types_for_summary.append("markdown")
if images:
translation_types_for_summary.append("images")
if notebook:
translation_types_for_summary.append("notebook")
if not translation_types_for_summary:
translation_types_for_summary = ["markdown", "notebook", "images"]
translation_types_for_summary = list(
resolve_translation_types(
markdown=markdown,
images=images,
notebook=notebook,
)
)
execution_targets: list[tuple[str, str | None, str | None]] = []
if groups is not None:
+22 -30
View File
@@ -16,13 +16,14 @@ from co_op_translator.utils.common.file_utils import (
update_readme_languages_table,
update_readme_other_courses,
)
from co_op_translator.utils.common.lang_utils import (
normalize_language_codes,
)
from co_op_translator.utils.common.metadata_utils import (
normalize_language_codes_in_lang_metadata,
)
from co_op_translator.core.project.language_migrator import LanguageFolderMigrator
from co_op_translator.core.project.translation.request import (
build_translation_request,
resolve_translation_types,
)
logger = logging.getLogger(__name__)
@@ -170,18 +171,13 @@ def translate_command(
# Check that the required environment variables are set
Config.check_configuration()
# Build translation types list based on user selection
translation_types = []
if markdown:
translation_types.append("markdown")
if images:
translation_types.append("images")
if notebook:
translation_types.append("notebook")
# Default: translate all supported file types if nothing specified
if not translation_types:
translation_types = ["markdown", "notebook", "images"]
translation_types = list(
resolve_translation_types(
markdown=markdown,
images=images,
notebook=notebook,
)
)
# Check Azure AI Service availability if images are included
if "images" in translation_types:
@@ -249,21 +245,17 @@ def translate_command(
click.echo("Proceeding with translation for all languages...")
else:
click.echo("Auto-confirming translation for all languages...")
# Use canonical list from config and normalize
lang_list = Config.get_language_codes()
if not lang_list:
raise click.ClickException(
"No valid language codes found in font mappings"
)
language_codes = " ".join(normalize_language_codes(lang_list))
else:
# Normalize explicit input codes to canonical form
lang_list = normalize_language_codes(
[code.strip() for code in language_codes.split()]
)
if not lang_list:
raise click.ClickException("No valid language codes provided")
language_codes = " ".join(lang_list)
request = build_translation_request(
language_codes=language_codes,
root_dir=root_dir,
markdown=markdown,
images=images,
notebook=notebook,
)
language_codes = request.language_codes
lang_list = request.language_list_values()
translation_types = request.translation_types_list()
# Detect and migrate alias-based language folders for the SELECTED languages
# This runs before any translation work to avoid redundant re-translation.
@@ -0,0 +1,97 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
from co_op_translator.config.base_config import Config
from co_op_translator.utils.common.lang_utils import normalize_language_codes
DEFAULT_TRANSLATION_TYPES = ("markdown", "notebook", "images")
@dataclass(frozen=True)
class TranslationRequest:
"""Normalized project translation options shared by CLI and API entrypoints."""
language_codes: str
language_list: tuple[str, ...]
translation_types: tuple[str, ...]
root_dir: str
root_path: Path
all_languages_selected: bool
def translation_types_list(self) -> list[str]:
return list(self.translation_types)
def language_list_values(self) -> list[str]:
return list(self.language_list)
def resolve_translation_types(
*,
markdown: bool = False,
images: bool = False,
notebook: bool = False,
) -> tuple[str, ...]:
"""Return translation types using the existing CLI/API option semantics."""
translation_types: list[str] = []
if markdown:
translation_types.append("markdown")
if images:
translation_types.append("images")
if notebook:
translation_types.append("notebook")
if not translation_types:
return DEFAULT_TRANSLATION_TYPES
return tuple(translation_types)
def normalize_requested_language_codes(
language_codes: str | Iterable[str],
) -> tuple[tuple[str, ...], bool]:
"""Normalize requested language codes and expand the existing exact ``all`` token."""
all_languages_selected = isinstance(language_codes, str) and language_codes == "all"
if all_languages_selected:
raw_codes = Config.get_language_codes()
if not raw_codes:
raise ValueError("No valid language codes found in font mappings")
elif isinstance(language_codes, str):
raw_codes = [code.strip() for code in language_codes.split()]
else:
raw_codes = [str(code).strip() for code in language_codes]
normalized = tuple(normalize_language_codes(raw_codes))
if not normalized:
raise ValueError("No valid language codes provided")
return normalized, all_languages_selected
def build_translation_request(
*,
language_codes: str | Iterable[str],
root_dir: str = ".",
markdown: bool = False,
images: bool = False,
notebook: bool = False,
) -> TranslationRequest:
"""Build normalized request data without performing I/O or provider checks."""
language_list, all_languages_selected = normalize_requested_language_codes(
language_codes
)
translation_types = resolve_translation_types(
markdown=markdown,
images=images,
notebook=notebook,
)
root_path = Path(root_dir).resolve()
return TranslationRequest(
language_codes=" ".join(language_list),
language_list=language_list,
translation_types=translation_types,
root_dir=root_dir,
root_path=root_path,
all_languages_selected=all_languages_selected,
)
@@ -0,0 +1,67 @@
from pathlib import Path
import pytest
from co_op_translator.core.project.translation import request
def test_resolve_translation_types_defaults_to_all_types():
assert request.resolve_translation_types() == ("markdown", "notebook", "images")
def test_resolve_translation_types_preserves_existing_explicit_order():
assert request.resolve_translation_types(
markdown=True,
images=True,
notebook=True,
) == ("markdown", "images", "notebook")
def test_build_translation_request_normalizes_language_codes(tmp_path):
result = request.build_translation_request(
language_codes="ko tw pt-br",
root_dir=str(tmp_path),
markdown=True,
)
assert result.language_codes == "ko zh-TW pt-BR"
assert result.language_list == ("ko", "zh-TW", "pt-BR")
assert result.translation_types == ("markdown",)
assert result.root_path == Path(tmp_path).resolve()
assert result.all_languages_selected is False
def test_build_translation_request_expands_all(monkeypatch, tmp_path):
monkeypatch.setattr(
request.Config,
"get_language_codes",
lambda: ["ko", "tw", "pt-br"],
)
result = request.build_translation_request(
language_codes="all",
root_dir=str(tmp_path),
)
assert result.language_codes == "ko zh-TW pt-BR"
assert result.language_list == ("ko", "zh-TW", "pt-BR")
assert result.translation_types == ("markdown", "notebook", "images")
assert result.all_languages_selected is True
def test_build_translation_request_rejects_empty_language_codes(tmp_path):
with pytest.raises(ValueError, match="No valid language codes provided"):
request.build_translation_request(
language_codes=" ",
root_dir=str(tmp_path),
)
def test_build_translation_request_rejects_empty_all_mapping(monkeypatch, tmp_path):
monkeypatch.setattr(request.Config, "get_language_codes", lambda: [])
with pytest.raises(ValueError, match="No valid language codes found"):
request.build_translation_request(
language_codes="all",
root_dir=str(tmp_path),
)