All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
- Add brainstorm feature with collaborative canvas, AI idea generation, live cursors, playback, and export - Add PDF upload/extraction/ingestion pipeline with pgvector document search (RAG) - Add document Q&A overlay with streaming chat and PDF preview - Add note attachments UI with status polling, grid layout, and auto-scroll - Add task extraction AI tool and agent executor improvements - Fix NoteEmbedding missing updatedAt column, re-index 66 notes with 1536-dim embeddings - Fix brainstorm 'Create Note' button: add success toast and redirect to created note - Fix memory echo notification infinite polling - Fix chat route to always include document_search tool - Add brainstorm i18n keys across all 14 locales - Add socket server for real-time brainstorm collaboration - Add hierarchical notebook selector and organize notebook dialog improvements - Add sidebar brainstorm section with session management - Update prisma schema with brainstorm tables, attachments, and document chunks
164 lines
5.1 KiB
Python
164 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Synchronise locales/*.json avec locales/en.json (référence des clés).
|
|
- Garde les chaînes déjà présentes et non vides dans la locale cible.
|
|
- Traduit les clés manquantes (Google via deep-translator).
|
|
Usage : depuis memento-note/ avec le venv :
|
|
.venv-i18n/bin/python scripts/sync_locales_from_en.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from deep_translator import GoogleTranslator
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
LOCALES = ROOT / "locales"
|
|
|
|
# Codes Google Translate pour deep-translator (source=en)
|
|
LANG_TARGETS = {
|
|
"fr": "fr",
|
|
"es": "es",
|
|
"de": "de",
|
|
"fa": "fa",
|
|
"it": "it",
|
|
"pt": "pt",
|
|
"ru": "ru",
|
|
"zh": "zh-CN",
|
|
"ja": "ja",
|
|
"ko": "ko",
|
|
"ar": "ar",
|
|
"hi": "hi",
|
|
"nl": "nl",
|
|
"pl": "pl",
|
|
}
|
|
|
|
|
|
def flatten_leaves(obj: dict, prefix: str = "") -> dict[str, str]:
|
|
out: dict[str, str] = {}
|
|
for k, v in obj.items():
|
|
path = f"{prefix}.{k}" if prefix else k
|
|
if isinstance(v, dict):
|
|
out.update(flatten_leaves(v, path))
|
|
elif isinstance(v, str):
|
|
out[path] = v
|
|
else:
|
|
raise TypeError(f"Valeur non supportée à {path}: {type(v)}")
|
|
return out
|
|
|
|
|
|
def unflatten_leaves(flat: dict[str, str]) -> dict:
|
|
root: dict = {}
|
|
for path, val in flat.items():
|
|
parts = path.split(".")
|
|
cur = root
|
|
for p in parts[:-1]:
|
|
cur = cur.setdefault(p, {})
|
|
cur[parts[-1]] = val
|
|
return root
|
|
|
|
|
|
def translate_unique(texts: list[str], target_code: str, batch_size: int = 35) -> dict[str, str]:
|
|
translator = GoogleTranslator(source="en", target=target_code)
|
|
mapping: dict[str, str] = {}
|
|
for i in range(0, len(texts), batch_size):
|
|
batch = texts[i : i + batch_size]
|
|
try:
|
|
outs = translator.translate_batch(batch)
|
|
except Exception as e:
|
|
print(f" batch erreur ({e}), traduction unitaire…", flush=True)
|
|
outs = []
|
|
for t in batch:
|
|
try:
|
|
outs.append(translator.translate(t))
|
|
except Exception:
|
|
outs.append(t)
|
|
time.sleep(0.15)
|
|
if len(outs) != len(batch):
|
|
outs = batch # fallback
|
|
for src, dst in zip(batch, outs):
|
|
mapping[src] = dst if isinstance(dst, str) else src
|
|
time.sleep(0.6)
|
|
print(f" … {min(i + batch_size, len(texts))}/{len(texts)}", flush=True)
|
|
return mapping
|
|
|
|
|
|
def merge_locale(en_flat: dict[str, str], loc_flat: dict[str, str], target_code: str) -> dict[str, str]:
|
|
text_to_keys: dict[str, list[str]] = {}
|
|
result: dict[str, str] = {}
|
|
|
|
for key, en_val in en_flat.items():
|
|
loc_val = loc_flat.get(key)
|
|
if isinstance(loc_val, str) and loc_val.strip():
|
|
result[key] = loc_val
|
|
else:
|
|
text_to_keys.setdefault(en_val, []).append(key)
|
|
|
|
if not text_to_keys:
|
|
return result
|
|
|
|
unique = list(text_to_keys.keys())
|
|
print(f" {len(unique)} textes uniques à traduire ({sum(len(v) for v in text_to_keys.values())} clés)", flush=True)
|
|
trans_map = translate_unique(unique, target_code)
|
|
|
|
for src, keys in text_to_keys.items():
|
|
tr = trans_map.get(src, src)
|
|
for k in keys:
|
|
result[k] = tr
|
|
return result
|
|
|
|
|
|
def main() -> int:
|
|
en_path = LOCALES / "en.json"
|
|
if not en_path.exists():
|
|
print("locales/en.json introuvable", file=sys.stderr)
|
|
return 1
|
|
|
|
en_obj = json.loads(en_path.read_text(encoding="utf-8"))
|
|
en_flat = flatten_leaves(en_obj)
|
|
print(f"Référence en.json : {len(en_flat)} clés feuilles", flush=True)
|
|
|
|
skip = {"en"}
|
|
for code, google_target in LANG_TARGETS.items():
|
|
if code in skip:
|
|
continue
|
|
path = LOCALES / f"{code}.json"
|
|
if not path.exists():
|
|
print(f"Absence de {path.name}, ignoré", flush=True)
|
|
continue
|
|
|
|
loc_obj = json.loads(path.read_text(encoding="utf-8"))
|
|
loc_flat = flatten_leaves(loc_obj)
|
|
|
|
before_missing = sum(1 for k in en_flat if k not in loc_flat or not str(loc_flat.get(k, "")).strip())
|
|
if before_missing == 0:
|
|
print(f"\n=== {code}.json — déjà complet ({len(en_flat)} clés), rien à faire", flush=True)
|
|
continue
|
|
|
|
print(f"\n=== {code}.json ({google_target}) — manquantes avant: {before_missing}", flush=True)
|
|
|
|
merged_flat = merge_locale(en_flat, loc_flat, google_target)
|
|
|
|
# Couverture complète des clés en
|
|
out_flat = dict(en_flat)
|
|
out_flat.update(merged_flat)
|
|
|
|
missing_after = sum(1 for k in en_flat if k not in out_flat or not str(out_flat[k]).strip())
|
|
if missing_after:
|
|
print(f"ERREUR: encore {missing_after} clés vides pour {code}", file=sys.stderr)
|
|
return 1
|
|
|
|
tree = unflatten_leaves(out_flat)
|
|
path.write_text(json.dumps(tree, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(f" Écrit {path.name}", flush=True)
|
|
|
|
print("\nTerminé.", flush=True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|