UNPKG

intl-t

Version:

A Fully-Typed Node-Based i18n Translation Library

205 lines (203 loc) 8.1 kB
#!/usr/bin/env node import { promises as fs, watch as fsWatch } from "fs"; import { join, basename, relative } from "path"; export const cmdName = "declarations"; export const commonDir = ["i18n", "locales", "messages", "translations", "dictionaries", "intl"]; export const usage = ` Usage: ${cmdName} <files|folders>... [options] Options: --out, --output Output file or folder --watch Watch files/folders for changes --format <fmt> Output format: ts, d.ts, d.json.ts (default) --symbol <name> Exported symbol name (default: data) --delete Delete original JSON files --no-search Disable default JSON search --recursive Search recursively in specified folders --silent Silence logs -h, --help Show help `; export function parseArgs(args, options = {}) { options.inputs ??= []; args.splice(0, args[2] === cmdName ? 3 : 2); args.forEach(arg => { if (!arg.startsWith("-")) return options.inputs.push(arg); const [key, value] = arg.split("="); switch (key) { case "-h": case "--help": console.log(usage); process.exit(0); case "--out": case "--output": options.output = value; break; case "--watch": options.watch = true; break; case "--no-search": options.disableSearch = true; break; case "--format": options.format = value; break; case "--symbol": options.symbolName = value; break; case "--delete": options.deleteOriginal = true; break; case "--silent": options.silent = true; break; default: throw new Error(`Unknown option ${arg}. Use --help for usage.`); } }); options.disableSearch ??= false; options.silent ??= false; return options; } export async function generateDeclarations(inputsOrOptions, { inputs = [inputsOrOptions].flat(), disableSearch = true, format = "d.json.ts", watch = false, deleteOriginal = false, silent = true, output, symbolName, } = Array.isArray(inputsOrOptions) || typeof inputsOrOptions === "string" ? { inputs: [inputsOrOptions].flat() } : inputsOrOptions) { const log = silent ? () => { } : console.log; const currentFolder = process.cwd(); if (!(inputs.length || disableSearch)) { inputs = Object.entries(Object.groupBy((await fs.readdir(currentFolder, { withFileTypes: true, recursive: true }).catch(err => { throw `Error reading directory "${currentFolder}": ${err.message}`; })).filter(file => !file.parentPath.match(/node_modules|\/\.\w+\/?/)), file => file.parentPath)) .filter(entries => { const [path, { length } = []] = entries; const files = entries[1]?.filter(file => file.name.match(/\.json\w*$/)) || []; return files.length && (files.length >= length / 2 || commonDir.some(dir => path.includes(dir))); }) .map(([path]) => path); } if (!inputs.length) { throw "No input files or folders specified. Use --help for usage."; } log(`Processing ${inputs.length} input file(s) or folder(s)... ${inputs.map(f => relative(currentFolder, f))}`); for (const input of inputs) { const stat = await fs.stat(input).catch(() => null); if (!stat) throw `File or folder not found: ${input}`; if (stat.isDirectory()) { let files = await fs.readdir(input, { withFileTypes: true, recursive: false }); files = files.filter(file => file.isFile() && file.name.match(/\.json\w*$/)); for (const file of files) { try { await generateDeclarations(join(file.parentPath, file.name), { disableSearch: true, deleteOriginal, watch: false, symbolName, format, }); } catch (err) { log(err); } } if (watch) { try { const watcher = fsWatch(input, { recursive: true }); watcher.on("change", (_, filename) => { if (!filename.toString().match(/\.json\w*$/)) return; try { generateDeclarations(join(input, filename.toString()), { disableSearch: true, deleteOriginal: false, watch: false, symbolName, format, }); } catch (err) { log(err); } }); } catch (err) { throw `Error watching directory "${input}": ${err.message}`; } } return; } if (!input.match(/\.json\w*$/)) continue; let json; try { json = await fs.readFile(input, "utf-8"); } catch (err) { throw `Error reading file "${input}": ${err.message}`; } try { json = JSON.parse(json); } catch (err) { throw `Error parsing JSON file "${input}": ${err.message}`; } try { json = JSON.stringify(json, null, 2); } catch (err) { throw `Error stringifying JSON file "${input}": ${err.message}`; } symbolName ??= basename(input) .replace(/\.json\w*$/, "") .replace(/[^a-zA-Z0-9_$]/g, "_"); switch (format) { case "ts": json = `export const ${symbolName} = (${json}) as const;\nexport type ${symbolName} = typeof ${symbolName};\nexport default ${symbolName};\n`; case "d.ts": json = `export declare const ${symbolName} = (${json}) as const;\nexport declare type ${symbolName} = typeof ${symbolName};\nexport default ${symbolName};\n`; case "d.json.ts": default: json = `export declare const ${symbolName}: ${json};\nexport type ${symbolName} = typeof ${symbolName};\nexport default ${symbolName};`; } output ??= input.replace(/\.json\w*$/, `.${format}`); try { await fs.writeFile(output, json, "utf-8"); if (deleteOriginal && input !== output) { try { await fs.unlink(input); } catch (err) { throw `Error deleting original file "${input}": ${err.message}`; } } } catch (err) { throw `Error writing output file "${output}": ${err.message}`; } if (watch) { try { const watcher = fsWatch(input); watcher.on("change", () => { try { generateDeclarations(input, { deleteOriginal: false, disableSearch: true, output, symbolName, format, watch: false, }); } catch (err) { log(err); } }); } catch (err) { throw `Error watching file "${input}": ${err.message}`; } } } } export default function main(args) { return generateDeclarations(parseArgs(args)); }