diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ccbe46 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/README.md b/README.md index a56bbe1..380dee4 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,18 @@ These might look incorrect but with `%s` inserted it will be things like - `destroyed skin` and `ripped skin` refer to NoN's ripped-clothing appearance. Prefer wording about a damaged appearance, outfit, or clothing when a literal translation would imply bodily injury. - Keep filenames and paths such as `destroyed_skin.png`, `destroyed_skin_mask_%s.png`, and `config/needsofnature` unchanged. - Keep mod and project names such as `Quick Skin`, `Skin Overrides`, `CPM`, `Figura`, `NoFigura`, and `NoN` unchanged. + +## Translation Utility + +make.ts merges multiple language files into one for easy editing, then splits them back into separate files. This tool lets you: +- find the original text without searching another file +- automatically remove duplicate text to save space +- sort your i18n files according to the official order + +### Usage + +1. Open `make.ts` and edit the `targetLangFiles` and the `workFile` fields, change `zh_cn` to your language. please notice that The `suffix` and `file` values must be unique. +2. Run `./make.ts` (or `ts-node make.ts` / `deno make.ts`). The language files will be merged into the `workFile` location (e.g. `zh_cn.json`). +3. Edit the output file. +4. Run `./make.ts` again. If the workfile is newer than the other files, the script will write the changes back to the language files (at the locations specified by `targetLangFiles`). +5. If you don't want a language to be written back, add `ro: true` to its entry in `targetLangFiles`. \ No newline at end of file diff --git a/make.ts b/make.ts new file mode 100755 index 0000000..6d62be5 --- /dev/null +++ b/make.ts @@ -0,0 +1,109 @@ +#!/usr/bin/env ts-node + +import fs from 'fs'; + +// a sample tool that merge (zh|en)(sfw|nsfw) ver together for edit faster +// for other language translator: if you need to use this script, just edit `targetLangFiles` and `workFile`. + +const targetLangFiles: LanguageInstance[] = [ + {suffix: "en_us_nsfw", file: "NSFW (Default Pack)/en_us.json", overrideSuffix: "en_us_sfw", ro: true}, + {suffix: "en_us_sfw", file: "SFW (baked in NoN)/en_us.json", ro: true}, + {suffix: "zh_cn_nsfw", file: "NSFW (Default Pack)/zh_cn.json", overrideSuffix: "zh_cn_sfw"}, + {suffix: "zh_cn_sfw", file: "SFW (baked in NoN)/zh_cn.json"} +] +const workFile = "zh_cn.json" + +type LanguageInstance = { + suffix: string, + file: string, + data?: Record, + overrideSuffix?: string, + override?: LanguageInstance, + ro?: boolean // read only, don't edit. +} + +const mainLangFile = "SFW (baked in NoN)/en_us.json"; + +const targetLangFilesBySuffix: Record = {} +for (let l of targetLangFiles) { + if (!l.suffix.match(/^[a-z0-9-_]+$/i)) throw new Error(`bad Suffix: ${JSON.stringify(l.suffix)} that not match ^[a-z0-9-_]+$ (ignore case)`) + targetLangFilesBySuffix[l.suffix] = l +} +for (let l of targetLangFiles) { + if (l.overrideSuffix) l.override = targetLangFilesBySuffix[l.overrideSuffix] +} + + +type KVPair = [string, string]; + +function parseOrdered(src: string): KVPair[] { + const o: KVPair[] = []; + JSON.parse(src, (k, v) => { + if (k === '') return v + if (typeof k !== "string" || typeof v !== "string") throw new Error(`unexpected k/v:${JSON.stringify([k, v])}`) + o.push([k, v]) + return v; + }); + return o; +} + +function stringify(data: KVPair[]): string { + const s = JSON.stringify + const body = data.map(([k, v]) => ` ${s(k)}: ${s(v)}`).join(",\n") + return `{\n${body}\n}\n` +} + +const keyOrder = parseOrdered(fs.readFileSync(mainLangFile, "utf-8")).map(([k, v]) => k) + + +function genMergedLang(): void { + for (const l of targetLangFiles) { + l.data = JSON.parse(fs.readFileSync(l.file, "utf-8")) as Record + } + + const output: KVPair[] = [] + for (let key of keyOrder) { + for (let l of targetLangFiles) { + let value = l.data?.[key]; + if (l.override?.data?.[key] === value) value = undefined + if (value) output.push([`${key}.${l.suffix}`, value]) + } + } + fs.writeFileSync(workFile, stringify(output)) +} + +function writeBackLang() { + let mixed = parseOrdered(fs.readFileSync(workFile, "utf-8")); + const buffer: Record = {} + for (const key of Object.keys(targetLangFilesBySuffix)) buffer[key] = [] + + for (const [k, v] of mixed) { + const [key, suffix] = k.split(/\.(?=[^.]*$)/); + if (!buffer[suffix]) throw new Error(`can't parse the suffix: ${JSON.stringify(suffix)} from key: ${JSON.stringify(k)}`) + buffer[suffix].push([key, v]) + } + + for (let suffix in buffer) { + if (targetLangFilesBySuffix[suffix].ro) continue + fs.writeFileSync(targetLangFilesBySuffix[suffix].file, stringify(buffer[suffix])) + } + +} + +;(() => { + let files = [targetLangFiles.map(l => l.file), workFile].flat() + let latestFile = files.reduce<{ mtime: Date, fileName: string } | null>( + (latest, file) => { + if (!fs.existsSync(file)) return latest + let stat = fs.statSync(file); + if (!latest) return {mtime: stat.mtime, fileName: file}; + return stat.mtime > latest.mtime ? {mtime: stat.mtime, fileName: file} : latest + }, null); + if (!latestFile) return + + if (latestFile.fileName === workFile) + writeBackLang() + else + genMergedLang() + +})(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..941cf8d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "NoN-Translations", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@types/node": "^26.6.2" + } + }, + "node_modules/@types/node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e300dc1 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@types/node": "^26.6.2" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..51ac49c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "nodenext", + "types": ["node"] + } +} \ No newline at end of file