scripts: add full build/translate scripts for furnidata i18n
This commit is contained in:
1 parent
2339485e9a
commit
e66b2c1a5a
3 files changed
+351
No files matched your search
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Full furnidata translation script using LibreTranslate.
|
||||
Translates all customs (missing official translations) for all 13 languages.
|
||||
Run from /var/www/atom-nexst
|
||||
"""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import os
|
||||
import time
|
||||
import concurrent.futures
|
||||
import urllib.request
|
||||
import sys
|
||||
|
||||
# ── Config ─────────────────────────────────────────────────────────────
|
||||
MASTER_PATH = "/var/www/Gamedata/config/FurnitureData.json"
|
||||
OUT_DIR = "/var/www/Gamedata/config"
|
||||
CACHE_PATH = "/var/www/Gamedata/config/.translations_libre_cache.json"
|
||||
LIBRE_URL = "http://127.0.0.1:5000/translate"
|
||||
CONCURRENCY = 6
|
||||
LANGUAGES = [
|
||||
("nl", "nl"),
|
||||
("en", "com"),
|
||||
("de", "de"),
|
||||
("fr", "fr"),
|
||||
("es", "es"),
|
||||
("tr", "com.tr"),
|
||||
("it", "it"),
|
||||
("pt", "com.br"),
|
||||
("fi", "fi"),
|
||||
("pl", "com"),
|
||||
("ru", "com"),
|
||||
("ar", "com"),
|
||||
("ja", "com"),
|
||||
]
|
||||
OFFICIAL_HOTEL_LANGS = {"nl", "en", "de", "fr", "es", "tr", "it", "pt", "fi"}
|
||||
# ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||
"Accept": "application/json,text/plain,*/*;q=0.9",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Sec-Fetch-Site": "cross-site",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
}
|
||||
|
||||
def load_json(path):
|
||||
return json.loads(pathlib.Path(path).read_text())
|
||||
|
||||
def save_json(path, data):
|
||||
tmp = path + ".tmp"
|
||||
pathlib.Path(tmp).write_text(json.dumps(data, ensure_ascii=False))
|
||||
os.rename(tmp, path)
|
||||
os.chmod(path, 0o640)
|
||||
try:
|
||||
import pwd, grp
|
||||
os.chown(path, pwd.getpwnam("www-data").pw_uid, grp.getgrnam("www-data").gr_gid)
|
||||
except:
|
||||
pass
|
||||
|
||||
def build_map(data):
|
||||
m = {}
|
||||
for sec in ("roomitemtypes", "wallitemtypes"):
|
||||
for e in data[sec]["furnitype"]:
|
||||
cn = e["classname"]
|
||||
if cn not in m:
|
||||
m[cn] = {"name": e.get("name", ""), "description": e.get("description", "")}
|
||||
return m
|
||||
|
||||
def is_meaningful(text, classname):
|
||||
if not isinstance(text, str):
|
||||
return False
|
||||
t = text.strip()
|
||||
if not t:
|
||||
return False
|
||||
if t == classname:
|
||||
return False
|
||||
if t == f"{classname} desc":
|
||||
return False
|
||||
if t == f"{classname} name":
|
||||
return False
|
||||
if len(t) < 2:
|
||||
return False
|
||||
return True
|
||||
|
||||
def libre_translate(text, target, cache):
|
||||
key = f"{target}|{text}"
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
data = json.dumps({"q": text, "source": "en", "target": target, "format": "text"}).encode()
|
||||
req = urllib.request.Request(LIBRE_URL, data=data, headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
out = json.loads(r.read().decode()).get("translatedText", text)
|
||||
except Exception as e:
|
||||
print(f" LibreTranslate error for {target} '{text[:40]}': {e}")
|
||||
out = text
|
||||
cache[key] = out
|
||||
return out
|
||||
|
||||
def fetch_official(lang, hotel):
|
||||
if lang not in OFFICIAL_HOTEL_LANGS:
|
||||
return {}
|
||||
url = f"https://www.habbo.{hotel}/gamedata/furnidata_json/1"
|
||||
print(f" Fetching official {lang} from habbo.{hotel}...")
|
||||
try:
|
||||
req = urllib.request.Request(url, headers=HEADERS)
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
data = json.loads(r.read().decode())
|
||||
except Exception as e:
|
||||
print(f" Failed to fetch official {lang}: {e}")
|
||||
return {}
|
||||
fmap = {}
|
||||
for sec in ("roomitemtypes", "wallitemtypes"):
|
||||
for e in data.get(sec, {}).get("furnitype", []):
|
||||
cn = e.get("classname", "")
|
||||
if not cn or cn in fmap:
|
||||
continue
|
||||
fmap[cn] = {"name": e.get("name", ""), "description": e.get("description", "")}
|
||||
print(f" Official {lang}: {len(fmap)} translations")
|
||||
return fmap
|
||||
|
||||
def translate_batch(texts, target, cache, concurrency=CONCURRENCY):
|
||||
if not texts:
|
||||
return {}
|
||||
print(f" Translating {len(texts)} unique texts to {target} with {concurrency} workers...")
|
||||
start = time.time()
|
||||
out = {}
|
||||
missing = [t for t in texts if f"{target}|{t}" not in cache]
|
||||
if not missing:
|
||||
return {t: cache[f"{target}|{t}"] for t in texts}
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex:
|
||||
futs = {ex.submit(libre_translate, t, target, cache): t for t in missing}
|
||||
done = 0
|
||||
for f in concurrent.futures.as_completed(futs):
|
||||
t = futs[f]
|
||||
try:
|
||||
out[t] = f.result()
|
||||
except Exception as e:
|
||||
print(f" Failed {t[:40]}: {e}")
|
||||
out[t] = t
|
||||
done += 1
|
||||
if done % 100 == 0:
|
||||
elapsed = time.time() - start
|
||||
print(f" {done}/{len(missing)} in {elapsed:.1f}s ({elapsed/max(done,1):.2f}s/req)")
|
||||
elapsed = time.time() - start
|
||||
print(f" Done {len(texts)} texts to {target} in {elapsed:.1f}s")
|
||||
return out
|
||||
|
||||
def main():
|
||||
print("Loading master FurnitureData.json...")
|
||||
master = load_json(MASTER_PATH)
|
||||
master_map = build_map(master)
|
||||
print(f"Master: {len(master_map)} entries")
|
||||
|
||||
# Load or create cache
|
||||
cache = {}
|
||||
if os.path.exists(CACHE_PATH):
|
||||
try:
|
||||
cache = load_json(CACHE_PATH)
|
||||
print(f"Loaded cache: {len(cache)} entries")
|
||||
except Exception:
|
||||
print("Cache corrupt, starting fresh")
|
||||
cache = {}
|
||||
|
||||
# For each language
|
||||
for lang, hotel in LANGUAGES:
|
||||
if lang == "en":
|
||||
print(f"\n=== {lang} (source) ===")
|
||||
# en file is just copy of master
|
||||
out_path = f"{OUT_DIR}/FurnitureData_en.json"
|
||||
save_json(out_path, master)
|
||||
print(f" Wrote {out_path}")
|
||||
continue
|
||||
|
||||
out_path = f"{OUT_DIR}/FurnitureData_{lang}.json"
|
||||
print(f"\n=== {lang} (hotel: {hotel}) ===")
|
||||
|
||||
# Fetch official translations if available
|
||||
official = fetch_official(lang, hotel) if lang in OFFICIAL_HOTEL_LANGS else {}
|
||||
|
||||
# Clone master
|
||||
cloned = json.loads(json.dumps(master))
|
||||
translated = 0
|
||||
official_count = 0
|
||||
libre_count = 0
|
||||
|
||||
# Collect all texts needing translation (customs not covered by official)
|
||||
missing_names = set()
|
||||
missing_descs = set()
|
||||
|
||||
for sec in ("roomitemtypes", "wallitemtypes"):
|
||||
for e in cloned[sec]["furnitype"]:
|
||||
cn = e["classname"]
|
||||
t = official.get(cn)
|
||||
if not t and "*" in cn:
|
||||
base = cn.split("*")[0]
|
||||
t = official.get(f"{base}*0") or official.get(base)
|
||||
if not t and "*" not in cn:
|
||||
t = official.get(f"{cn}*0")
|
||||
|
||||
orig_name = e["name"]
|
||||
orig_desc = e.get("description", "")
|
||||
|
||||
# Apply official if available
|
||||
if t:
|
||||
did = False
|
||||
if t.get("name"):
|
||||
e["name"] = t["name"]
|
||||
did = True
|
||||
if t.get("description"):
|
||||
e["description"] = t["description"]
|
||||
did = True
|
||||
if did:
|
||||
translated += 1
|
||||
official_count += 1
|
||||
continue
|
||||
|
||||
# Collect missing for LibreTranslate
|
||||
if is_meaningful(orig_name, cn):
|
||||
missing_names.add(orig_name.strip())
|
||||
if is_meaningful(orig_desc, cn):
|
||||
missing_descs.add(orig_desc.strip())
|
||||
|
||||
print(f" Official applied: {official_count}, missing for Libre: {len(missing_names)} names, {len(missing_descs)} descs")
|
||||
|
||||
# Translate missing via LibreTranslate
|
||||
if missing_names:
|
||||
tr_names = translate_batch(missing_names, lang, cache, CONCURRENCY)
|
||||
for sec in ("roomitemtypes", "wallitemtypes"):
|
||||
for e in cloned[sec]["furnitype"]:
|
||||
cn = e["classname"]
|
||||
orig = e["name"]
|
||||
if is_meaningful(orig, cn):
|
||||
tr = tr_names.get(orig.strip())
|
||||
if tr and tr != orig:
|
||||
e["name"] = tr
|
||||
translated += 1
|
||||
libre_count += 1
|
||||
|
||||
if missing_descs:
|
||||
tr_descs = translate_batch(missing_descs, lang, cache, CONCURRENCY)
|
||||
for sec in ("roomitemtypes", "wallitemtypes"):
|
||||
for e in cloned[sec]["furnitype"]:
|
||||
cn = e["classname"]
|
||||
orig = e.get("description", "")
|
||||
if is_meaningful(orig, cn):
|
||||
tr = tr_descs.get(orig.strip())
|
||||
if tr and tr != orig:
|
||||
e["description"] = tr
|
||||
translated += 1
|
||||
libre_count += 1
|
||||
|
||||
# Save cache after each language
|
||||
save_json(CACHE_PATH, cache)
|
||||
print(f" LibreTranslate applied: {libre_count}, total translated: {translated}")
|
||||
|
||||
# Write output
|
||||
save_json(out_path, cloned)
|
||||
print(f" Wrote {out_path} ({pathlib.Path(out_path).stat().st_size / 1024 / 1024:.1f} MB)")
|
||||
|
||||
# Final cache save
|
||||
save_json(CACHE_PATH, cache)
|
||||
print(f"\n=== All done! Cache: {len(cache)} entries ===")
|
||||
|
||||
# Summary
|
||||
for lang, _ in LANGUAGES:
|
||||
fmap = build_map(load_json(f"{OUT_DIR}/FurnitureData_{lang}.json"))
|
||||
ident = sum(1 for k, v in master_map.items() if fmap.get(k) and fmap[k]["name"] == v["name"])
|
||||
pct = (len(master_map) - ident) / len(master_map) * 100
|
||||
print(f" {lang}: {len(master_map)-ident}/{len(master_map)} translated ({pct:.1f}%)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,43 @@
|
||||
import "./load-env";
|
||||
import { buildLocalizedFurniDataFiles } from "../src/lib/services/furni-data-i18n";
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const full = args.includes("--full");
|
||||
const maxPerLang = full
|
||||
? undefined
|
||||
: args.includes("--limit")
|
||||
? parseInt(args[args.indexOf("--limit") + 1] || "2000", 10)
|
||||
: 1500;
|
||||
const concurrency = args.includes("--concurrency")
|
||||
? parseInt(args[args.indexOf("--concurrency") + 1] || "6", 10)
|
||||
: 6;
|
||||
|
||||
console.log(
|
||||
`[build-languages] Starting ${full ? "FULL" : `limited ${maxPerLang} per lang`} build (concurrency ${concurrency})...`,
|
||||
);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const results = await buildLocalizedFurniDataFiles({
|
||||
maxPerLang,
|
||||
concurrency,
|
||||
useLibre: true,
|
||||
});
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.log(`[build-languages] Done in ${elapsed}s`);
|
||||
for (const r of results) {
|
||||
const totalTranslated = r.translatedRoom + r.translatedWall;
|
||||
const pct =
|
||||
r.total > 0 ? ((totalTranslated / r.total) * 100).toFixed(1) : "0.0";
|
||||
console.log(
|
||||
` ${r.lang} (${r.hotel}): ${totalTranslated}/${r.total} translated (${pct}%) -> ${r.file} ${r.ok ? "OK" : `FAIL ${r.error}`}`,
|
||||
);
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
console.error("[build-languages] Failed:", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,32 @@
|
||||
import "./load-env";
|
||||
import { buildLocalizedFurniDataFiles } from "../src/lib/services/furni-data-i18n";
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const maxPerLang = args.includes("--full")
|
||||
? undefined
|
||||
: args.includes("--limit")
|
||||
? parseInt(args[args.indexOf("--limit") + 1] || "2000", 10)
|
||||
: 1500;
|
||||
const full = args.includes("--full");
|
||||
console.log(
|
||||
`[translate-furnidata] Starting ${full ? "FULL" : `limited ${maxPerLang} per lang`} build...`,
|
||||
);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const results = await buildLocalizedFurniDataFiles(
|
||||
full ? {} : { maxPerLang },
|
||||
);
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.log(`[translate-furnidata] Done in ${elapsed}s`);
|
||||
for (const r of results) {
|
||||
console.log(
|
||||
` ${r.lang} (${r.hotel}): ${r.translatedRoom + r.translatedWall}/${r.total} translated -> ${r.file} ${r.ok ? "OK" : `FAIL ${r.error}`}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
main();
|
||||
Reference in new issue
Block a user