mirror of
https://github.com/Azure/co-op-translator
synced 2026-08-09 12:00:08 +00:00
Core: Share translation request normalization (#470)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
Reference in New Issue
Block a user