Files
openobserve/scripts/translations/main.py
T
Prabhat Sharma eaba77fa34 fix(i18n): DeepSeek translation pipeline, BCP-47/en-US locales, +ru/pl/vi, full backfill (#13263)
## Summary

Replaces the AWS Translate i18n pipeline with a DeepSeek (LLM) backend,
standardizes locale files to BCP-47 naming with an `en-US` base, adds
new languages, and backfills **every locale to 100%**.

## What changed

### Translation engine (DeepSeek)
- Swap AWS Translate → DeepSeek via its OpenAI-compatible API
(`scripts/translations/`). Auth via the `DEEPSEEK_API_KEY` secret;
per-request 60s timeout.
- **Batched** translation (50/call) with **per-string validation**:
interpolation placeholders (`{count}`, `%s`, `@:key`), vue-i18n
**pluralization pipes** (`{n} Day \| {n} Days`), and literal escapes
(`{'@'}`, `{'{'}`, `{'\|'}`) are preserved or the string is rejected and
retried.
- Robust recovery: index-aligned batches, split-on-mismatch to isolate a
single bad string, temperature-perturbed retries, and rejection of
empty/whitespace output (never blanks a label). Removed the old
`TRANSLATION_MAX_KEYS` safety cap.

### Locale naming + base language
- All 15 locale files renamed to **BCP-47 hyphen** form (`en-US.json`,
`zh-CN.json`, `zh-TW.json`, `fr-FR.json`, …) — matching
`navigator.language` / `Intl` / HTML `lang`.
- **English base switched `en-gb` → `en-us`** (app code,
`fallbackLocale`, BCP-47 map, selector, defaults). British-Pound
currency and date-formatting `en-GB` usages left untouched. Existing
`en-gb` cookies fall back cleanly to English — no migration needed.

### New languages
- **Russian (`ru`), Polish (`pl`), Vietnamese (`vi`)** — fully generated
and wired into `localeFileMap`, `APP_LOCALE_TO_BCP47`, and the language
selector.
- **Traditional Chinese (`zh-TW`)** now maintained by the translator
alongside Simplified (`zh-CN`).
- RTL languages (`ar`, `fa`) intentionally **deferred** until the app
has `dir="rtl"` support.

### Full backfill (one-time)
- Every pre-existing locale filled from ~5.9k up to the full **8,269
keys** (~26.9k strings). `.translation_state.json` rebuilt complete, so
future runs are incremental.

### Nav i18n bug fix
- The **Dashboards** and **Data** nav group labels were hardcoded
English (bypassing i18n) and stayed English in every locale. Now
resolved via `t()` (`menu.dashboard`, new `menu.data`), localized and
re-resolving on language change.

## Verification
- Full web unit suite: **39,035 passed / 414 skipped / 0 failed** (1,167
files).
- `type-check` clean (0 TS errors).
- Locale registry sync spec green; all 15 files = 8,269 leaves.
- Translation engine verified live against `deepseek-v4-flash`.

## Notes
- Since all locales are now 100%, the post-merge `update-translations`
workflow will find nothing pending (no follow-up translation PR).
- `ar`/`fa` to be added with RTL support in a follow-up.
2026-07-19 05:29:01 +00:00

108 lines
3.4 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Copyright 2026 OpenObserve Inc.
"""
Translation Generator for OpenObserve.
Generates translation files from en-US.json using DeepSeek (an LLM) via its
OpenAI-compatible API. Only newly added or modified English strings are
translated; already-translated, unchanged text is never re-sent (see
translator.py for the change-detection model).
Usage:
python main.py # translate all supported languages
python main.py fr-FR es-ES de-DE # translate specific languages (filename stems)
Environment:
DEEPSEEK_API_KEY Required. API key for https://api.deepseek.com.
DEEPSEEK_MODEL Model id (default "deepseek-v4-flash").
TRANSLATION_BATCH_SIZE Strings per API call (default 50).
"""
import json
import sys
from translator import (
build_locale,
build_state,
collect_pending_leaves,
get_language_file_path,
get_supported_languages,
load_json,
load_source,
load_state,
new_counters,
save_state,
translate_pending,
)
def main():
supported = get_supported_languages()
args = sys.argv[1:]
# `--force` is accepted for backward compatibility but is now a no-op: there
# is no safety cap to bypass.
requested = [a for a in args if not a.startswith("--")]
if requested:
invalid = [lang for lang in requested if lang not in supported]
if invalid:
print(f"WARNING: Unsupported language codes: {', '.join(invalid)}")
print(f"Supported languages: {', '.join(supported)}")
locales = [lang for lang in requested if lang in supported]
else:
locales = supported
if not locales:
print("ERROR: No valid languages to translate.")
sys.exit(1)
source = load_source()
if not source:
print("ERROR: en-US.json source is empty or missing.")
sys.exit(1)
state = load_state()
full_run = set(locales) == set(supported)
counters = new_counters()
locale_targets = {}
for locale in locales:
existing = load_json(get_language_file_path(locale), {})
pending = collect_pending_leaves(source, existing, state)
print(f"\nTranslating: {locale} ({len(pending)} strings pending)")
translated = translate_pending(pending, locale) if pending else {}
target = build_locale(source, existing, state, translated, counters)
with open(get_language_file_path(locale), "w", encoding="utf-8") as f:
f.write(json.dumps(target, indent=2, ensure_ascii=False) + "\n")
locale_targets[locale] = target
# Advance shared state only on a full run, where every supported locale was
# processed and "present in all locales" is meaningful. Subset runs translate
# what they need but leave state untouched (safe: at worst a later full run
# re-checks those keys).
if full_run:
state = build_state(source, locale_targets, counters["failed_paths"])
save_state(state)
else:
print("\nℹ️ Subset run — state file not advanced (run all languages to persist state).")
print("\n" + "-" * 60)
print(
f"Done. translated={counters['translated']}, kept={counters['kept']}, "
f"failed={counters['failed']} across {len(locales)} language(s)."
)
if counters["failed"]:
print(
f"ERROR: {counters['failed']} translation(s) failed — they will retry "
f"next run."
)
sys.exit(3)
if __name__ == "__main__":
main()