From 1147c61abaf6c11925cdfc710cf414c19f876647 Mon Sep 17 00:00:00 2001 From: Tulio Leao Date: Sun, 17 Aug 2025 12:50:04 -0300 Subject: [PATCH 1/2] Add scripts for importing localised strings from OpenRCT2/Localisation --- tools/scripts/modules/lang-js-parse.js | 8 ++ tools/scripts/modules/lang-tools.js | 66 ++++++++++++ tools/scripts/modules/lang-txt-parse.js | 7 ++ tools/scripts/modules/prettify-json.js | 90 +++++++++++++++++ tools/scripts/modules/utils.js | 22 ++++ tools/scripts/prettify-object-json.js | 48 +++++++++ tools/scripts/update-localisation.js | 129 ++++++++++++++++++++++++ 7 files changed, 370 insertions(+) create mode 100644 tools/scripts/modules/lang-js-parse.js create mode 100644 tools/scripts/modules/lang-tools.js create mode 100644 tools/scripts/modules/lang-txt-parse.js create mode 100644 tools/scripts/modules/prettify-json.js create mode 100644 tools/scripts/modules/utils.js create mode 100644 tools/scripts/prettify-object-json.js create mode 100644 tools/scripts/update-localisation.js diff --git a/tools/scripts/modules/lang-js-parse.js b/tools/scripts/modules/lang-js-parse.js new file mode 100644 index 0000000000..da24a59561 --- /dev/null +++ b/tools/scripts/modules/lang-js-parse.js @@ -0,0 +1,8 @@ +const fs = require("fs"); + +function parse(file) { + var res = JSON.parse(fs.readFileSync(file, "utf8")); + return res; +} + +module.exports = { parse } \ No newline at end of file diff --git a/tools/scripts/modules/lang-tools.js b/tools/scripts/modules/lang-tools.js new file mode 100644 index 0000000000..930446189e --- /dev/null +++ b/tools/scripts/modules/lang-tools.js @@ -0,0 +1,66 @@ +const parserJS = require("./lang-js-parse") +const parserTxt = require("./lang-txt-parse") +const utils = require("./utils"); +const path = require("node:path") +const fs = require("fs"); + +const SupportedLanguages = [ + "ar-EG", "ca-ES", "cs-CZ", "da-DK", "de-DE", "en-GB", "en-US", "eo-ZZ", + "es-ES", "fi-FI", "fr-FR", "gl-ES", "hu-HU", "it-IT", "ja-JP", "ko-KR", "nb-NO", + "nl-NL", "pl-PL", "pt-BR", "ru-RU", "sv-SE", "tr-TR", "zh-CN", "zh-TW" +]; + +const Language = { + ArEG: "ar-EG", + CaES: "ca-ES", + CsCZ: "cs-CZ", + DaDK: "da-DK", + DeDE: "de-DE", + EnGB: "en-GB", + EnUS: "en-US", + EoZZ: "eo-ZZ", + EsES: "es-ES", + FiFI: "fi-FI", + FrFR: "fr-FR", + GlES: "gl-ES", + HuHU: "hu-HU", + ItIT: "it-IT", + JaJP: "ja-JP", + KoKR: "ko-KR", + NbNO: "nb-NO", + NlNL: "nl-NL", + PlPL: "pl-PL", + PtBR: "pt-BR", + RuRU: "ru-RU", + SvSE: "sv-SE", + TrTR: "tr-TR", + ZhCN: "zh-CN", + ZhTW: "zh-TW" +}; + +function parseLanguage(file) { + var ext = path.extname(file); + var data = null; + if(ext == ".json") { + data = parserJS.parse(file); + } + else if(ext == ".txt") { + data = parserTxt.parse(file); + } + if(data == null) { + throw "Unable to parse file"; + } + return data; +} + +function parseObject(file) { + var res = JSON.parse(fs.readFileSync(file, "utf8")); + return res; +} + +module.exports = { + Language, + utils, + parseLanguage, + parseObject +} diff --git a/tools/scripts/modules/lang-txt-parse.js b/tools/scripts/modules/lang-txt-parse.js new file mode 100644 index 0000000000..d8eb7bd170 --- /dev/null +++ b/tools/scripts/modules/lang-txt-parse.js @@ -0,0 +1,7 @@ +const fs = require("fs"); + +function parse(file) { + throw "Not implemented"; +} + +module.exports = { parse } \ No newline at end of file diff --git a/tools/scripts/modules/prettify-json.js b/tools/scripts/modules/prettify-json.js new file mode 100644 index 0000000000..685e2a7f6c --- /dev/null +++ b/tools/scripts/modules/prettify-json.js @@ -0,0 +1,90 @@ +function init(state, opts) { + var spacer = ""; + if (opts.useTabs) { + spacer = "\t"; + } else { + for (var i = 0; i < opts.tabSize; ++i) { + spacer += " "; + } + } + for (var i = 0; i < 20; ++i) { + var spacing = ""; + for(var n = 0; n < i; ++n) { + spacing += spacer; + } + state.spacing[i] = spacing; + } +} + +function getSpacing(state, opts) { + return state.spacing[state.indent]; +} + +function beginScope(state, opts, str, inline) { + var res = str + (inline ? "" : "\n"); + state.indent++; + return res; +} + +function endScope(state, opts, str, inline) { + state.indent--; + return (inline ? "" : getSpacing(state, opts)) + str; +} + +function processValue(state, opts, value, inline) { + var res = ""; + if (value instanceof Array) { + res += beginScope(state, opts, "[", false); + var values = []; + for (var i = 0; i < value.length; ++i) { + const innerBody = getSpacing(state, opts) + processValue(state, opts, value[i], inline); + values.push(innerBody); + } + if (values.length > 0) { + res += values.join(",\n") + "\n"; + } + res += endScope(state, opts, "]", false); + } + else if (value instanceof Object) { + res += beginScope(state, opts, inline ? "{ " : "{", inline); + + var values = []; + for (const key of Object.keys(value)) { + const inlineChildren = opts.forceInline.includes(key); + const innerBody = (inline ? "" : getSpacing(state, opts)) + JSON.stringify(key) + ": " + processValue(state, opts, value[key], inlineChildren); + values.push(innerBody); + } + if (values.length > 0) { + res += values.join(inline ? ", " : ",\n") + (inline ? "" : "\n"); + } + res += endScope(state, opts, inline ? " }" : "}", inline); + } + else { + res += JSON.stringify(value); + } + return res; +} + +function prettifyJSON(value, opts) { + var state = { + indent: 0, + spacing: [], + } + if (opts == null || opts == undefined) { + opts = {}; + } + if (!('forceInline' in opts)) { + opts.forceInline = []; + } + if (!('useTabs' in opts)) { + opts.useTabs = false; + } + if (!('tabSize' in opts)) { + opts.tabSize = 4; + } + init(state, opts); + return processValue(state, opts, value, false) + + "\n" /* Some editors like adding newlines so lets just keep it that way */; +} + +module.exports = { prettifyJSON }; \ No newline at end of file diff --git a/tools/scripts/modules/utils.js b/tools/scripts/modules/utils.js new file mode 100644 index 0000000000..792e1ff763 --- /dev/null +++ b/tools/scripts/modules/utils.js @@ -0,0 +1,22 @@ +const path = require("path") +const fs = require("fs"); + +function getFiles(path) { + const files = [] + for (const file of fs.readdirSync(path)) { + const fullPath = path + '/' + file + if(fs.lstatSync(fullPath).isDirectory()) + getFiles(fullPath).forEach(x => files.push(file + '/' + x)) + else files.push(file) + } + return files +} + +function getObjectFiles(filePath) { + var files = getFiles(filePath); + return files.filter(file => { + return path.extname(file) === ".json"; + }); +} + +module.exports = { getObjectFiles }; diff --git a/tools/scripts/prettify-object-json.js b/tools/scripts/prettify-object-json.js new file mode 100644 index 0000000000..267e5d6812 --- /dev/null +++ b/tools/scripts/prettify-object-json.js @@ -0,0 +1,48 @@ +// To use this script do: +// node ./tools/scripts/prettify-object-json.js PATH_TO_OBJECTS_REPO + +const fs = require("fs"); +const path = require("path"); +const utils = require("./modules/utils.js"); +const { prettifyJSON } = require("./modules/prettify-json"); + +const formatOptions = { + "forceInline": ["images", "noCsgImages"], + "useTabs": false, + "tabSize": 4, +}; + +const args = process.argv +if (args.length < 3) { + console.error("ERROR: Expected "); + process.exit(-1); +} + +const objectsPathOrFile = args[2]; + +function formatObjectFile(fullPath) { + console.info(`Formatting: ${fullPath}`); + const fileData = fs.readFileSync(fullPath, "utf8"); + const jsonData = JSON.parse(fileData); + const prettified = prettifyJSON(jsonData, formatOptions); + return prettified; +} + +const fileInfo = fs.lstatSync(objectsPathOrFile); +if (fileInfo.isDirectory()) { + // Format entire directory. + const objectFiles = utils.getObjectFiles(objectsPathOrFile); + objectFiles.forEach(filePath => { + const fullPath = path.join(objectsPathOrFile, filePath); + const prettified = formatObjectFile(fullPath); + fs.writeFileSync(fullPath, prettified, "utf8"); + }); +} else if(fileInfo.isFile()) { + // Format single file. + const prettified = formatObjectFile(objectsPathOrFile); + //console.log(prettified); + fs.writeFileSync(objectsPathOrFile, prettified, "utf8"); +} else { + console.error("ERROR: Invalid argument provided, argument is not a file or directory."); + process.exit(-1); +} diff --git a/tools/scripts/update-localisation.js b/tools/scripts/update-localisation.js new file mode 100644 index 0000000000..af70040361 --- /dev/null +++ b/tools/scripts/update-localisation.js @@ -0,0 +1,129 @@ +// To use this script do: +// node ./tools/scripts/update-localisation.js PATH_TO_LOCALISATION_REPO PATH_TO_OBJECTS_REPO + +const fs = require("node:fs"); +const path = require("node:path"); +const langTools = require("./modules/lang-tools.js"); +const utils = require("./modules/utils.js") +const { prettifyJSON } = require("./modules/prettify-json"); + +const formatOptions = { + "forceInline": ["images", "noCsgImages"], + "useTabs": false, + "tabSize": 4, +}; + +function parseObjectLanguages(localisationRootPath) { + var res = []; + for (let key in langTools.Language) { + const langId = langTools.Language[key]; + const langPath = path.join(localisationRootPath, "objects", langId + ".json"); + const langData = langTools.parseLanguage(langPath, langId); + res[langId] = langData; + } + return res; +} + +function mergeLanguageData(objectData, langData, lang) { + // Create a copy. + objectData = Object.assign({}, objectData); + const objectId = objectData["id"]; + + var objectLangData = langData[objectId]; + if(objectLangData === undefined) + { + console.warn(`Missing object group '${objectId}' in language '${lang}'`); + return objectData; + } + + var langStrings = objectData["strings"]; + for(var key in langStrings) { + const translation = objectLangData[key]; + if(translation === undefined) { + console.warn(`Missing key ${key} in ${lang} data`); + continue; + } + const referenceTranslation = objectLangData[`reference-${key}`]; + if(lang != langTools.Language.EnGB && lang != langTools.Language.EnUS && referenceTranslation == translation) + { + //console.warn(`Warning: Translation ${key} is same as reference for non-english language: ${lang}, skipping.`); + continue; + } + else { + const oldEntry = langStrings[key][lang]; + if(oldEntry === undefined) { + //console.log(`Created key ${lang}:${key} -> '${translation}'`); + langStrings[key][lang] = translation; + } + else if(oldEntry != translation) { + console.log(`Updated key ${lang}:${key} from '${oldEntry}' -> '${translation}'`); + langStrings[key][lang] = translation; + } + else if(oldEntry == translation) { + //console.log(`Translation identical for key ${lang}:${key} -> ${translation}`); + } + } + } + + return objectData; +} + +function processObjectFile(objectsLangData, objectFilePath) { + console.info(`Processing Object ${objectFilePath}`); + + var objectData = langTools.parseObject(objectFilePath); + if (objectData == null) { + throw `Failed to read object data: ${objectFilePath}`; + } + + // Update all keys. + for (let key in langTools.Language) { + const langId = langTools.Language[key]; + const langData = objectsLangData[langId]; + objectData = mergeLanguageData(objectData, langData, langId); + } + + // Remove en-US when its same as en-GB + for(let nameKey in objectData["strings"]) { + var stringEntries = objectData["strings"][nameKey]; + if(stringEntries[langTools.Language.EnUS] !== undefined && stringEntries[langTools.Language.EnUS] == stringEntries[langTools.Language.EnGB]) { + delete stringEntries[langTools.Language.EnUS]; + } + } + + // Save to file. + var jsonStr = prettifyJSON(objectData, formatOptions); + fs.writeFileSync(objectFilePath, jsonStr, "utf8"); +} + +function mergeLocalisationToObjects(localisationRootPath, objectsRootPath) { + const objectsPath = path.join(objectsRootPath, "objects"); + + const objectFiles = utils.getObjectFiles(objectsPath); + if (objectFiles.length == 0) { + throw "No object files found"; + } + + const objectsLangData = parseObjectLanguages(localisationRootPath); + objectFiles.forEach(objectFile => processObjectFile(objectsLangData, path.join(objectsPath, objectFile))); +} + +const args = process.argv +if (args.length < 4) { + console.error("ERROR: Expected arguments: "); + process.exit(-1); +} + +const localisationPath = args[2]; +if (!fs.lstatSync(localisationPath).isDirectory()) { + console.error("ERROR: Argument provided for localisation is not a directory."); + process.exit(-1); +} + +const objectsPath = args[3]; +if (!fs.lstatSync(objectsPath).isDirectory()) { + console.error("ERROR: Argument provided for objects is not a directory."); + process.exit(-1); +} + +mergeLocalisationToObjects(localisationPath, objectsPath); \ No newline at end of file From 0a1b25635d8f6959e2e0bf881f51a74168f56d6f Mon Sep 17 00:00:00 2001 From: Tulio Leao Date: Sun, 17 Aug 2025 12:56:50 -0300 Subject: [PATCH 2/2] Add fr-CA, uk-UA, vi-VN to scripts --- tools/scripts/languages.py | 6 +++--- tools/scripts/modules/lang-tools.js | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/scripts/languages.py b/tools/scripts/languages.py index bf42511721..9e96358fbd 100644 --- a/tools/scripts/languages.py +++ b/tools/scripts/languages.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 SUPPORTED_LANGUAGES = ["ar-EG", "ca-ES", "cs-CZ", "da-DK", "de-DE", "en-GB", "en-US", "eo-ZZ",\ - "es-ES", "fi-FI", "fr-FR", "gl-ES", "hu-HU", "it-IT", "ja-JP", "ko-KR",\ - "nb-NO", "nl-NL", "pl-PL", "pt-BR", "ru-RU", "sv-SE", "tr-TR", "uk-UA",\ - "zh-CN", "zh-TW"] + "es-ES", "fi-FI", "fr-CA", "fr-FR", "gl-ES", "hu-HU", "it-IT", "ja-JP",\ + "ko-KR", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "ru-RU", "sv-SE", "tr-TR",\ + "uk-UA", "vi-VN", "zh-CN", "zh-TW"] diff --git a/tools/scripts/modules/lang-tools.js b/tools/scripts/modules/lang-tools.js index 930446189e..b79a25373a 100644 --- a/tools/scripts/modules/lang-tools.js +++ b/tools/scripts/modules/lang-tools.js @@ -6,8 +6,9 @@ const fs = require("fs"); const SupportedLanguages = [ "ar-EG", "ca-ES", "cs-CZ", "da-DK", "de-DE", "en-GB", "en-US", "eo-ZZ", - "es-ES", "fi-FI", "fr-FR", "gl-ES", "hu-HU", "it-IT", "ja-JP", "ko-KR", "nb-NO", - "nl-NL", "pl-PL", "pt-BR", "ru-RU", "sv-SE", "tr-TR", "zh-CN", "zh-TW" + "es-ES", "fi-FI", "fr-CA", "fr-FR", "gl-ES", "hu-HU", "it-IT", "ja-JP", + "ko-KR", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "ru-RU", "sv-SE", "tr-TR", + "uk-UA", "vi-VN", "zh-CN", "zh-TW" ]; const Language = { @@ -21,6 +22,7 @@ const Language = { EoZZ: "eo-ZZ", EsES: "es-ES", FiFI: "fi-FI", + FrCA: "fr-CA", FrFR: "fr-FR", GlES: "gl-ES", HuHU: "hu-HU", @@ -34,6 +36,8 @@ const Language = { RuRU: "ru-RU", SvSE: "sv-SE", TrTR: "tr-TR", + UkUA: "uk-UA", + ViVN: "vi-VN", ZhCN: "zh-CN", ZhTW: "zh-TW" };