mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
## 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.
108 lines
3.4 KiB
Python
Executable File
108 lines
3.4 KiB
Python
Executable File
#!/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()
|