All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
- Fix useBrainstormSocket: stable guestId via useRef, remove setState in cleanup - Fix GhostCursor: direct DOM manipulation via refs, no useState re-renders - Fix all SQL embedding queries: add ::vector cast on text columns - Fix embedding truncation to 15000 chars (under 8192 token limit) - Fix NoteEmbedding INSERT: remove non-existent updatedAt column - Fix billing page: show all quota stats in grid instead of single metric - Fix usage meter: accordion expand/collapse, per-feature detail - Fix semantic search: rebuild 103 note embeddings, ::vector cast on vectorSearch - Fix brainstorm expand/manual-idea/create: ::vector cast on embedding SQL
293 lines
12 KiB
JavaScript
293 lines
12 KiB
JavaScript
#!/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.')
|