Live video on the AT Protocol
at natb/command-errors 254 lines 6.8 kB view raw
1#!/usr/bin/env node 2 3const fs = require("fs"); 4const path = require("path"); 5const { parse: parseFtl } = require("@fluent/syntax"); 6 7// Load language manifest 8const MANIFEST_PATH = path.join(__dirname, "..", "locales", "manifest.json"); 9const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf-8")); 10 11// Configuration 12const LOCALES_SOURCE_DIR = path.join(__dirname, "..", "locales"); 13const LOCALES_OUTPUT_DIR = path.join(__dirname, "..", "public", "locales"); 14 15/** 16 * Find all .ftl files in a directory (non-recursive, just top-level) 17 */ 18function findFtlFiles(dir) { 19 const files = []; 20 const entries = fs.readdirSync(dir, { withFileTypes: true }); 21 22 for (const entry of entries) { 23 if (entry.isFile() && entry.name.endsWith(".ftl")) { 24 files.push({ 25 path: path.join(dir, entry.name), 26 namespace: path.basename(entry.name, ".ftl"), 27 }); 28 } 29 } 30 31 return files; 32} 33 34/** 35 * Parse FTL content using @fluent/syntax and extract messages 36 */ 37function extractMessagesFromFtl(content) { 38 const messages = {}; 39 40 try { 41 const resource = parseFtl(content); 42 43 for (const entry of resource.body) { 44 // Only process Message entries (not Comments, Terms, etc.) 45 if (entry.type === "Message" && entry.id) { 46 const key = entry.id.name; 47 48 // For now, just serialize the pattern back to FTL format 49 // i18next-fluent will handle the actual parsing at runtime 50 if (entry.value) { 51 messages[key] = serializePattern(entry.value); 52 } 53 } 54 } 55 } catch (error) { 56 console.error("Error parsing FTL:", error.message); 57 throw error; 58 } 59 60 return messages; 61} 62 63/** 64 * Serialize a Fluent pattern back to FTL string format 65 * This is a simple serializer - @fluent/syntax has a full serializer 66 * but we just need the pattern text for JSON storage 67 */ 68function serializePattern(pattern) { 69 if (!pattern || !pattern.elements) { 70 return ""; 71 } 72 73 let result = ""; 74 75 for (const element of pattern.elements) { 76 if (element.type === "TextElement") { 77 result += element.value; 78 } else if (element.type === "Placeable") { 79 result += serializePlaceable(element); 80 } 81 } 82 83 return result; 84} 85 86/** 87 * Serialize a placeable (variables, select expressions, etc.) 88 */ 89function serializePlaceable(placeable) { 90 if (!placeable.expression) { 91 return "{}"; 92 } 93 94 const expr = placeable.expression; 95 96 if (expr.type === "VariableReference") { 97 return `{$${expr.id.name}}`; 98 } else if (expr.type === "SelectExpression") { 99 let result = `{${serializeInlineExpression(expr.selector)} ->`; 100 101 for (const variant of expr.variants) { 102 const key = 103 variant.key.type === "Identifier" 104 ? `[${variant.key.name}]` 105 : `[${variant.key.value}]`; 106 107 result += `\n ${variant.default ? "*" : ""}${key} ${serializePattern(variant.value)}`; 108 } 109 110 result += "\n }"; 111 return result; 112 } else if (expr.type === "FunctionReference") { 113 const args = expr.arguments.positional 114 .map((arg) => serializeInlineExpression(arg)) 115 .join(", "); 116 return `{${expr.id.name}(${args})}`; 117 } 118 119 return "{}"; 120} 121 122/** 123 * Serialize inline expressions (for function arguments, etc.) 124 */ 125function serializeInlineExpression(expr) { 126 if (expr.type === "VariableReference") { 127 return `$${expr.id.name}`; 128 } else if (expr.type === "NumberLiteral") { 129 return expr.value; 130 } else if (expr.type === "StringLiteral") { 131 return `"${expr.value}"`; 132 } 133 134 return ""; 135} 136 137/** 138 * Main compilation function 139 */ 140function compileTranslations() { 141 console.log("🌍 Compiling translation files..."); 142 143 if (!fs.existsSync(LOCALES_SOURCE_DIR)) { 144 console.error(`❌ Locales directory not found: ${LOCALES_SOURCE_DIR}`); 145 process.exit(1); 146 } 147 148 // Create output directory if it doesn't exist 149 if (!fs.existsSync(LOCALES_OUTPUT_DIR)) { 150 fs.mkdirSync(LOCALES_OUTPUT_DIR, { recursive: true }); 151 } 152 153 // Get supported locales from manifest 154 const manifestLocales = manifest.supportedLocales; 155 const locales = manifestLocales.filter((locale) => { 156 const localeDir = path.join(LOCALES_SOURCE_DIR, locale); 157 return fs.existsSync(localeDir) && fs.statSync(localeDir).isDirectory(); 158 }); 159 160 if (locales.length === 0) { 161 console.error(`❌ No locale directories found in ${LOCALES_SOURCE_DIR}`); 162 process.exit(1); 163 } 164 165 let totalFiles = 0; 166 167 // Process each locale 168 for (const locale of locales) { 169 const localeSourceDir = path.join(LOCALES_SOURCE_DIR, locale); 170 const localeOutputDir = path.join(LOCALES_OUTPUT_DIR, locale); 171 172 console.log(`📦 Processing locale: ${locale}`); 173 174 // Create locale output directory 175 if (!fs.existsSync(localeOutputDir)) { 176 fs.mkdirSync(localeOutputDir, { recursive: true }); 177 } 178 179 // Find all .ftl files for this locale 180 const ftlFiles = findFtlFiles(localeSourceDir); 181 182 if (ftlFiles.length === 0) { 183 console.warn(`⚠️ No .ftl files found in ${localeSourceDir}`); 184 continue; 185 } 186 187 // Process each .ftl file as a separate namespace 188 for (const { path: ftlPath, namespace } of ftlFiles) { 189 try { 190 const content = fs.readFileSync(ftlPath, "utf-8"); 191 const messages = extractMessagesFromFtl(content); 192 193 if (Object.keys(messages).length === 0) { 194 console.warn(`⚠️ No messages found in ${ftlPath}`); 195 continue; 196 } 197 198 // Write namespace JSON file 199 const outputPath = path.join(localeOutputDir, `${namespace}.json`); 200 fs.writeFileSync( 201 outputPath, 202 JSON.stringify(messages, null, 2), 203 "utf-8", 204 ); 205 206 console.log( 207 `${namespace}: ${Object.keys(messages).length} keys → ${path.relative(process.cwd(), outputPath)}`, 208 ); 209 totalFiles++; 210 } catch (error) { 211 console.error(`❌ Error processing ${ftlPath}:`, error.message); 212 } 213 } 214 } 215 216 console.log( 217 `🎉 Compilation complete! ${totalFiles} namespace files generated.`, 218 ); 219} 220 221/** 222 * Copy compiled translations to app/public/locales 223 */ 224function copyToApp() { 225 const appPublicLocales = path.join( 226 __dirname, 227 "..", 228 "..", 229 "app", 230 "public", 231 "locales", 232 ); 233 234 console.log("\n📋 Copying translations to app..."); 235 236 // Remove old locales directory in app 237 if (fs.existsSync(appPublicLocales)) { 238 fs.rmSync(appPublicLocales, { recursive: true, force: true }); 239 } 240 241 // Copy compiled locales to app 242 fs.cpSync(LOCALES_OUTPUT_DIR, appPublicLocales, { recursive: true }); 243 244 console.log(`✅ Copied to ${path.relative(process.cwd(), appPublicLocales)}`); 245} 246 247// Run the compilation 248compileTranslations(); 249 250// Copy to app if it exists 251const appPath = path.join(__dirname, "..", "..", "app"); 252if (fs.existsSync(appPath)) { 253 copyToApp(); 254}