From 3fe0dbb5b2c81b66ac85a36bbe6ebdfc469df7de Mon Sep 17 00:00:00 2001 From: "Km.Van" Date: Sat, 11 Jul 2026 16:55:38 +0800 Subject: [PATCH] fix lang builder --- tools/pot-builder.mjs | 125 ++++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 48 deletions(-) diff --git a/tools/pot-builder.mjs b/tools/pot-builder.mjs index bf172d1..18c3424 100644 --- a/tools/pot-builder.mjs +++ b/tools/pot-builder.mjs @@ -1,77 +1,106 @@ -/** - * @version 1.0.1 - */ -import { lstatSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; -import { extname } from "node:path"; +import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { extname, join } from "node:path"; + +const GETTEXT_REG = + /gettext\s*\(\s*(["'`])((?:(?!\1)[^\\]|\\.)*)\1\s*(?:,\s*(?:(["'`])((?:(?!\3)[^\\]|\\.)*)\3)?)?\s*\)/gms; -const GETTEXT_REG = /gettext\s*\(\s*(".+?")\s*,*\s*(".+?")*\s*\)/gm; export class PotBuilder { potPath = ""; sourceDir = ""; entries = {}; - /** - * @type {string[]} - */ filePaths = []; + constructor({ potPath, sourceDir }) { this.potPath = potPath; this.sourceDir = sourceDir; } + build() { + this.filePaths = []; // 清空历史,防止重复调用时叠加 this.fetchDirOrFile(this.sourceDir); + for (const path of this.filePaths) { this.buildEntries(path); } this.buildPotFile(); } + buildEntries(path) { - const code = readFileSync(path).toString(); - const matches = code.matchAll(GETTEXT_REG); - if (matches) { - for (const match of matches) { - const msgid = match[1].slice(1, -1); - const msgctxt = (match[2] || "").slice(1, -1); - if (this.entries[`${msgid}${msgctxt}`]) { - continue; - } - this.entries[`${msgid}${msgctxt}`] = ` -${msgctxt ? `msgctxt ${JSON.stringify(msgctxt)}` : ""} -msgid ${JSON.stringify(msgid)} -msgstr "" -`.trim(); + const code = readFileSync(path, "utf8"); + + for (const match of code.matchAll(GETTEXT_REG)) { + const [, , msgid] = match; + const msgctxt = match[4] || ""; + + const cacheKey = `${msgid}____${msgctxt}`; + if (this.entries[cacheKey]) { + continue; } + + // 组装 entry,确保没有多余的空行 + const lines = []; + if (msgctxt) { + lines.push(`msgctxt ${JSON.stringify(msgctxt)}`); + } + lines.push(`msgid ${JSON.stringify(msgid)}`); + lines.push('msgstr ""'); + + this.entries[cacheKey] = lines.join("\n"); } } - buildPotFile() { - const toWriteData = ` -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: \\n" -"POT-Creation-Date: \\n" -"PO-Revision-Date: \\n" -"Last-Translator: Km.Van\\n" -"Language-Team: INN STUDIO\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"X-Generator: Poedit 3.0.1\\n" -"X-Poedit-Basepath: ../src\\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\\n" -"X-Poedit-SourceCharset: UTF-8\\n" -"X-Poedit-KeywordsList: gettext\\n" -${Object.values(this.entries).join("\n\n")} -`.trim(); + buildPotFile() { + const headers = [ + "#, fuzzy", + 'msgid ""', + 'msgstr ""', + '"Project-Id-Version: \\n"', + '"POT-Creation-Date: \\n"', + '"PO-Revision-Date: \\n"', + '"Last-Translator: Km.Van\\n"', + '"Language-Team: INN STUDIO\\n"', + '"MIME-Version: 1.0\\n"', + '"Content-Type: text/plain; charset=UTF-8\\n"', + '"Content-Transfer-Encoding: 8bit\\n"', + '"X-Generator: Poedit 3.0.1\\n"', + '"X-Poedit-Basepath: ../src\\n"', + '"Plural-Forms: nplurals=2; plural=(n != 1);\\n"', + '"X-Poedit-SourceCharset: UTF-8\\n"', + '"X-Poedit-KeywordsList: gettext\\n"', + ].join("\n"); + + const content = Object.values(this.entries).join("\n\n"); + const toWriteData = `${headers}\n\n${content}\n`; // 标准文件末尾留一空行 + writeFileSync(this.potPath, toWriteData, "utf8"); } + fetchDirOrFile(filePathOrDir) { - if (lstatSync(filePathOrDir).isDirectory()) { - for (const path of readdirSync(filePathOrDir)) { - this.fetchDirOrFile(`${filePathOrDir}/${path}`); + try { + const stat = statSync(filePathOrDir); + + if (stat.isDirectory()) { + const entries = readdirSync(filePathOrDir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(filePathOrDir, entry.name); + if (entry.isDirectory()) { + this.fetchDirOrFile(fullPath); + } else if ( + entry.isFile() && + [".ts", ".tsx"].includes(extname(entry.name)) + ) { + this.filePaths.push(fullPath); + } + } + } else if ( + stat.isFile() && + [".ts", ".tsx"].includes(extname(filePathOrDir)) + ) { + this.filePaths.push(filePathOrDir); } - } else if ([".ts", ".tsx"].includes(extname(filePathOrDir))) { - this.filePaths.push(filePathOrDir); + } catch (err) { + console.error(`无法读取路径: ${filePathOrDir}`, err); } } }