Linting of 'generate translations'

This commit is contained in:
Pieter Vander Vennet 2024-08-29 21:49:11 +02:00
parent c9751a3eb5
commit 5302ec3342

View file

@ -6,6 +6,7 @@ import Script from "./Script"
const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"] const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"]
const ignoreTerms = ["searchTerms"] const ignoreTerms = ["searchTerms"]
class TranslationPart { class TranslationPart {
contents: Map<string, TranslationPart | string> = new Map<string, TranslationPart | string>() contents: Map<string, TranslationPart | string> = new Map<string, TranslationPart | string>()
@ -14,7 +15,8 @@ class TranslationPart {
const rootTranslation = new TranslationPart() const rootTranslation = new TranslationPart()
for (const file of files) { for (const file of files) {
const content = JSON.parse(readFileSync(file, { encoding: "utf8" })) const content = JSON.parse(readFileSync(file, { encoding: "utf8" }))
rootTranslation.addTranslation(file.substr(0, file.length - ".json".length), content) const language = file.substr(0, file.length - ".json".length)
rootTranslation.addTranslation(language, content)
} }
return rootTranslation return rootTranslation
} }
@ -46,10 +48,6 @@ class TranslationPart {
return return
} }
for (const translationsKey in translations) { for (const translationsKey in translations) {
if (!translations.hasOwnProperty(translationsKey)) {
continue
}
const v = translations[translationsKey] const v = translations[translationsKey]
if (typeof v != "string") { if (typeof v != "string") {
console.error( console.error(
@ -104,9 +102,6 @@ class TranslationPart {
} }
for (let key in object) { for (let key in object) {
if (!object.hasOwnProperty(key)) {
continue
}
if (ignoreTerms.indexOf(key) >= 0) { if (ignoreTerms.indexOf(key) >= 0) {
continue continue
} }
@ -155,13 +150,13 @@ class TranslationPart {
this.contents.set(key, new TranslationPart()) this.contents.set(key, new TranslationPart())
} }
;(this.contents.get(key) as TranslationPart).recursiveAdd(v, context + "." + key) (this.contents.get(key) as TranslationPart).recursiveAdd(v, context + "." + key)
} }
} }
knownLanguages(): string[] { knownLanguages(): string[] {
const languages = [] const languages = []
for (let key of Array.from(this.contents.keys())) { for (const key of Array.from(this.contents.keys())) {
const value = this.contents.get(key) const value = this.contents.get(key)
if (typeof value === "string") { if (typeof value === "string") {
@ -180,20 +175,20 @@ class TranslationPart {
const parts = [] const parts = []
let keys = Array.from(this.contents.keys()) let keys = Array.from(this.contents.keys())
keys = keys.sort() keys = keys.sort()
for (let key of keys) { for (const key of keys) {
let value = this.contents.get(key) let value = this.contents.get(key)
if (typeof value === "string") { if (typeof value === "string") {
value = value.replace(/"/g, '\\"').replace(/\n/g, "\\n") value = value.replace(/"/g, "\\\"").replace(/\n/g, "\\n")
if (neededLanguage === undefined) { if (neededLanguage === undefined) {
parts.push(`\"${key}\": \"${value}\"`) parts.push(`"${key}": "${value}"`)
} else if (key === neededLanguage) { } else if (key === neededLanguage) {
return `"${value}"` return `"${value}"`
} }
} else { } else {
const sub = (value as TranslationPart).toJson(neededLanguage) const sub = (value as TranslationPart).toJson(neededLanguage)
if (sub !== "") { if (sub !== "") {
parts.push(`\"${key}\": ${sub}`) parts.push(`"${key}": ${sub}`)
} }
} }
} }
@ -234,7 +229,7 @@ class TranslationPart {
} else if (!isLeaf) { } else if (!isLeaf) {
errors.push({ errors.push({
error: "Mixed node: non-leaf node has translation strings", error: "Mixed node: non-leaf node has translation strings",
path: path, path: path
}) })
} }
@ -285,7 +280,7 @@ class TranslationPart {
value + value +
"\n" + "\n" +
fixLink, fixLink,
path: path, path: path
}) })
} }
return return
@ -297,7 +292,7 @@ class TranslationPart {
error: `The translation for ${key} does not have the required subpart ${part} (in ${usedByLanguage}). error: `The translation for ${key} does not have the required subpart ${part} (in ${usedByLanguage}).
\tThe full translation is ${value} \tThe full translation is ${value}
\t${fixLink}`, \t${fixLink}`,
path: path, path: path
}) })
} }
} }
@ -334,24 +329,6 @@ class TranslationPart {
} }
} }
/**
* Checks that the given object only contains string-values
* @param tr
*/
function isTranslation(tr: any): boolean {
if (tr["#"] === "no-translations") {
return false
}
if (tr["special"]) {
return false
}
for (const key in tr) {
if (typeof tr[key] !== "string") {
return false
}
}
return true
}
/** /**
* Converts a translation object into something that can be added to the 'generated translations'. * Converts a translation object into something that can be added to the 'generated translations'.
@ -361,9 +338,10 @@ function isTranslation(tr: any): boolean {
function transformTranslation( function transformTranslation(
obj: any, obj: any,
path: string[] = [], path: string[] = [],
languageWhitelist: string[] = undefined languageWhitelist: string[] = undefined,
shortNotation = false
) { ) {
if (isTranslation(obj)) { if (GenerateTranslations.isTranslation(obj)) {
return `new Translation( ${JSON.stringify(obj)} )` return `new Translation( ${JSON.stringify(obj)} )`
} }
@ -380,7 +358,7 @@ function transformTranslation(
} }
let value = obj[key] let value = obj[key]
if (isTranslation(value)) { if (GenerateTranslations.isTranslation(value)) {
if (languageWhitelist !== undefined) { if (languageWhitelist !== undefined) {
const nv = {} const nv = {}
for (const ln of languageWhitelist) { for (const ln of languageWhitelist) {
@ -395,7 +373,7 @@ function transformTranslation(
)}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}` )}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}`
} }
const subParts: string[] = value["en"].match(/{[^}]*}/g) const subParts: string[] = value["en"].match(/{[^}]*}/g)
let expr = `return new Translation(${JSON.stringify(value)}, "core:${path.join( let expr = `new Translation(${JSON.stringify(value)}, "core:${path.join(
"." "."
)}.${key}")` )}.${key}")`
if (subParts !== null) { if (subParts !== null) {
@ -409,12 +387,16 @@ function transformTranslation(
"." "."
)}: A subpart contains invalid characters: ${subParts.join(", ")}` )}: A subpart contains invalid characters: ${subParts.join(", ")}`
} }
expr = `return new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify( expr = `new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify(
value value
)}, "core:${path.join(".")}.${key}")` )}, "core:${path.join(".")}.${key}")`
} }
if (shortNotation) {
values.push(`${spaces} ${key}: ${expr}`)
values.push(`${spaces}get ${key}() { ${expr} }`) } else {
values.push(`${spaces}get ${key}() { return ${expr} }`)
}
} else { } else {
values.push( values.push(
spaces + key + ": " + transformTranslation(value, [...path, key], languageWhitelist) spaces + key + ": " + transformTranslation(value, [...path, key], languageWhitelist)
@ -469,54 +451,11 @@ function formatFile(path) {
writeFileSync(path, JSON.stringify(contents, null, " ") + (endsWithNewline ? "\n" : "")) writeFileSync(path, JSON.stringify(contents, null, " ") + (endsWithNewline ? "\n" : ""))
} }
/**
* Generates the big compiledTranslations file
*/
function genTranslations() {
if (!fs.existsSync("./src/assets/generated/")) {
fs.mkdirSync("./src/assets/generated/")
}
const translations = JSON.parse(
fs.readFileSync("./src/assets/generated/translations.json", "utf-8")
)
const transformed = transformTranslation(translations)
let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`
module += " public static t = " + transformed
module += "\n }"
fs.writeFileSync("./src/assets/generated/CompiledTranslations.ts", module)
}
/** /**
* Reads 'lang/*.json', writes them into to 'assets/generated/translations.json'. * Reads 'lang/*.json', writes them into to 'assets/generated/translations.json'.
* This is only for the core translations * This is only for the core translations
*/ */
function compileTranslationsFromWeblate() {
const translations = ScriptUtils.readDirRecSync("./langs", 1).filter(
(path) => path.indexOf(".json") > 0
)
const allTranslations = new TranslationPart()
allTranslations.validateStrict()
for (const translationFile of translations) {
try {
const contents = JSON.parse(readFileSync(translationFile, "utf-8"))
let language = translationFile.substring(translationFile.lastIndexOf("/") + 1)
language = language.substring(0, language.length - 5)
allTranslations.add(language, contents)
} catch (e) {
throw "Could not read file " + translationFile + " due to " + e
}
}
writeFileSync(
"./src/assets/generated/translations.json",
JSON.stringify(JSON.parse(allTranslations.toJson()), null, " ")
)
}
/** /**
* Get all the strings out of the layers; writes them onto the weblate paths * Get all the strings out of the layers; writes them onto the weblate paths
@ -608,7 +547,7 @@ function MergeTranslation(source: any, target: any, language: string, context: s
if (targetV[language] !== undefined && targetV[language] !== sourceV) { if (targetV[language] !== undefined && targetV[language] !== sourceV) {
was = " (overwritten " + targetV[language] + ")" was = " (overwritten " + targetV[language] + ")"
} }
console.log(" + ", context + "." + language, "-->", sourceV, was) // console.log(" + ", context + "." + language, "-->", sourceV, was)
continue continue
} }
if (typeof sourceV === "object") { if (typeof sourceV === "object") {
@ -697,7 +636,7 @@ function removeNonEnglishTranslations(object: any) {
leaf["en"] = en leaf["en"] = en
}, },
(possibleLeaf) => (possibleLeaf) =>
possibleLeaf !== null && typeof possibleLeaf === "object" && isTranslation(possibleLeaf) possibleLeaf !== null && typeof possibleLeaf === "object" && GenerateTranslations.isTranslation(possibleLeaf)
) )
} }
@ -732,6 +671,25 @@ class GenerateTranslations extends Script {
super("Syncs translations from/to the theme and layer files") super("Syncs translations from/to the theme and layer files")
} }
/**
* Checks that the given object only contains string-values
* @param tr
*/
static isTranslation(tr: Record<string, string | object>): boolean {
if (tr["#"] === "no-translations") {
return false
}
if (tr["special"]) {
return false
}
for (const key in tr) {
if (typeof tr[key] !== "string") {
return false
}
}
return true
}
/** /**
* OUtputs the 'used_languages.json'-file * OUtputs the 'used_languages.json'-file
*/ */
@ -754,22 +712,74 @@ class GenerateTranslations extends Script {
} }
} }
/**
* Generates the big compiledTranslations file based on 'translations.json'
*/
genTranslations(englishOnly?: boolean) {
if (!fs.existsSync("./src/assets/generated/")) {
fs.mkdirSync("./src/assets/generated/")
}
const translations = JSON.parse(
fs.readFileSync("./src/assets/generated/translations.json", "utf-8")
)
const transformed = transformTranslation(translations, undefined, englishOnly ? ["en"] : undefined, englishOnly)
let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`
module += " public static t = " + transformed
module += "\n }"
fs.writeFileSync("./src/assets/generated/CompiledTranslations.ts", module)
}
compileTranslationsFromWeblate(englishOnly: boolean) {
const translations = ScriptUtils.readDirRecSync("./langs", 1).filter(
(path) => path.indexOf(".json") > 0
)
const allTranslations = new TranslationPart()
allTranslations.validateStrict()
for (const translationFile of translations) {
try {
const contents = JSON.parse(readFileSync(translationFile, "utf-8"))
let language = translationFile.substring(translationFile.lastIndexOf("/") + 1)
language = language.substring(0, language.length - 5)
if (englishOnly && language !== "en") {
continue
}
allTranslations.add(language, contents)
} catch (e) {
throw "Could not read file " + translationFile + " due to " + e
}
}
writeFileSync(
"./src/assets/generated/translations.json",
JSON.stringify(JSON.parse(allTranslations.toJson()), null, " ")
)
}
async main(args: string[]): Promise<void> { async main(args: string[]): Promise<void> {
if (!existsSync("./langs/themes")) { if (!existsSync("./langs/themes")) {
mkdirSync("./langs/themes") mkdirSync("./langs/themes")
} }
const themeOverwritesWeblate = args[0] === "--ignore-weblate" const themeOverwritesWeblate = args[0] === "--ignore-weblate"
const englishOnly = args[0] === "--english-only" const englishOnly = args[0] === "--english-only"
if (englishOnly) {
console.log("ENGLISH ONLY")
}
if (!themeOverwritesWeblate) { if (!themeOverwritesWeblate) {
mergeLayerTranslations() mergeLayerTranslations(englishOnly)
mergeThemeTranslations() mergeThemeTranslations(englishOnly)
compileTranslationsFromWeblate() this.compileTranslationsFromWeblate(englishOnly)
} else { } else {
console.log("Ignore weblate") console.log("Ignore weblate")
} }
this.detectUsedLanguages() this.detectUsedLanguages()
genTranslations() this.genTranslations(englishOnly)
{ {
const allTranslationFiles = ScriptUtils.readDirRecSync("langs").filter((path) => const allTranslationFiles = ScriptUtils.readDirRecSync("langs").filter((path) =>
path.endsWith(".json") path.endsWith(".json")