fix: use Google Cloud API key for classic mode + translation verification
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2s
Two critical fixes: 1. Provider "google" (default classic mode) now checks for a Google Cloud API key (GOOGLE_CLOUD_API_KEY in env or admin settings). If present, uses GoogleCloudTranslationProvider (official API). Previously it always fell through to deep_translator (free scraper) which gets blocked in production, silently returning untranslated text. 2. Added translation verification: each translator now tracks how many texts were attempted vs actually changed. If 0 texts were translated, the job is marked as FAILED with a clear error message instead of returning the original file as "completed". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -951,7 +951,21 @@ async def _run_translation_job(
|
|||||||
translation_provider = None
|
translation_provider = None
|
||||||
_p = provider.lower()
|
_p = provider.lower()
|
||||||
|
|
||||||
if _p in ("openrouter", "llm") and api_key:
|
# "google" (default classic mode): use Google Cloud API key if available,
|
||||||
|
# otherwise fall back to deep_translator (legacy, no key).
|
||||||
|
if _p == "google":
|
||||||
|
gc_key = _cfg(
|
||||||
|
getattr(_admin_cfg.google_cloud, "api_key", None),
|
||||||
|
"GOOGLE_CLOUD_API_KEY",
|
||||||
|
)
|
||||||
|
if gc_key:
|
||||||
|
from services.providers.google_cloud_provider import LegacyGoogleCloudAdapter
|
||||||
|
translation_provider = LegacyGoogleCloudAdapter(gc_key)
|
||||||
|
logger.info("google_provider_using_cloud_api", job_id=job_id)
|
||||||
|
else:
|
||||||
|
logger.info("google_provider_no_cloud_key_using_legacy", job_id=job_id)
|
||||||
|
|
||||||
|
elif _p in ("openrouter", "llm") and api_key:
|
||||||
translation_provider = OpenRouterTranslationProvider(
|
translation_provider = OpenRouterTranslationProvider(
|
||||||
api_key, model, full_prompt
|
api_key, model, full_prompt
|
||||||
)
|
)
|
||||||
@@ -1114,6 +1128,30 @@ async def _run_translation_job(
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported file type: {file_extension}")
|
raise ValueError(f"Unsupported file type: {file_extension}")
|
||||||
|
|
||||||
|
# ── Verify translation actually produced results ──
|
||||||
|
if not output_path.exists() or output_path.stat().st_size == 0:
|
||||||
|
error_msg = "Translation failed: output file is empty or missing. The translation provider may be unavailable."
|
||||||
|
logger.error(f"Job {job_id}: {error_msg}")
|
||||||
|
tracker.set_error(error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
stats = job_translator.get_translation_stats()
|
||||||
|
attempted = stats.get("attempted", 0)
|
||||||
|
changed = stats.get("changed", 0)
|
||||||
|
|
||||||
|
if attempted > 0:
|
||||||
|
ratio = changed / attempted
|
||||||
|
logger.info(f"Job {job_id}: translation stats — {changed}/{attempted} texts changed ({ratio:.0%})")
|
||||||
|
if ratio < 0.15 and changed == 0:
|
||||||
|
error_msg = (
|
||||||
|
f"Translation failed: 0 out of {attempted} texts were translated. "
|
||||||
|
f"The provider ({provider}) may be unavailable or misconfigured. "
|
||||||
|
f"Check your API keys in admin settings."
|
||||||
|
)
|
||||||
|
logger.error(f"Job {job_id}: {error_msg}")
|
||||||
|
tracker.set_error(error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
await tier_quota_service.increment_on_success(user_id)
|
await tier_quota_service.increment_on_success(user_id)
|
||||||
# Persist monthly usage counters in PostgreSQL (docs + pages)
|
# Persist monthly usage counters in PostgreSQL (docs + pages)
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class ExcelTranslator:
|
|||||||
self._provider = provider
|
self._provider = provider
|
||||||
self.formula_pattern = re.compile(r"=.*")
|
self.formula_pattern = re.compile(r"=.*")
|
||||||
self._custom_prompt: Optional[str] = None
|
self._custom_prompt: Optional[str] = None
|
||||||
|
self._translation_stats = {"attempted": 0, "changed": 0}
|
||||||
|
|
||||||
def set_provider(self, provider: TranslationProvider) -> None:
|
def set_provider(self, provider: TranslationProvider) -> None:
|
||||||
"""Set the translation provider."""
|
"""Set the translation provider."""
|
||||||
@@ -387,26 +388,26 @@ class ExcelTranslator:
|
|||||||
def _batch_translate(
|
def _batch_translate(
|
||||||
self, texts: List[str], target_language: str, source_language: str = "auto"
|
self, texts: List[str], target_language: str, source_language: str = "auto"
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
|
||||||
Batch translate using new provider interface.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
texts: List of texts to translate
|
|
||||||
target_language: Target language code
|
|
||||||
source_language: Source language code
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of translated texts (same order as input)
|
|
||||||
"""
|
|
||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
non_empty = [t for t in texts if t and t.strip()]
|
||||||
|
self._translation_stats["attempted"] += len(non_empty)
|
||||||
|
|
||||||
if self._provider is not None:
|
if self._provider is not None:
|
||||||
return self._translate_with_provider(
|
translated = self._translate_with_provider(
|
||||||
texts, target_language, source_language
|
texts, target_language, source_language
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
translated = self._translate_with_legacy(texts, target_language, source_language)
|
||||||
|
|
||||||
return self._translate_with_legacy(texts, target_language, source_language)
|
changed = sum(1 for orig, trans in zip(texts, translated) if orig != trans and trans.strip())
|
||||||
|
self._translation_stats["changed"] += changed
|
||||||
|
|
||||||
|
return translated
|
||||||
|
|
||||||
|
def get_translation_stats(self) -> dict:
|
||||||
|
return dict(self._translation_stats)
|
||||||
|
|
||||||
def _translate_with_provider(
|
def _translate_with_provider(
|
||||||
self, texts: List[str], target_language: str, source_language: str
|
self, texts: List[str], target_language: str, source_language: str
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class PDFTranslator:
|
|||||||
def __init__(self, provider=None):
|
def __init__(self, provider=None):
|
||||||
self._provider = provider
|
self._provider = provider
|
||||||
self._font_path: Optional[str] = None
|
self._font_path: Optional[str] = None
|
||||||
|
self._translation_stats = {"attempted": 0, "changed": 0}
|
||||||
|
|
||||||
def _get_font_path(self) -> Optional[str]:
|
def _get_font_path(self) -> Optional[str]:
|
||||||
"""Resolve a Unicode-capable TTF/OTF font file."""
|
"""Resolve a Unicode-capable TTF/OTF font file."""
|
||||||
@@ -825,18 +826,31 @@ class PDFTranslator:
|
|||||||
self, texts: List[str], target_language: str, source_language: str
|
self, texts: List[str], target_language: str, source_language: str
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""Translate a batch of texts."""
|
"""Translate a batch of texts."""
|
||||||
|
non_empty = [t for t in texts if t and t.strip()]
|
||||||
|
self._translation_stats["attempted"] += len(non_empty)
|
||||||
|
|
||||||
|
translated = None
|
||||||
if self._provider is not None:
|
if self._provider is not None:
|
||||||
try:
|
try:
|
||||||
return self._provider.translate_batch(texts, target_language, source_language)
|
translated = self._provider.translate_batch(texts, target_language, source_language)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("provider_translate_failed", error=str(e))
|
logger.warning("provider_translate_failed", error=str(e))
|
||||||
|
|
||||||
from services.translation_service import translation_service
|
if translated is None:
|
||||||
try:
|
from services.translation_service import translation_service
|
||||||
return translation_service.translate_batch(texts, target_language, source_language)
|
try:
|
||||||
except Exception as e:
|
translated = translation_service.translate_batch(texts, target_language, source_language)
|
||||||
logger.warning("legacy_translate_failed", error=str(e))
|
except Exception as e:
|
||||||
return texts
|
logger.warning("legacy_translate_failed", error=str(e))
|
||||||
|
translated = texts
|
||||||
|
|
||||||
|
changed = sum(1 for orig, trans in zip(texts, translated) if orig != trans and trans.strip())
|
||||||
|
self._translation_stats["changed"] += changed
|
||||||
|
|
||||||
|
return translated
|
||||||
|
|
||||||
|
def get_translation_stats(self) -> dict:
|
||||||
|
return dict(self._translation_stats)
|
||||||
|
|
||||||
def _validate_file(self, file_path: Path) -> None:
|
def _validate_file(self, file_path: Path) -> None:
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ class PowerPointTranslator:
|
|||||||
"""
|
"""
|
||||||
self._provider = provider
|
self._provider = provider
|
||||||
self._custom_prompt: Optional[str] = None
|
self._custom_prompt: Optional[str] = None
|
||||||
|
self._translation_stats = {"attempted": 0, "changed": 0}
|
||||||
|
|
||||||
def set_provider(self, provider: TranslationProvider) -> None:
|
def set_provider(self, provider: TranslationProvider) -> None:
|
||||||
"""Set the translation provider."""
|
"""Set the translation provider."""
|
||||||
@@ -381,26 +382,26 @@ class PowerPointTranslator:
|
|||||||
def _batch_translate(
|
def _batch_translate(
|
||||||
self, texts: List[str], target_language: str, source_language: str = "auto"
|
self, texts: List[str], target_language: str, source_language: str = "auto"
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
|
||||||
Batch translate using new provider interface.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
texts: List of texts to translate
|
|
||||||
target_language: Target language code
|
|
||||||
source_language: Source language code
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of translated texts (same order as input)
|
|
||||||
"""
|
|
||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
non_empty = [t for t in texts if t and t.strip()]
|
||||||
|
self._translation_stats["attempted"] += len(non_empty)
|
||||||
|
|
||||||
if self._provider is not None:
|
if self._provider is not None:
|
||||||
return self._translate_with_provider(
|
translated = self._translate_with_provider(
|
||||||
texts, target_language, source_language
|
texts, target_language, source_language
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
translated = self._translate_with_legacy(texts, target_language, source_language)
|
||||||
|
|
||||||
return self._translate_with_legacy(texts, target_language, source_language)
|
changed = sum(1 for orig, trans in zip(texts, translated) if orig != trans and trans.strip())
|
||||||
|
self._translation_stats["changed"] += changed
|
||||||
|
|
||||||
|
return translated
|
||||||
|
|
||||||
|
def get_translation_stats(self) -> dict:
|
||||||
|
return dict(self._translation_stats)
|
||||||
|
|
||||||
def _translate_with_provider(
|
def _translate_with_provider(
|
||||||
self, texts: List[str], target_language: str, source_language: str
|
self, texts: List[str], target_language: str, source_language: str
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ class WordTranslator:
|
|||||||
"""
|
"""
|
||||||
self._provider = provider
|
self._provider = provider
|
||||||
self._custom_prompt: Optional[str] = None
|
self._custom_prompt: Optional[str] = None
|
||||||
|
self._translation_stats = {"attempted": 0, "changed": 0}
|
||||||
|
|
||||||
def set_provider(self, provider: TranslationProvider) -> None:
|
def set_provider(self, provider: TranslationProvider) -> None:
|
||||||
"""Set the translation provider."""
|
"""Set the translation provider."""
|
||||||
@@ -439,12 +440,23 @@ class WordTranslator:
|
|||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
non_empty = [t for t in texts if t and t.strip()]
|
||||||
|
self._translation_stats["attempted"] += len(non_empty)
|
||||||
|
|
||||||
if self._provider is not None:
|
if self._provider is not None:
|
||||||
return self._translate_with_provider(
|
translated = self._translate_with_provider(
|
||||||
texts, target_language, source_language
|
texts, target_language, source_language
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
translated = self._translate_with_legacy(texts, target_language, source_language)
|
||||||
|
|
||||||
return self._translate_with_legacy(texts, target_language, source_language)
|
changed = sum(1 for orig, trans in zip(texts, translated) if orig != trans and trans.strip())
|
||||||
|
self._translation_stats["changed"] += changed
|
||||||
|
|
||||||
|
return translated
|
||||||
|
|
||||||
|
def get_translation_stats(self) -> dict:
|
||||||
|
return dict(self._translation_stats)
|
||||||
|
|
||||||
def _translate_with_provider(
|
def _translate_with_provider(
|
||||||
self, texts: List[str], target_language: str, source_language: str
|
self, texts: List[str], target_language: str, source_language: str
|
||||||
|
|||||||
Reference in New Issue
Block a user