feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
@@ -0,0 +1,593 @@
|
||||
/**
|
||||
* Met à jour agents.form.slideThemes + richTextEditor (aiReformulateFailed, translateTargets)
|
||||
* dans memento-note/locales/*.json (seul en.json est laissé hors de ce script).
|
||||
*/
|
||||
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')
|
||||
|
||||
const slideKeys = [
|
||||
'modern_wellness',
|
||||
'business_authority',
|
||||
'nature_outdoors',
|
||||
'vintage_academic',
|
||||
'soft_creative',
|
||||
'bohemian',
|
||||
'vibrant_tech',
|
||||
'craft_artisan',
|
||||
'tech_night',
|
||||
'education_charts',
|
||||
'forest_eco',
|
||||
'elegant_fashion',
|
||||
'art_food',
|
||||
'luxury_mystery',
|
||||
'pure_tech_blue',
|
||||
'coastal_coral',
|
||||
'vibrant_orange_mint',
|
||||
'platinum_white_gold',
|
||||
]
|
||||
|
||||
const TT_KEYS = [
|
||||
'Francais',
|
||||
'English',
|
||||
'Espanol',
|
||||
'Deutsch',
|
||||
'Persan',
|
||||
'Portugais',
|
||||
'Italiano',
|
||||
'Chinois',
|
||||
'Japonais',
|
||||
]
|
||||
|
||||
/** @returns {Record<string,string>} slideThemes avec toutes les clés présentes */
|
||||
function st(...values) {
|
||||
const out = {}
|
||||
slideKeys.forEach((k, i) => {
|
||||
out[k] = values[i]
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
/** @returns {Record<string,string>} translateTargets avec clés fixes */
|
||||
function tt(vf, ven, ves, vd, vp, vp2, vit, vch, vja) {
|
||||
return {
|
||||
Francais: vf,
|
||||
English: ven,
|
||||
Espanol: ves,
|
||||
Deutsch: vd,
|
||||
Persan: vp,
|
||||
Portugais: vp2,
|
||||
Italiano: vit,
|
||||
Chinois: vch,
|
||||
Japonais: vja,
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Record<string, { slideThemes: Record<string,string>, translateTargets: Record<string,string>, aiReformulateFailed: string }>} */
|
||||
const LOCALE = {
|
||||
de: {
|
||||
slideThemes: st(
|
||||
'Modern & Wohlbefinden',
|
||||
'Business & Autorität',
|
||||
'Natur & Outdoor',
|
||||
'Vintage & Akademisch',
|
||||
'Sanft & Kreativ',
|
||||
'Bohemian',
|
||||
'Lebhaft & Tech',
|
||||
'Handwerk & Artisan',
|
||||
'Tech & Nacht (dunkel)',
|
||||
'Bildung & Diagramme',
|
||||
'Wald & Öko',
|
||||
'Elegant & Mode',
|
||||
'Kunst & Kulinarik',
|
||||
'Luxus & Mystik',
|
||||
'Reines Tech-Blau',
|
||||
'Küsten-Koralle',
|
||||
'Lebhaft Orange & Minze',
|
||||
'Platin Weißgold',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'Französisch',
|
||||
'Englisch',
|
||||
'Spanisch',
|
||||
'Deutsch',
|
||||
'Persisch',
|
||||
'Portugiesisch',
|
||||
'Italienisch',
|
||||
'Chinesisch',
|
||||
'Japanisch',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'Der KI-Assistent konnte diesen Text nicht verarbeiten. Bitte erneut versuchen.',
|
||||
},
|
||||
es: {
|
||||
slideThemes: st(
|
||||
'Moderno y bienestar',
|
||||
'Negocios y autoridad',
|
||||
'Naturaleza y aire libre',
|
||||
'Vintage y académico',
|
||||
'Suave y creativo',
|
||||
'Bohemio',
|
||||
'Vibrante y tech',
|
||||
'Artesanía',
|
||||
'Tech y noche (oscuro)',
|
||||
'Educación y gráficos',
|
||||
'Bosque y eco',
|
||||
'Elegancia y moda',
|
||||
'Arte y gastronomía',
|
||||
'Lujo y misterio',
|
||||
'Tech azul puro',
|
||||
'Coral costero',
|
||||
'Naranja vibrante y menta',
|
||||
'Platino blanco y oro',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'Francés',
|
||||
'Inglés',
|
||||
'Español',
|
||||
'Alemán',
|
||||
'Persa',
|
||||
'Portugués',
|
||||
'Italiano',
|
||||
'Chino',
|
||||
'Japonés',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'La IA no pudo procesar este texto. Inténtalo de nuevo.',
|
||||
},
|
||||
fr: {
|
||||
slideThemes: st(
|
||||
'Moderne & bien-être',
|
||||
'Affaires & autorité',
|
||||
'Nature & plein air',
|
||||
'Vintage & académique',
|
||||
'Doux & créatif',
|
||||
'Bohème',
|
||||
'Éclat & tech',
|
||||
'Artisan & fait main',
|
||||
'Tech & nuit (sombre)',
|
||||
'Éducation & graphiques',
|
||||
'Forêt & éco',
|
||||
'Élégance & mode',
|
||||
'Art & gastronomie',
|
||||
'Luxe & mystère',
|
||||
'Bleu tech pur',
|
||||
'Corail côtier',
|
||||
'Orange vif & menthe',
|
||||
'Platine blanc & or',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'Français',
|
||||
'Anglais',
|
||||
'Espagnol',
|
||||
'Allemand',
|
||||
'Persan',
|
||||
'Portugais',
|
||||
'Italien',
|
||||
'Chinois',
|
||||
'Japonais',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
"L'assistant IA n'a pas pu traiter ce texte. Réessayez.",
|
||||
},
|
||||
it: {
|
||||
slideThemes: st(
|
||||
'Moderno e benessere',
|
||||
'Business e autorevolezza',
|
||||
'Natura e outdoor',
|
||||
'Vintage e accademico',
|
||||
'Morbido e creativo',
|
||||
'Bohemien',
|
||||
'Vivace e tech',
|
||||
'Artigianato',
|
||||
'Tech e notte (scuro)',
|
||||
'Formazione e grafici',
|
||||
'Foresta ed eco',
|
||||
'Eleganza e moda',
|
||||
'Arte e cucina',
|
||||
'Lusso e mistero',
|
||||
'Tech blu puro',
|
||||
'Corallo costiero',
|
||||
'Arancio vivace e menta',
|
||||
'Platino bianco e oro',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'Francese',
|
||||
'Inglese',
|
||||
'Spagnolo',
|
||||
'Tedesco',
|
||||
'Persiano',
|
||||
'Portoghese',
|
||||
'Italiano',
|
||||
'Cinese',
|
||||
'Giapponese',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
"L'assistente IA non ha potuto elaborare questo testo. Riprova.",
|
||||
},
|
||||
pt: {
|
||||
slideThemes: st(
|
||||
'Moderno e bem-estar',
|
||||
'Negócios e autoridade',
|
||||
'Natureza e ar livre',
|
||||
'Vintage e académico',
|
||||
'Suave e criativo',
|
||||
'Boémio',
|
||||
'Vibrante e tech',
|
||||
'Artesanato',
|
||||
'Tech e noite (escuro)',
|
||||
'Educação e gráficos',
|
||||
'Floresta e eco',
|
||||
'Elegância e moda',
|
||||
'Arte e gastronomia',
|
||||
'Luxo e mistério',
|
||||
'Tech azul puro',
|
||||
'Coral costeiro',
|
||||
'Laranja vibrante e hortelã',
|
||||
'Platina branca e ouro',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'Francês',
|
||||
'Inglês',
|
||||
'Espanhol',
|
||||
'Alemão',
|
||||
'Persa',
|
||||
'Português',
|
||||
'Italiano',
|
||||
'Chinês',
|
||||
'Japonês',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'A IA não conseguiu processar este texto. Tenta novamente.',
|
||||
},
|
||||
nl: {
|
||||
slideThemes: st(
|
||||
'Modern & welzijn',
|
||||
'Zaken & autoriteit',
|
||||
'Natuur & buiten',
|
||||
'Vintage & academisch',
|
||||
'Zacht & creatief',
|
||||
'Bohemian',
|
||||
'Levendig & tech',
|
||||
'Ambacht',
|
||||
'Tech & nacht (donker)',
|
||||
'Onderwijs & grafieken',
|
||||
'Bos & eco',
|
||||
'Elegant & mode',
|
||||
'Kunst & gastronomie',
|
||||
'Luxe & mysterie',
|
||||
'Strak techblauw',
|
||||
'Kustkoraal',
|
||||
'Levend oranje & munt',
|
||||
'Platina witgoud',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'Frans',
|
||||
'Engels',
|
||||
'Spaans',
|
||||
'Duits',
|
||||
'Perzisch',
|
||||
'Portugees',
|
||||
'Italiaans',
|
||||
'Chinees',
|
||||
'Japans',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'De AI-assistent kon deze tekst niet verwerken. Probeer het opnieuw.',
|
||||
},
|
||||
pl: {
|
||||
slideThemes: st(
|
||||
'Nowoczesny wellness',
|
||||
'Biznes i autorytet',
|
||||
'Natura i outdoor',
|
||||
'Vintage i akademicki',
|
||||
'Miękki i kreatywny',
|
||||
'Boho',
|
||||
'Żywy tech',
|
||||
'Rzemiosło',
|
||||
'Tech i noc (ciemny)',
|
||||
'Edukacja i wykresy',
|
||||
'Las i eco',
|
||||
'Elegancja i moda',
|
||||
'Sztuka i kuchnia',
|
||||
'Luksus i tajemnica',
|
||||
'Czysty tech niebieski',
|
||||
'Koral przybrzeżny',
|
||||
'Żywa pomarańcz i mięta',
|
||||
'Platyna i białe złoto',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'Francuski',
|
||||
'Angielski',
|
||||
'Hiszpański',
|
||||
'Niemiecki',
|
||||
'Perski',
|
||||
'Portugalski',
|
||||
'Włoski',
|
||||
'Chiński',
|
||||
'Japoński',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'Asystent IA nie mógł przetworzyć tego tekstu. Spróbuj ponownie.',
|
||||
},
|
||||
ru: {
|
||||
slideThemes: st(
|
||||
'Современность и велнес',
|
||||
'Бизнес и авторитет',
|
||||
'Природа и активный отдых',
|
||||
'Винтаж и академизм',
|
||||
'Мягкость и креатив',
|
||||
'Богемный',
|
||||
'Яркость и техно',
|
||||
'Ремесло',
|
||||
'Техно и ночь (тёмный)',
|
||||
'Образование и графики',
|
||||
'Лес и эко',
|
||||
'Элегантность и мода',
|
||||
'Искусство и гастрономия',
|
||||
'Роскошь и тайна',
|
||||
'Чистый техно-синий',
|
||||
'Прибрежный коралл',
|
||||
'Яркий оранжевый и мята',
|
||||
'Платина и белое золото',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'Французский',
|
||||
'Английский',
|
||||
'Испанский',
|
||||
'Немецкий',
|
||||
'Персидский',
|
||||
'Португальский',
|
||||
'Итальянский',
|
||||
'Китайский',
|
||||
'Японский',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'ИИ-помощник не смог обработать этот текст. Попробуйте снова.',
|
||||
},
|
||||
zh: {
|
||||
slideThemes: st(
|
||||
'现代与活力',
|
||||
'商务权威',
|
||||
'自然户外',
|
||||
'复古学院',
|
||||
'柔和创意',
|
||||
'波希米亚',
|
||||
'活力科技',
|
||||
'手工艺匠心',
|
||||
'科技暗夜(深色)',
|
||||
'教育与图表',
|
||||
'森林生态',
|
||||
'优雅时尚',
|
||||
'艺术与美食',
|
||||
'奢华神秘',
|
||||
'纯正科技蓝',
|
||||
'海岸珊瑚',
|
||||
'活力橙与薄荷',
|
||||
'铂金与白金色',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'法语',
|
||||
'英语',
|
||||
'西班牙语',
|
||||
'德语',
|
||||
'波斯语',
|
||||
'葡萄牙语',
|
||||
'意大利语',
|
||||
'中文',
|
||||
'日语',
|
||||
),
|
||||
aiReformulateFailed: 'AI 助手无法处理此文本,请重试。',
|
||||
},
|
||||
ja: {
|
||||
slideThemes: st(
|
||||
'モダン&ウェルネス',
|
||||
'ビジネス&オーソリティ',
|
||||
'ネイチャー&アウトドア',
|
||||
'ヴィンテージ&アカデミック',
|
||||
'ソフト&クリエイティブ',
|
||||
'ボヘミアン',
|
||||
'ビブラント&テック',
|
||||
'クラフト&職人',
|
||||
'テック&ナイト(ダーク)',
|
||||
'教育&チャート',
|
||||
'フォレスト&エコ',
|
||||
'エレガント&ファッション',
|
||||
'アート&フード',
|
||||
'ラグジュアリー&ミステリー',
|
||||
'ピュアテックブルー',
|
||||
'コースタルコーラル',
|
||||
'ビビッドオレンジ&ミント',
|
||||
'プラチナホワイトゴールド',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'フランス語',
|
||||
'英語',
|
||||
'スペイン語',
|
||||
'ドイツ語',
|
||||
'ペルシア語',
|
||||
'ポルトガル語',
|
||||
'イタリア語',
|
||||
'中国語',
|
||||
'日本語',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'AI アシスタントがこのテキストを処理できませんでした。もう一度お試しください。',
|
||||
},
|
||||
ko: {
|
||||
slideThemes: st(
|
||||
'모던 앤 웰니스',
|
||||
'비즈니스 앤 오서리티',
|
||||
'네이처 앤 아웃도어',
|
||||
'빈티지 앤 아카데믹',
|
||||
'소프트 앤 크리에이티브',
|
||||
'보헤미안',
|
||||
'비브런트 앤 테크',
|
||||
'크래프트 앤 아티즌',
|
||||
'테크 앤 나イト(다크)',
|
||||
'교육 앤 차트',
|
||||
'포레스트 앤 에코',
|
||||
'엘리건트 앤 패션',
|
||||
'아트 앤 푸드',
|
||||
'럭셔리 앤 미스터리',
|
||||
'퓨어 테크 블루',
|
||||
'코스트럴 코럴',
|
||||
'비브런트 오렌지 앤 민트',
|
||||
'플래티넘 화이트 골드',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'프랑스어',
|
||||
'영어',
|
||||
'스페인어',
|
||||
'독일어',
|
||||
'페르시아어',
|
||||
'포르투갈어',
|
||||
'이탈리아어',
|
||||
'중국어',
|
||||
'일본어',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'AI 어시스턴트가 이 텍스트를 처리하지 못했습니다. 다시 시도해 주세요.',
|
||||
},
|
||||
ar: {
|
||||
slideThemes: st(
|
||||
'عصري وصحة وعافية',
|
||||
'عمل وسلطة مهنية',
|
||||
'طبيعة وهواء طلق',
|
||||
'عتيق وأكاديمي',
|
||||
'ناعم وإبداعي',
|
||||
'بوهيمي',
|
||||
'زاهٍ وتقني',
|
||||
'حرفة وصناعة يدوية',
|
||||
'تقني وليلة (داكن)',
|
||||
'تعليم ورسوم بيانية',
|
||||
'غابة وبيئة',
|
||||
'أنيق وأزياء',
|
||||
'فن وطعام',
|
||||
'فخامة وغموض',
|
||||
'أزرق تقني نقي',
|
||||
'مرجان ساحلي',
|
||||
'برتقالي زاهٍ ونعناع',
|
||||
'بلاتيني ذهب أبيض',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'الفرنسية',
|
||||
'الإنجليزية',
|
||||
'الإسبانية',
|
||||
'الألمانية',
|
||||
'الفارسية',
|
||||
'البرتغالية',
|
||||
'الإيطالية',
|
||||
'الصينية',
|
||||
'اليابانية',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'تعذّر على المساعد الذكي معالجة هذا النص. حاول مجدداً.',
|
||||
},
|
||||
fa: {
|
||||
slideThemes: st(
|
||||
'مدرن و تندرستی',
|
||||
'کسبوکار و اعتبار',
|
||||
'طبیعت و فضای باز',
|
||||
'کلاسیک دانشگاهی',
|
||||
'نرم و خلاقانه',
|
||||
'بوهمیایی',
|
||||
'پرجنبوجوش و تکنولوژی',
|
||||
'صنعت دستی و هنرمند',
|
||||
'فناوری و شب (تیره)',
|
||||
'آموزش و نمودار',
|
||||
'جنگل و اکولوژی',
|
||||
'شیک و مد',
|
||||
'هنر و غذا',
|
||||
'لوکس و مرموز',
|
||||
'آبی تکنولوژی خالص',
|
||||
'مرجان ساحلی',
|
||||
'نارنجی شاد و نعناع',
|
||||
'پلاتین و سفید طلایی',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'فرانسوی',
|
||||
'انگلیسی',
|
||||
'اسپانیایی',
|
||||
'آلمانی',
|
||||
'فارسی',
|
||||
'پرتغالی',
|
||||
'ایتالیایی',
|
||||
'چینی',
|
||||
'ژاپنی',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'دستیار هوش مصنوعی نتوانست این متن را پردازش کند. دوباره تلاش کنید.',
|
||||
},
|
||||
hi: {
|
||||
slideThemes: st(
|
||||
'आधुनिक और वेलनेस',
|
||||
'व्यवसाय और प्राधिकरण',
|
||||
'प्रकृति और बाहर',
|
||||
'विंटेज और शैक्षणिक',
|
||||
'मुलायम और रचनात्मक',
|
||||
'बोहो',
|
||||
'जीवंत और टेक',
|
||||
'शिल्प और कारीगर',
|
||||
'टेक और रात (डार्क)',
|
||||
'शिक्षा और चार्ट',
|
||||
'वन और इको',
|
||||
'सुरुचिपूर्ण और फैशन',
|
||||
'कला और भोजन',
|
||||
'लक्ज़री और रहस्य',
|
||||
'शुद्ध टेक नीला',
|
||||
'तटीय कोरल',
|
||||
'जीवंत संतरा और पुदीना',
|
||||
'प्लैटिनम सफ़ेद सोना',
|
||||
),
|
||||
translateTargets: tt(
|
||||
'फ़्रेंच',
|
||||
'अंग्रेज़ी',
|
||||
'स्पेनिश',
|
||||
'जर्मन',
|
||||
'फ़ारसी',
|
||||
'पुर्तगाली',
|
||||
'इतालवी',
|
||||
'चीनी',
|
||||
'जापानी',
|
||||
),
|
||||
aiReformulateFailed:
|
||||
'AI सहायक इस पाठ को प्रोसेस नहीं कर सका। दोबारा कोशिश करें।',
|
||||
},
|
||||
}
|
||||
|
||||
function applyLocale(code, bundle) {
|
||||
const file = path.join(localesDir, `${code}.json`)
|
||||
if (!fs.existsSync(file)) {
|
||||
console.warn('Fichier manquant:', file)
|
||||
return
|
||||
}
|
||||
const raw = fs.readFileSync(file, 'utf8')
|
||||
const j = JSON.parse(raw)
|
||||
if (!j.agents?.form) {
|
||||
console.warn(`${code}: agents.form absent, ignoré`)
|
||||
return
|
||||
}
|
||||
if (!j.agents.form.slideThemes) j.agents.form.slideThemes = {}
|
||||
slideKeys.forEach((k) => {
|
||||
j.agents.form.slideThemes[k] = bundle.slideThemes[k]
|
||||
})
|
||||
|
||||
if (!j.richTextEditor) j.richTextEditor = {}
|
||||
TT_KEYS.forEach((k) => {
|
||||
j.richTextEditor.translateTargets =
|
||||
j.richTextEditor.translateTargets ?? {}
|
||||
j.richTextEditor.translateTargets[k] = bundle.translateTargets[k]
|
||||
})
|
||||
j.richTextEditor.aiReformulateFailed = bundle.aiReformulateFailed
|
||||
|
||||
fs.writeFileSync(file, JSON.stringify(j, null, 2) + '\n', 'utf8')
|
||||
console.log('OK', code)
|
||||
}
|
||||
|
||||
for (const code of Object.keys(LOCALE)) {
|
||||
applyLocale(code, LOCALE[code])
|
||||
}
|
||||
35
memento-note/scripts/merge-missing-locale-keys.mjs
Normal file
35
memento-note/scripts/merge-missing-locale-keys.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Ajoute récursivement les clés présentes dans locales/en.json mais absentes
|
||||
* des autres fichiers (valeur = texte anglais, à retraduire si besoin).
|
||||
* Usage: node scripts/merge-missing-locale-keys.mjs
|
||||
*/
|
||||
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')
|
||||
const enPath = path.join(localesDir, 'en.json')
|
||||
|
||||
function mergeMissing(target, source) {
|
||||
for (const [k, v] of Object.entries(source)) {
|
||||
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
||||
if (!target[k] || typeof target[k] !== 'object' || Array.isArray(target[k])) {
|
||||
target[k] = {}
|
||||
}
|
||||
mergeMissing(target[k], v)
|
||||
} else {
|
||||
if (!(k in target)) target[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const en = JSON.parse(fs.readFileSync(enPath, 'utf8'))
|
||||
for (const name of fs.readdirSync(localesDir)) {
|
||||
if (!name.endsWith('.json') || name === 'en.json') continue
|
||||
const p = path.join(localesDir, name)
|
||||
const loc = JSON.parse(fs.readFileSync(p, 'utf8'))
|
||||
mergeMissing(loc, en)
|
||||
fs.writeFileSync(p, JSON.stringify(loc, null, 2) + '\n')
|
||||
}
|
||||
console.log('merge-missing-locale-keys: OK')
|
||||
Reference in New Issue
Block a user