fix: resolve Auto-Trim canvas CORS block with webSecurity, implement dynamic multi-language localization (EN, TR, DE, RU, AR) with English base
This commit is contained in:
+358
-19
@@ -33,6 +33,7 @@ const elements = {
|
||||
btnShowCredits: document.getElementById('btn-show-credits'),
|
||||
creditsOverlay: document.getElementById('credits-overlay'),
|
||||
btnCloseCredits: document.getElementById('btn-close-credits'),
|
||||
langSelect: document.getElementById('lang-select'),
|
||||
|
||||
// Setup Wizard
|
||||
setupOverlay: document.getElementById('setup-overlay'),
|
||||
@@ -106,6 +107,7 @@ const elements = {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
setupTitlebarListeners();
|
||||
setupToastListeners();
|
||||
initLanguage(); // Initialize translations immediately on load
|
||||
|
||||
// Check if Python virtual environment exists
|
||||
const { hasVenv } = await window.api.checkPythonSetup();
|
||||
@@ -424,18 +426,25 @@ function setupToolbarListeners() {
|
||||
function autoTrimTransparent() {
|
||||
if (!cropper || !processedTempPath) return;
|
||||
|
||||
// We request canvas size matching the full uncropped image dimension
|
||||
const canvas = cropper.getCroppedCanvas({
|
||||
width: cropper.getImageData().naturalWidth,
|
||||
height: cropper.getImageData().naturalHeight
|
||||
});
|
||||
// Create a temporary hidden canvas to read pixels directly from the original image element
|
||||
// This bypasses any cropper canvas restrictions or CORS issues when webSecurity is false
|
||||
const canvas = document.createElement('canvas');
|
||||
const img = elements.imageElement;
|
||||
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!canvas) {
|
||||
showToast("Trim Warning", "Could not analyze transparent boundaries.", "error");
|
||||
try {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
} catch (err) {
|
||||
const title = getTrans('toastTrimFailed');
|
||||
const msg = getTrans('toastTrimFailedMsg') + ": " + err.message;
|
||||
showToast(title, msg, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imgData.data;
|
||||
|
||||
@@ -445,11 +454,11 @@ function autoTrimTransparent() {
|
||||
let maxY = 0;
|
||||
let foundAlpha = false;
|
||||
|
||||
// Loop through pixels in grid to find bounding box of non-transparent values
|
||||
// Scan alpha channel (index + 3) to locate non-empty boundaries
|
||||
for (let y = 0; y < canvas.height; y++) {
|
||||
for (let x = 0; x < canvas.width; x++) {
|
||||
const alphaIndex = (y * canvas.width + x) * 4 + 3;
|
||||
if (data[alphaIndex] > 8) { // Alpha transparency cut-off
|
||||
if (data[alphaIndex] > 8) { // transparency threshold
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
@@ -460,16 +469,21 @@ function autoTrimTransparent() {
|
||||
}
|
||||
|
||||
if (foundAlpha) {
|
||||
// Add small padding to avoid clipping exactly
|
||||
// Add small padding to avoid clipping edge pixels exactly
|
||||
const pad = 10;
|
||||
minX = Math.max(0, minX - pad);
|
||||
minY = Math.max(0, minY - pad);
|
||||
maxX = Math.min(canvas.width, maxX + pad);
|
||||
maxY = Math.min(canvas.height, maxY + pad);
|
||||
|
||||
// Turn on Crop selection mode visual indicators
|
||||
cropper.setDragMode('crop');
|
||||
elements.ctrlCropDrag.classList.remove('active');
|
||||
elements.ctrlCropBox.classList.add('active');
|
||||
|
||||
// Convert pixel coordinates to Cropper canvas boundaries
|
||||
const canvasData = cropper.getCanvasData();
|
||||
const scale = canvasData.width / cropper.getImageData().naturalWidth;
|
||||
const scale = canvasData.width / img.naturalWidth;
|
||||
|
||||
// Set crop boundaries
|
||||
cropper.setCropBoxData({
|
||||
@@ -479,17 +493,342 @@ function autoTrimTransparent() {
|
||||
height: (maxY - minY) * scale
|
||||
});
|
||||
|
||||
// Switch to crop selection mode visual indicator
|
||||
cropper.setDragMode('crop');
|
||||
elements.ctrlCropDrag.classList.remove('active');
|
||||
elements.ctrlCropBox.classList.add('active');
|
||||
|
||||
showToast("Auto-Trim Complete", "Crop boundaries adjusted securely around subject.", "success");
|
||||
showToast(getTrans('toastTrimComplete'), getTrans('toastTrimCompleteMsg'), "success");
|
||||
} else {
|
||||
showToast("Trim Info", "No transparent border boundaries identified.", "info");
|
||||
showToast(getTrans('toastTitleInfo'), getTrans('toastTrimFailedMsg'), "info");
|
||||
}
|
||||
}
|
||||
|
||||
// --- LOCALIZATION ENGINE (i18n) ---
|
||||
const TRANSLATIONS = {
|
||||
en: {
|
||||
title: "Dekupai - Trunçgil AI Decoupage",
|
||||
sourceFile: "Source File",
|
||||
noImage: "No image loaded",
|
||||
fileName: "Name",
|
||||
fileResolution: "Resolution",
|
||||
fileSize: "Size",
|
||||
newImage: "Import New Image",
|
||||
bgRemoval: "Background Removal",
|
||||
extractSubject: "Extract Subject",
|
||||
initAI: "Initializing AI Model...",
|
||||
loadingWeights: "First launch downloads model (~200MB)",
|
||||
bgSettings: "Background Settings",
|
||||
transparent: "Transparent",
|
||||
solidColor: "Solid Color",
|
||||
gradient: "Gradient",
|
||||
selectSolid: "Select Solid Fill",
|
||||
selectGrad: "Select High-End Gradient",
|
||||
exportStudio: "Export Studio",
|
||||
format: "Format",
|
||||
quality: "Quality",
|
||||
exportMasterpiece: "Export Masterpiece",
|
||||
toastTitleInfo: "Info",
|
||||
toastTitleSuccess: "Success",
|
||||
toastTitleError: "Error",
|
||||
toastImageChanged: "Image Changed",
|
||||
toastImageChangedMsg: "Workspace cleared. Ready for a new image.",
|
||||
toastTrimComplete: "Auto-Trim Complete",
|
||||
toastTrimCompleteMsg: "Crop boundaries adjusted securely.",
|
||||
toastTrimFailed: "Trim Warning",
|
||||
toastTrimFailedMsg: "Could not analyze transparent boundaries.",
|
||||
toastSetupSuccess: "Setup Success",
|
||||
toastSetupSuccessMsg: "Python environment is fully configured.",
|
||||
toastSetupError: "Setup Error",
|
||||
toastSetupErrorMsg: "Failed to configure Python environment.",
|
||||
initWizardTitle: "Initializing AI Engine",
|
||||
initWizardSubtitle: "Dekupai requires a local Python environment to run the BiRefNet neural network models.",
|
||||
configureEngine: "Configure Engine (Automatic)",
|
||||
stepVenv: "Creating isolated virtual environment (.venv)",
|
||||
stepPip: "Upgrading Python package installer (pip)",
|
||||
stepDeps: "Installing PyTorch AI Engine (CPU version)",
|
||||
stepLibs: "Installing HuggingFace Transformers & PIL library",
|
||||
creditsVision: "Developed under the vision of",
|
||||
creditsExpertise: "by the engineering expertise of",
|
||||
creditsDesc: "Dekupai is an advanced deep-learning desktop workspace that provides local, high-precision background removal using BiRefNet neural segmentation networks.",
|
||||
closeCredits: "Close Credits",
|
||||
dragDropText: "Drag & Drop Image Here",
|
||||
dragDropSub: "Supports PNG, JPG, JPEG, or WebP formats",
|
||||
orText: "OR",
|
||||
browseFiles: "Browse Local Files",
|
||||
autoTrim: "Auto-Trim"
|
||||
},
|
||||
tr: {
|
||||
title: "Dekupai - Trunçgil AI Dekupaj",
|
||||
sourceFile: "Kaynak Dosya",
|
||||
noImage: "Görsel yüklenmedi",
|
||||
fileName: "Dosya Adı",
|
||||
fileResolution: "Çözünürlük",
|
||||
fileSize: "Boyut",
|
||||
newImage: "Yeni Görsel Yükle",
|
||||
bgRemoval: "Arka Plan Temizleme",
|
||||
extractSubject: "Özneyi Dekupe Et",
|
||||
initAI: "AI Modeli Başlatılıyor...",
|
||||
loadingWeights: "İlk çalıştırmada model indirilir (~200MB)",
|
||||
bgSettings: "Arka Plan Ayarları",
|
||||
transparent: "Şeffaf",
|
||||
solidColor: "Tek Renk",
|
||||
gradient: "Degrade",
|
||||
selectSolid: "Düz Renk Seçin",
|
||||
selectGrad: "Premium Degrade Seçin",
|
||||
exportStudio: "Dışa Aktarma Stüdyosu",
|
||||
format: "Format",
|
||||
quality: "Kalite",
|
||||
exportMasterpiece: "Çalışmayı Dışa Aktar",
|
||||
toastTitleInfo: "Bilgi",
|
||||
toastTitleSuccess: "Başarılı",
|
||||
toastTitleError: "Hata",
|
||||
toastImageChanged: "Görsel Değiştirildi",
|
||||
toastImageChangedMsg: "Çalışma alanı temizlendi. Yeni görsel için hazır.",
|
||||
toastTrimComplete: "Otomatik Kırpma Tamamlandı",
|
||||
toastTrimCompleteMsg: "Kırpma sınırları güvenli bir şekilde ayarlandı.",
|
||||
toastTrimFailed: "Kırpma Uyarısı",
|
||||
toastTrimFailedMsg: "Şeffaf sınırlar analiz edilemedi.",
|
||||
toastSetupSuccess: "Kurulum Başarılı",
|
||||
toastSetupSuccessMsg: "Python ortamı tamamen yapılandırıldı.",
|
||||
toastSetupError: "Kurulum Hatası",
|
||||
toastSetupErrorMsg: "Python ortamı yapılandırılamadı.",
|
||||
initWizardTitle: "AI Motoru Başlatılıyor",
|
||||
initWizardSubtitle: "Dekupai, BiRefNet yapay zeka modelini çalıştırmak için yerel bir Python ortamına ihtiyaç duyar.",
|
||||
configureEngine: "Motoru Yapılandır (Otomatik)",
|
||||
stepVenv: "İzole sanal ortam oluşturuluyor (.venv)",
|
||||
stepPip: "Python paket yöneticisi güncelleniyor (pip)",
|
||||
stepDeps: "PyTorch AI motoru kuruluyor (CPU versiyonu)",
|
||||
stepLibs: "HuggingFace Transformers ve PIL kuruluyor",
|
||||
creditsVision: "Vizyon ve liderlik",
|
||||
creditsExpertise: "yazılım geliştirme ve mühendislik",
|
||||
creditsDesc: "Dekupai, BiRefNet yapay sinir ağı segmentasyon modellerini kullanarak yerel ve yüksek hassasiyetli arka plan temizleme sağlayan gelişmiş bir derin öğrenme masaüstü uygulamasıdır.",
|
||||
closeCredits: "Kapat",
|
||||
dragDropText: "Görseli Buraya Sürükleyip Bırakın",
|
||||
dragDropSub: "PNG, JPG, JPEG veya WebP formatlarını destekler",
|
||||
orText: "VEYA",
|
||||
browseFiles: "Yerel Dosyalara Göz Atın",
|
||||
autoTrim: "Oto-Kırp"
|
||||
},
|
||||
de: {
|
||||
title: "Dekupai - Trunçgil AI Dekupage",
|
||||
sourceFile: "Quelldatei",
|
||||
noImage: "Kein Bild geladen",
|
||||
fileName: "Dateiname",
|
||||
fileResolution: "Auflösung",
|
||||
fileSize: "Größe",
|
||||
newImage: "Neues Bild Laden",
|
||||
bgRemoval: "Hintergrundentfernung",
|
||||
extractSubject: "Motiv Freistellen",
|
||||
initAI: "AI-Modell wird initialisiert...",
|
||||
loadingWeights: "Erster Start lädt Modell herunter (~200MB)",
|
||||
bgSettings: "Hintergrundeinstellungen",
|
||||
transparent: "Transparent",
|
||||
solidColor: "Vollfarbe",
|
||||
gradient: "Verlauf",
|
||||
selectSolid: "Volltonfarbe wählen",
|
||||
selectGrad: "Premium-Verlauf wählen",
|
||||
exportStudio: "Export Studio",
|
||||
format: "Format",
|
||||
quality: "Qualität",
|
||||
exportMasterpiece: "Meisterwerk Exportieren",
|
||||
toastTitleInfo: "Info",
|
||||
toastTitleSuccess: "Erfolg",
|
||||
toastTitleError: "Fehler",
|
||||
toastImageChanged: "Bild Geändert",
|
||||
toastImageChangedMsg: "Arbeitsbereich geleert. Bereit für neues Bild.",
|
||||
toastTrimComplete: "Auto-Zuschneiden Fertig",
|
||||
toastTrimCompleteMsg: "Zuschnittsgrenzen sicher angepasst.",
|
||||
toastTrimFailed: "Zuschnitt Warnung",
|
||||
toastTrimFailedMsg: "Transparente Grenzen konnten nicht analysiert werden.",
|
||||
toastSetupSuccess: "Setup erfolgreich",
|
||||
toastSetupSuccessMsg: "Python-Umgebung ist vollständig konfiguriert.",
|
||||
toastSetupError: "Setup-Fehler",
|
||||
toastSetupErrorMsg: "Konfiguration der Python-Umgebung fehlgeschlagen.",
|
||||
initWizardTitle: "AI-Engine wird initialisiert",
|
||||
initWizardSubtitle: "Dekupai erfordert eine lokale Python-Umgebung, um die BiRefNet-Modelle auszuführen.",
|
||||
configureEngine: "Engine konfigurieren (Automatisch)",
|
||||
stepVenv: "Erstellen einer isolierten virtuellen Umgebung (.venv)",
|
||||
stepPip: "Upgrade des Python-Paketinstallers (pip)",
|
||||
stepDeps: "Installation der PyTorch AI-Engine (CPU-Version)",
|
||||
stepLibs: "Installation der HuggingFace Transformers & PIL Bibliothek",
|
||||
creditsVision: "Entwickelt unter der Vision von",
|
||||
creditsExpertise: "durch die Software-Expertise von",
|
||||
creditsDesc: "Dekupai ist ein fortschrittlicher Deep-Learning-Desktop-Arbeitsbereich, der eine lokale, hochpräzise Hintergrundentfernung mithilfe von BiRefNet-Segmentierungsnetzwerken bietet.",
|
||||
closeCredits: "Schließen",
|
||||
dragDropText: "Bild hierher ziehen und ablegen",
|
||||
dragDropSub: "Unterstützt PNG-, JPG-, JPEG- oder WebP-Formate",
|
||||
orText: "ODER",
|
||||
browseFiles: "Lokale Dateien durchsuchen",
|
||||
autoTrim: "Auto-Trim"
|
||||
},
|
||||
ru: {
|
||||
title: "Dekupai - Trunçgil AI Декупаж",
|
||||
sourceFile: "Исходный файл",
|
||||
noImage: "Изображение не загружено",
|
||||
fileName: "Имя",
|
||||
fileResolution: "Разрешение",
|
||||
fileSize: "Размер",
|
||||
newImage: "Загрузить новое изображение",
|
||||
bgRemoval: "Удаление фона",
|
||||
extractSubject: "Вырезать объект",
|
||||
initAI: "Инициализация модели ИИ...",
|
||||
loadingWeights: "Первый запуск загружает модель (~200 МБ)",
|
||||
bgSettings: "Настройки фона",
|
||||
transparent: "Прозрачный",
|
||||
solidColor: "Сплошной цвет",
|
||||
gradient: "Градиент",
|
||||
selectSolid: "Выберите сплошной цвет",
|
||||
selectGrad: "Выберите премиум градиент",
|
||||
exportStudio: "Студия экспорта",
|
||||
format: "Формат",
|
||||
quality: "Качество",
|
||||
exportMasterpiece: "Экспортировать шедевр",
|
||||
toastTitleInfo: "Информация",
|
||||
toastTitleSuccess: "Успех",
|
||||
toastTitleError: "Ошибка",
|
||||
toastImageChanged: "Изображение изменено",
|
||||
toastImageChangedMsg: "Рабочая область очищена. Готово к новому изображению.",
|
||||
toastTrimComplete: "Автообрезка завершена",
|
||||
toastTrimCompleteMsg: "Границы обрезки успешно скорректированы.",
|
||||
toastTrimFailed: "Предупреждение об обрезке",
|
||||
toastTrimFailedMsg: "Не удалось проанализировать прозрачные границы.",
|
||||
toastSetupSuccess: "Установка успешна",
|
||||
toastSetupSuccessMsg: "Среда Python полностью настроена.",
|
||||
toastSetupError: "Ошибка установки",
|
||||
toastSetupErrorMsg: "Не удалось настроить среду Python.",
|
||||
initWizardTitle: "Инициализация движка ИИ",
|
||||
initWizardSubtitle: "Для работы Dekupai требуется локальная среда Python для запуска нейросетевых моделей BiRefNet.",
|
||||
configureEngine: "Настроить движок (Автоматически)",
|
||||
stepVenv: "Создание изолированной виртуальной среды (.venv)",
|
||||
stepPip: "Обновление установщика пакетов Python (pip)",
|
||||
stepDeps: "Установка движка ИИ PyTorch (версия для CPU)",
|
||||
stepLibs: "Установка библиотек HuggingFace Transformers и PIL",
|
||||
creditsVision: "Разработано под руководством",
|
||||
creditsExpertise: "программная инженерия и разработка",
|
||||
creditsDesc: "Dekupai — это продвинутое настольное приложение для глубокого обучения, обеспечивающее локальное высокоточное удаление фона с использованием моделей сегментации BiRefNet.",
|
||||
closeCredits: "Закрыть",
|
||||
dragDropText: "Перетащите изображение сюда",
|
||||
dragDropSub: "Поддерживает форматы PNG, JPG, JPEG или WebP",
|
||||
orText: "ИЛИ",
|
||||
browseFiles: "Обзор локальных файлов",
|
||||
autoTrim: "Автообрезка"
|
||||
},
|
||||
ar: {
|
||||
title: "Dekupai - Trunçgil AI إزالة الخلفية",
|
||||
sourceFile: "الملف المصدر",
|
||||
noImage: "لم يتم تحميل أي صورة",
|
||||
fileName: "الاسم",
|
||||
fileResolution: "الدقة",
|
||||
fileSize: "الحجم",
|
||||
newImage: "تحميل صورة جديدة",
|
||||
bgRemoval: "إزالة الخلفية",
|
||||
extractSubject: "استخراج العنصر",
|
||||
initAI: "تهيئة نموذج الذكاء الاصطناعي...",
|
||||
loadingWeights: "التشغيل الأول يقوم بتحميل النموذج (~200 ميجابايت)",
|
||||
bgSettings: "إعدادات الخلفية",
|
||||
transparent: "شفاف",
|
||||
solidColor: "لون موحد",
|
||||
gradient: "تدرج لوني",
|
||||
selectSolid: "اختر لوناً موحداً",
|
||||
selectGrad: "اختر تدرجاً لونيًا فاخرًا",
|
||||
exportStudio: "استوديو التصدير",
|
||||
format: "الصيغة",
|
||||
quality: "الجودة",
|
||||
exportMasterpiece: "تصدير العمل الفني",
|
||||
toastTitleInfo: "معلومات",
|
||||
toastTitleSuccess: "نجاح",
|
||||
toastTitleError: "خطأ",
|
||||
toastImageChanged: "تم تغيير الصورة",
|
||||
toastImageChangedMsg: "تم مسح مساحة العمل. جاهز لصورة جديدة.",
|
||||
toastTrimComplete: "اكتمل القص التلقائي",
|
||||
toastTrimCompleteMsg: "تم ضبط حدود القص بأمان حول العنصر.",
|
||||
toastTrimFailed: "تحذير القص",
|
||||
toastTrimFailedMsg: "تعذر تحليل الحدود الشفافة.",
|
||||
toastSetupSuccess: "نجاح التهيئة",
|
||||
toastSetupSuccessMsg: "تم تكوين بيئة بايثون بنجاح.",
|
||||
toastSetupError: "خطأ في التهيئة",
|
||||
toastSetupErrorMsg: "فشل تكوين بيئة بايثون.",
|
||||
initWizardTitle: "تهيئة محرك الذكاء الاصطناعي",
|
||||
initWizardSubtitle: "يتطلب Dekupai بيئة بايثون محلية لتشغيل نماذج الشبكة العصبية BiRefNet.",
|
||||
configureEngine: "تكوين المحرك (تلقائي)",
|
||||
stepVenv: "إنشاء بيئة افتراضية معزولة (.venv)",
|
||||
stepPip: "ترقية مثبت حزم بايثون (pip)",
|
||||
stepDeps: "تثبيت محرك الذكاء الاصطناعي PyTorch (نسخة المعالج CPU)",
|
||||
stepLibs: "تثبيت مكتبة HuggingFace Transformers و PIL",
|
||||
creditsVision: "تم التطوير تحت رؤية",
|
||||
creditsExpertise: "بواسطة الخبرة البرمجية والهندسية لـ",
|
||||
creditsDesc: "Dekupai هو مساحة عمل متقدمة للتعلم العميق توفر إزالة محلية ودقيقة للغاية للخلفيات باستخدام نماذج BiRefNet.",
|
||||
closeCredits: "إغلاق",
|
||||
dragDropText: "اسحب وأسقط الصورة هنا",
|
||||
dragDropSub: "يدعم صيغ PNG أو JPG أو JPEG أو WebP",
|
||||
orText: "أو",
|
||||
browseFiles: "تصفح الملفات المحلية",
|
||||
autoTrim: "قص تلقائي"
|
||||
}
|
||||
};
|
||||
|
||||
let currentLang = 'en'; // default English base language
|
||||
|
||||
function changeLanguage(lang) {
|
||||
currentLang = lang;
|
||||
localStorage.setItem('dekupai_lang', lang);
|
||||
|
||||
if (elements.langSelect) {
|
||||
elements.langSelect.value = lang;
|
||||
}
|
||||
|
||||
// Update HTML elements marked with data-i18n
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
if (TRANSLATIONS[lang] && TRANSLATIONS[lang][key]) {
|
||||
// If element contains an inner span for icon text
|
||||
const span = el.querySelector('span');
|
||||
if (span) {
|
||||
span.innerText = TRANSLATIONS[lang][key];
|
||||
} else if (el.tagName === 'OPTION') {
|
||||
el.innerText = TRANSLATIONS[lang][key];
|
||||
} else {
|
||||
// Preserving SVG structure inside tags
|
||||
const svg = el.querySelector('svg');
|
||||
if (svg) {
|
||||
// If it has SVG, clear other text and append text node safely
|
||||
const textNodes = Array.from(el.childNodes).filter(node => node.nodeType === Node.TEXT_NODE);
|
||||
if (textNodes.length > 0) {
|
||||
textNodes[textNodes.length - 1].nodeValue = " " + TRANSLATIONS[lang][key];
|
||||
} else {
|
||||
el.appendChild(document.createTextNode(" " + TRANSLATIONS[lang][key]));
|
||||
}
|
||||
} else {
|
||||
el.innerText = TRANSLATIONS[lang][key];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle RTL for Arabic
|
||||
if (lang === 'ar') {
|
||||
document.body.dir = 'rtl';
|
||||
} else {
|
||||
document.body.dir = 'ltr';
|
||||
}
|
||||
|
||||
// Update original title
|
||||
document.title = TRANSLATIONS[lang].title;
|
||||
}
|
||||
|
||||
function getTrans(key) {
|
||||
return (TRANSLATIONS[currentLang] && TRANSLATIONS[currentLang][key]) || TRANSLATIONS['en'][key] || key;
|
||||
}
|
||||
|
||||
function initLanguage() {
|
||||
const savedLang = localStorage.getItem('dekupai_lang') || 'en';
|
||||
|
||||
if (elements.langSelect) {
|
||||
elements.langSelect.value = savedLang;
|
||||
elements.langSelect.addEventListener('change', (e) => {
|
||||
changeLanguage(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
changeLanguage(savedLang);
|
||||
}
|
||||
|
||||
// --- CONTROL TOWER CONTROLLER ---
|
||||
function setupControlTowerListeners() {
|
||||
// Remove Background click
|
||||
|
||||
Reference in New Issue
Block a user