#!/usr/bin/env node /** * Merges missing i18n keys (admin fallback, brainstorm quota, landing page) * into locale files. Contextual translations — not word-for-word. */ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const localesDir = path.join(__dirname, '../locales') function deepMerge(target, source) { for (const key of Object.keys(source)) { const sv = source[key] if (sv && typeof sv === 'object' && !Array.isArray(sv)) { if (!target[key] || typeof target[key] !== 'object') target[key] = {} deepMerge(target[key], sv) } else { target[key] = sv } } return target } function flatten(obj, prefix = '') { const result = {} for (const [k, v] of Object.entries(obj)) { const key = prefix ? `${prefix}.${k}` : k if (v && typeof v === 'object' && !Array.isArray(v)) Object.assign(result, flatten(v, key)) else result[key] = v } return result } const adminAiFallback = { de: { fallbackSectionTitle: 'Ausweich-Anbieter (optional)', fallbackSectionDescription: 'Wird bei Anbieterfehlern automatisch genutzt (429, 5xx). Ein erneuter Versuch innerhalb von 1,5 s.', fallbackProvider: 'Ausweich-Anbieter', fallbackModel: 'Ausweich-Modell', fallbackNone: 'Keiner (deaktiviert)', fallbackModelPlaceholder: 'z. B. gpt-4o-mini', }, es: { fallbackSectionTitle: 'Proveedor de respaldo (opcional)', fallbackSectionDescription: 'Se usa automáticamente ante errores del proveedor (429, 5xx). Un reintento en 1,5 s.', fallbackProvider: 'Proveedor de respaldo', fallbackModel: 'Modelo de respaldo', fallbackNone: 'Ninguno (desactivado)', fallbackModelPlaceholder: 'p. ej. gpt-4o-mini', }, it: { fallbackSectionTitle: 'Provider di riserva (opzionale)', fallbackSectionDescription: 'Usato automaticamente in caso di errori del provider (429, 5xx). Un solo nuovo tentativo entro 1,5 s.', fallbackProvider: 'Provider di riserva', fallbackModel: 'Modello di riserva', fallbackNone: 'Nessuno (disattivato)', fallbackModelPlaceholder: 'es. gpt-4o-mini', }, pt: { fallbackSectionTitle: 'Provedor de contingência (opcional)', fallbackSectionDescription: 'Usado automaticamente em erros do provedor (429, 5xx). Uma nova tentativa em 1,5 s.', fallbackProvider: 'Provedor de contingência', fallbackModel: 'Modelo de contingência', fallbackNone: 'Nenhum (desativado)', fallbackModelPlaceholder: 'ex.: gpt-4o-mini', }, nl: { fallbackSectionTitle: 'Fallback-provider (optioneel)', fallbackSectionDescription: 'Wordt automatisch gebruikt bij providerfouten (429, 5xx). Eén nieuwe poging binnen 1,5 s.', fallbackProvider: 'Fallback-provider', fallbackModel: 'Fallback-model', fallbackNone: 'Geen (uitgeschakeld)', fallbackModelPlaceholder: 'bijv. gpt-4o-mini', }, pl: { fallbackSectionTitle: 'Zapasowy dostawca (opcjonalnie)', fallbackSectionDescription: 'Używany automatycznie przy błędach dostawcy (429, 5xx). Jedna ponowna próba w 1,5 s.', fallbackProvider: 'Zapasowy dostawca', fallbackModel: 'Zapasowy model', fallbackNone: 'Brak (wyłączone)', fallbackModelPlaceholder: 'np. gpt-4o-mini', }, ru: { fallbackSectionTitle: 'Резервный провайдер (необязательно)', fallbackSectionDescription: 'Используется автоматически при ошибках провайдера (429, 5xx). Одна повторная попытка за 1,5 с.', fallbackProvider: 'Резервный провайдер', fallbackModel: 'Резервная модель', fallbackNone: 'Нет (отключено)', fallbackModelPlaceholder: 'напр. gpt-4o-mini', }, ar: { fallbackSectionTitle: 'مزود احتياطي (اختياري)', fallbackSectionDescription: 'يُستخدم تلقائياً عند أخطاء المزود (429، 5xx). محاولة واحدة خلال 1,5 ثانية.', fallbackProvider: 'مزود احتياطي', fallbackModel: 'نموذج احتياطي', fallbackNone: 'لا شيء (معطّل)', fallbackModelPlaceholder: 'مثال: gpt-4o-mini', }, fa: { fallbackSectionTitle: 'ارائه‌دهنده پشتیبان (اختیاری)', fallbackSectionDescription: 'در صورت خطای ارائه‌دهنده (429، 5xx) به‌صورت خودکار استفاده می‌شود. یک تلاش مجدد در ۱,۵ ثانیه.', fallbackProvider: 'ارائه‌دهنده پشتیبان', fallbackModel: 'مدل پشتیبان', fallbackNone: 'هیچ (غیرفعال)', fallbackModelPlaceholder: 'مثلاً gpt-4o-mini', }, hi: { fallbackSectionTitle: 'फ़ॉलबैक प्रदाता (वैकल्पिक)', fallbackSectionDescription: 'प्रदाता त्रुटियों (429, 5xx) पर स्वतः उपयोग। 1.5 सेकंड में एक पुनः प्रयास।', fallbackProvider: 'फ़ॉलबैक प्रदाता', fallbackModel: 'फ़ॉलबैक मॉडल', fallbackNone: 'कोई नहीं (अक्षम)', fallbackModelPlaceholder: 'उदा. gpt-4o-mini', }, ja: { fallbackSectionTitle: 'フォールバックプロバイダー(任意)', fallbackSectionDescription: 'プロバイダーエラー時(429、5xx)に自動使用。1.5秒以内に1回再試行。', fallbackProvider: 'フォールバックプロバイダー', fallbackModel: 'フォールバックモデル', fallbackNone: 'なし(無効)', fallbackModelPlaceholder: '例: gpt-4o-mini', }, ko: { fallbackSectionTitle: '대체 제공업체(선택)', fallbackSectionDescription: '제공업체 오류(429, 5xx) 시 자동 사용. 1.5초 이내 1회 재시도.', fallbackProvider: '대체 제공업체', fallbackModel: '대체 모델', fallbackNone: '없음(비활성화)', fallbackModelPlaceholder: '예: gpt-4o-mini', }, zh: { fallbackSectionTitle: '备用提供商(可选)', fallbackSectionDescription: '提供商出错时(429、5xx)自动启用。1.5 秒内重试一次。', fallbackProvider: '备用提供商', fallbackModel: '备用模型', fallbackNone: '无(已禁用)', fallbackModelPlaceholder: '例如 gpt-4o-mini', }, } const brainstormQuota = { de: { quotaGuest: 'Der Gastgeber der Sitzung hat sein KI-Kontingent aufgebraucht. Bitte ihn, seinen Tarif zu erweitern.', quotaHost: 'Sie haben Ihr KI-Kontingent für dieses Brainstorming erreicht. Wechseln Sie den Tarif, um fortzufahren.', }, es: { quotaGuest: 'El anfitrión de la sesión ha alcanzado su límite de IA. Pídele que mejore su plan.', quotaHost: 'Has alcanzado tu límite de IA para este brainstorm. Mejora tu plan para continuar.', }, it: { quotaGuest: "L'host della sessione ha raggiunto il limite IA. Chiedigli di aggiornare il piano.", quotaHost: 'Hai raggiunto il limite IA per questo brainstorm. Passa a un piano superiore per continuare.', }, pt: { quotaGuest: 'O anfitrião da sessão atingiu o limite de IA. Peça-lhe para atualizar o plano.', quotaHost: 'Atingiu o limite de IA deste brainstorm. Atualize o plano para continuar.', }, nl: { quotaGuest: 'De sessiehost heeft zijn AI-limiet bereikt. Vraag om een upgrade van het abonnement.', quotaHost: 'Je hebt je AI-limiet voor deze brainstorm bereikt. Upgrade je abonnement om door te gaan.', }, pl: { quotaGuest: 'Gospodarz sesji wyczerpał limit AI. Poproś go o ulepszenie planu.', quotaHost: 'Osiągnąłeś limit AI dla tego brainstormu. Ulepsz plan, aby kontynuować.', }, ru: { quotaGuest: 'Организатор сессии исчерпал лимит ИИ. Попросите его обновить тариф.', quotaHost: 'Вы исчерпали лимит ИИ для этого мозгового штурма. Обновите тариф, чтобы продолжить.', }, ar: { quotaGuest: 'استنفد مضيف الجلسة حدّ الذكاء الاصطناعي. اطلب منه ترقية خطته.', quotaHost: 'لقد وصلت إلى حدّ الذكاء الاصطناعي لهذه الجلسة. رقِّ خطتك للمتابعة.', }, fa: { quotaGuest: 'میزبان جلسه به سقف هوش مصنوعی رسیده. از او بخواهید طرحش را ارتقا دهد.', quotaHost: 'به سقف هوش مصنوعی این طوفان فکری رسیدید. برای ادامه، طرح خود را ارتقا دهید.', }, hi: { quotaGuest: 'सत्र के होस्ट की AI सीमा समाप्त हो गई है। उनसे अपना प्लान अपग्रेड करने को कहें।', quotaHost: 'इस ब्रेनस्टॉर्म के लिए आपकी AI सीमा समाप्त हो गई है। जारी रखने के लिए प्लान अपग्रेड करें।', }, ja: { quotaGuest: 'セッションのホストがAI利用上限に達しました。プランのアップグレードを依頼してください。', quotaHost: 'このブレインストームのAI上限に達しました。続けるにはプランをアップグレードしてください。', }, ko: { quotaGuest: '세션 호스트의 AI 한도에 도달했습니다. 플랜 업그레이드를 요청하세요.', quotaHost: '이 브레인스토밍의 AI 한도에 도달했습니다. 계속하려면 플랜을 업그레이드하세요.', }, zh: { quotaGuest: '会话主持人已达到 AI 额度上限。请让对方升级套餐。', quotaHost: '您已达到此头脑风暴的 AI 额度上限。升级套餐以继续。', }, } // Landing blocks — load from generated JSON (keeps this script maintainable) const landingPatchesPath = path.join(__dirname, 'i18n-landing-patches.json') if (!fs.existsSync(landingPatchesPath)) { console.error('Missing i18n-landing-patches.json — run generate-landing-patches first') process.exit(1) } const landingPatches = JSON.parse(fs.readFileSync(landingPatchesPath, 'utf8')) const TARGET_LANGS = [ 'ar', 'de', 'es', 'fa', 'hi', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'zh', ] for (const lang of TARGET_LANGS) { const filePath = path.join(localesDir, `${lang}.json`) const data = JSON.parse(fs.readFileSync(filePath, 'utf8')) if (adminAiFallback[lang]) { if (!data.admin) data.admin = {} if (!data.admin.ai) data.admin.ai = {} Object.assign(data.admin.ai, adminAiFallback[lang]) } if (brainstormQuota[lang]) { if (!data.brainstorm) data.brainstorm = {} Object.assign(data.brainstorm, brainstormQuota[lang]) } if (landingPatches[lang]) { if (!data.landing) data.landing = {} deepMerge(data.landing, landingPatches[lang]) } fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8') console.log(`✓ ${lang}.json updated`) } // Verify const en = flatten(JSON.parse(fs.readFileSync(path.join(localesDir, 'en.json'), 'utf8'))) const enKeys = Object.keys(en) let ok = true for (const lang of TARGET_LANGS) { const loc = flatten(JSON.parse(fs.readFileSync(path.join(localesDir, `${lang}.json`), 'utf8'))) const missing = enKeys.filter((k) => !(k in loc)) if (missing.length) { console.error(`✗ ${lang}: still ${missing.length} missing keys`) ok = false } } console.log(ok ? '\nAll locales complete.' : '\nSome keys still missing.')