Files
Momento/memento-note/scripts/apply-missing-i18n.mjs
Antigravity 8c7ca69640
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
fix: brainstorm infinite loop, ghost cursor, embedding ::vector cast, semantic search, billing stats, usage meter accordion
- 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
2026-05-16 18:50:34 +00:00

293 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.')