web/js/i18n.js
Rama main · réplica de lectura del repositorio original.
/* web/js/i18n.js — product chrome strings (fa default, en secondary).
*
* The engine accepts Persian and English questions either way; this file
* only covers the shell the analyst clicks. Full transcript copy stays
* Persian-first until a dedicated pass translates render/*.
*
* Usage:
* import { t, applyLang, getLang, setLang } from "./i18n.js";
* applyLang("en");
* t("apiKey") → "API key"
*
* Markup: put data-i18n="key" on an element; applyLang fills textContent.
* Placeholders use data-i18n-attr="placeholder:askPlaceholder".
*/
"use strict";
const STORAGE_LANG_KEY = "lsa-web-lang";
/** @type {Record<"fa"|"en", Record<string, string>>} */
const STRINGS = {
fa: {
productTitle: "عامل هوشمند SQL",
productTagline: "پرسش به زبان فارسی ← SQL ← پاسخ، روی زیرساخت خودتان",
memory: "حافظهٔ تحلیلی",
health: "سلامت",
healthStateUp: "سالم",
healthStateDown: "قطع",
healthStateUnknown: "نامشخص",
userMenu: "حساب",
theme: "پوسته",
themeSystem: "سیستم",
themeLight: "روشن",
themeDark: "تیره",
language: "زبان",
langFa: "فارسی",
langEn: "English",
apiKey: "کلید API",
apiKeyPlaceholder: "کلید API خود را وارد کنید",
apiKeySave: "ذخیره",
apiKeyChange: "تغییر کلید",
apiKeyClear: "حذف کلید",
apiKeySaved: "کلید: ذخیره شده",
apiKeyUnset: "کلید: تنظیم نشده",
apiKeyRejected: "کلید: رد شد",
apiKeyStoredNotice: "کلید API ذخیره شد — این کلید فقط در همین مرورگر نگهداری میشود.",
apiKeyClearedNotice: "کلید API حذف شد. برای پرسیدن سؤال در حالت زندهٔ API باید دوباره یک کلید وارد کنید.",
accessRequests: "درخواستهای دسترسی",
accessRequestsEmpty: "درخواستی ثبت نشده است.",
accessRequestStatusOpen: "در انتظار بررسی",
accessRequestStatusApproved: "تأیید شد",
accessRequestStatusDenied: "رد شد",
sessions: "گفتگوها",
newSession: "+ گفتگوی جدید",
askLabel: "پرسش خود را به زبان طبیعی بنویسید",
askPlaceholder: "مثلاً «معاملات مشتریهای تالار سیمان را نشان بده»",
ask: "پرسش",
interpret: "تفسیر نتیجه",
interpretHint: "خلاصهٔ سادهشده، با فرستادن حداکثر ۲۰ ردیف نتیجه به مدل زبانی",
samples: "داستان نمونه:",
startExamples: "یا یکی از اینها را بپرسید:",
transcriptEmptyHint: "برای شروع، پرسش خود را در پایین بنویسید.",
transcript: "گفتگوی تحلیلی",
memoryPanel: "حافظهٔ تحلیلی — اولویتهای ثابت",
closeMemory: "بستن پنل حافظه",
footLiveChecking: "حالت زندهٔ API — در حال بررسی اتصال به بکاند...",
themeSystemLabel: "پوسته: سیستم",
themeLightLabel: "پوسته: روشن",
themeDarkLabel: "پوسته: تیره",
invalidBaseUrlNotice: "نشانی بکاند نامعتبر است و نادیده گرفته شد — باید یک نشانی http:// یا https:// معتبر باشد.",
},
en: {
productTitle: "SQL Agent",
productTagline: "Ask in natural language → SQL → answer, on your infrastructure",
memory: "Memory",
health: "Health",
healthStateUp: "up",
healthStateDown: "down",
healthStateUnknown: "unknown",
userMenu: "Account",
theme: "Theme",
themeSystem: "System",
themeLight: "Light",
themeDark: "Dark",
language: "Language",
langFa: "فارسی",
langEn: "English",
apiKey: "API key",
apiKeyPlaceholder: "Enter your API key",
apiKeySave: "Save",
apiKeyChange: "Change key",
apiKeyClear: "Remove key",
apiKeySaved: "Key: saved",
apiKeyUnset: "Key: not set",
apiKeyRejected: "Key: rejected",
apiKeyStoredNotice: "API key saved — it stays only in this browser.",
apiKeyClearedNotice: "API key removed. Enter a key again to ask live questions.",
accessRequests: "Access requests",
accessRequestsEmpty: "No requests yet.",
accessRequestStatusOpen: "Pending",
accessRequestStatusApproved: "Approved",
accessRequestStatusDenied: "Denied",
sessions: "Conversations",
newSession: "+ New",
askLabel: "Ask a question in natural language",
askPlaceholder: "e.g. “Top cement-ring customers by trade value”",
ask: "Ask",
interpret: "Interpret result",
interpretHint: "Plain-language summary; up to 20 result rows go to the model",
samples: "Sample stories:",
startExamples: "Or ask one of these:",
transcriptEmptyHint: "Ask your question below to get started.",
transcript: "Conversation",
memoryPanel: "Analytical memory — standing preferences",
closeMemory: "Close memory panel",
footLiveChecking: "Live API — checking backend…",
themeSystemLabel: "Theme: system",
themeLightLabel: "Theme: light",
themeDarkLabel: "Theme: dark",
invalidBaseUrlNotice: "Invalid backend address — ignored. Must be a valid http:// or https:// address.",
},
};
let current = "fa";
/**
* @returns {"fa"|"en"}
*/
export function getLang() {
return current;
}
/**
* @param {string} key
* @returns {string}
*/
export function t(key) {
const table = STRINGS[current] || STRINGS.fa;
return table[key] ?? STRINGS.fa[key] ?? key;
}
/**
* Persist and apply chrome language (html lang/dir + [data-i18n]).
*
* @param {"fa"|"en"} lang
* @returns {void}
*/
export function setLang(lang) {
current = lang === "en" ? "en" : "fa";
try {
localStorage.setItem(STORAGE_LANG_KEY, current);
} catch { /* private mode — apply for this session only */ }
applyLang(current);
}
/**
* Read persisted language, default fa.
*
* @returns {"fa"|"en"}
*/
export function loadLang() {
try {
const raw = localStorage.getItem(STORAGE_LANG_KEY);
if (raw === "en" || raw === "fa") current = raw;
} catch { /* ignore */ }
return current;
}
/**
* Set document direction and fill data-i18n nodes.
*
* @param {"fa"|"en"} [lang]
* @returns {void}
*/
export function applyLang(lang) {
if (lang) current = lang === "en" ? "en" : "fa";
const root = document.documentElement;
root.lang = current;
root.dir = current === "fa" ? "rtl" : "ltr";
document.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.getAttribute("data-i18n");
if (key) el.textContent = t(key);
});
document.querySelectorAll("[data-i18n-attr]").forEach((el) => {
// "placeholder:key" or "aria-label:key"; comma-separated for several
const spec = el.getAttribute("data-i18n-attr") || "";
spec.split(",").forEach((part) => {
const sep = part.indexOf(":");
if (sep < 1) return;
const attr = part.slice(0, sep).trim();
const key = part.slice(sep + 1).trim();
if (attr && key) el.setAttribute(attr, t(key));
});
});
}