forked from MapComplete/MapComplete
		
	Add layer translation files
This commit is contained in:
		
							parent
							
								
									caba824a54
								
							
						
					
					
						commit
						d2d0209ead
					
				
					 9 changed files with 6060 additions and 227 deletions
				
			
		|  | @ -12,230 +12,248 @@ import {Translation} from "../UI/i18n/Translation"; | |||
| // It spits out an overview of those to be used to load them
 | ||||
| 
 | ||||
| interface LayersAndThemes { | ||||
|     themes: any[] , layers: {parsed: any, path: string}[] | ||||
|     themes: any[], | ||||
|     layers: { parsed: any, path: string }[] | ||||
| } | ||||
| 
 | ||||
| function loadThemesAndLayers():LayersAndThemes{ | ||||
|      | ||||
| const layerFiles = ScriptUtils.readDirRecSync("./assets/layers") | ||||
|     .filter(path => path.indexOf(".json") > 0) | ||||
|     .filter(path => path.indexOf("license_info.json") < 0) | ||||
|     .map(path => { | ||||
|         try { | ||||
|             const parsed = JSON.parse(readFileSync(path, "UTF8")); | ||||
|             return {parsed: parsed, path: path} | ||||
|         } catch (e) { | ||||
|             console.error("Could not parse file ", "./assets/layers/" + path, "due to ", e) | ||||
|         } | ||||
|     }) | ||||
| const themeFiles: any[] = ScriptUtils.readDirRecSync("./assets/themes") | ||||
|     .filter(path => path.endsWith(".json")) | ||||
|     .filter(path => path.indexOf("license_info.json") < 0) | ||||
|     .map(path => { | ||||
|         try{ | ||||
|             return JSON.parse(readFileSync(path, "UTF8")); | ||||
|         }catch(e){ | ||||
|             console.error("Could not read file ", path, "due to ", e) | ||||
|             throw e | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
| console.log("Discovered", layerFiles.length, "layers and", themeFiles.length, "themes\n") | ||||
| return { | ||||
|     layers: layerFiles, | ||||
|     themes: themeFiles | ||||
| } | ||||
| } | ||||
| 
 | ||||
| function writeFiles(lt: LayersAndThemes){ | ||||
|     writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({ | ||||
|         "layers": lt.layers.map(l => l.parsed), | ||||
|         "themes": lt.themes | ||||
|     })) | ||||
| } | ||||
| 
 | ||||
| function validateLayer(layerJson: LayerConfigJson, path: string, knownPaths: Set<string>, context?: string): string[] { | ||||
|     let errorCount = []; | ||||
|     if (layerJson["overpassTags"] !== undefined) { | ||||
|         errorCount.push("Layer " + layerJson.id + "still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)") | ||||
|     } | ||||
|     try { | ||||
|         const layer = new LayerConfig(layerJson, "test", true) | ||||
|         const images = Array.from(layer.ExtractImages()) | ||||
|         const remoteImages = images.filter(img => img.indexOf("http") == 0) | ||||
|         for (const remoteImage of remoteImages) { | ||||
|             errorCount.push("Found a remote image: " + remoteImage + " in layer " + layer.id + ", please download it. You can use the fixTheme script to automate this") | ||||
|         } | ||||
|         const expected: string = `assets/layers/${layer.id}/${layer.id}.json` | ||||
|         if (path != undefined && path.indexOf(expected) < 0) { | ||||
|             errorCount.push("Layer is in an incorrect place. The path is " + path + ", but expected " + expected) | ||||
|         } | ||||
| 
 | ||||
|         for (const image of images) { | ||||
|             if(image.indexOf("{") >= 0){ | ||||
|                 console.warn("Ignoring image with { in the path: ", image) | ||||
|                 continue | ||||
|             } | ||||
|              | ||||
|             if (!knownPaths.has(image)) { | ||||
|                 const ctx = context === undefined ? "" : ` in a layer defined in the theme ${context}` | ||||
|                 errorCount.push(`Image with path ${image} not found or not attributed; it is used in ${layer.id}${ctx}`) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } catch (e) { | ||||
|         console.error(e) | ||||
|         return [`Layer ${layerJson.id}` ?? JSON.stringify(layerJson).substring(0, 50) + " is invalid: " + e] | ||||
|     } | ||||
|     return errorCount | ||||
| } | ||||
| 
 | ||||
| function validateTranslationCompletenessOfObject(object: any, expectedLanguages: string[], context: string) { | ||||
|     const missingTranlations = [] | ||||
|     const translations: {tr: Translation, context: string}[] = []; | ||||
|     const queue: {object: any, context: string}[] = [{object: object, context: context}] | ||||
| 
 | ||||
|     while (queue.length > 0) { | ||||
|         const item = queue.pop(); | ||||
|         const o = item.object | ||||
|         for (const key in o) { | ||||
|             const v = o[key]; | ||||
|             if (v === undefined) { | ||||
|                 continue; | ||||
|             } | ||||
|             if (v instanceof Translation || v?.translations !== undefined) { | ||||
|                 translations.push({tr: v, context: item.context}); | ||||
|             } else if ( | ||||
|                 ["string", "function", "boolean", "number"].indexOf(typeof (v)) < 0) { | ||||
|                 queue.push({object: v, context: item.context + "." + key}) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     const missing = {} | ||||
|     const present = {} | ||||
|     for (const ln of expectedLanguages) { | ||||
|         missing[ln] = 0; | ||||
|         present[ln] = 0; | ||||
|         for (const translation of translations) { | ||||
|             if (translation.tr.translations["*"] !== undefined) { | ||||
|                 continue; | ||||
|             } | ||||
|             const txt = translation.tr.translations[ln]; | ||||
|             const isMissing = txt === undefined || txt === "" || txt.toLowerCase().indexOf("todo") >= 0; | ||||
|             if (isMissing) { | ||||
|                 missingTranlations.push(`${translation.context},${ln},${translation.tr.txt}`) | ||||
|                 missing[ln]++ | ||||
|             } else { | ||||
|                 present[ln]++; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     let message = `Translation completeness for ${context}` | ||||
|     let isComplete = true; | ||||
|     for (const ln of expectedLanguages) { | ||||
|         const amiss = missing[ln]; | ||||
|         const ok = present[ln]; | ||||
|         const total = amiss + ok; | ||||
|         message += ` ${ln}: ${ok}/${total}` | ||||
|         if (ok !== total) { | ||||
|             isComplete = false; | ||||
|         } | ||||
|     } | ||||
|     return missingTranlations | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| function main(args: string[]) { | ||||
|      | ||||
|     const lt = loadThemesAndLayers(); | ||||
|     const layerFiles = lt.layers; | ||||
|     const themeFiles = lt.themes; | ||||
|      | ||||
|     console.log("   ---------- VALIDATING ---------") | ||||
|     const licensePaths = [] | ||||
|     for (const i in licenses) { | ||||
|         licensePaths.push(licenses[i].path) | ||||
|     } | ||||
|     const knownPaths = new Set<string>(licensePaths) | ||||
| 
 | ||||
|     let layerErrorCount = [] | ||||
|     const knownLayerIds = new Map<string, LayerConfig>(); | ||||
|     for (const layerFile of layerFiles) { | ||||
| 
 | ||||
|         layerErrorCount.push(...validateLayer(layerFile.parsed, layerFile.path, knownPaths)) | ||||
|         knownLayerIds.set(layerFile.parsed.id, new LayerConfig(layerFile.parsed)) | ||||
|     } | ||||
| 
 | ||||
|     let themeErrorCount = [] | ||||
|     let missingTranslations = [] | ||||
|     for (const themeFile of themeFiles) { | ||||
|         if (typeof themeFile.language === "string") { | ||||
|             themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings") | ||||
|         } | ||||
|         for (const layer of themeFile.layers) { | ||||
|             if (typeof layer === "string") { | ||||
|                 if (!knownLayerIds.has(layer)) { | ||||
|                     themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`) | ||||
|                 } else { | ||||
|                     const layerConfig = knownLayerIds.get(layer); | ||||
|                     missingTranslations.push(...validateTranslationCompletenessOfObject(layerConfig, themeFile.language, "Layer " + layer)) | ||||
| export default class LayerOverviewUtils { | ||||
| 
 | ||||
|     getLayerFiles(): {parsed: LayerConfigJson, path: string}[] { | ||||
|         return ScriptUtils.readDirRecSync("./assets/layers") | ||||
|             .filter(path => path.indexOf(".json") > 0) | ||||
|             .filter(path => path.indexOf("license_info.json") < 0) | ||||
|             .map(path => { | ||||
|                 try { | ||||
|                     const parsed = JSON.parse(readFileSync(path, "UTF8")); | ||||
|                     return {parsed: parsed, path: path} | ||||
|                 } catch (e) { | ||||
|                     console.error("Could not parse file ", "./assets/layers/" + path, "due to ", e) | ||||
|                 } | ||||
|             } else { | ||||
|                 if (layer.builtin !== undefined) { | ||||
|                     if (!knownLayerIds.has(layer.builtin)) { | ||||
|                         themeErrorCount.push("Unknown layer id: " + layer.builtin + "(which uses inheritance)") | ||||
|             }) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     getThemeFiles() { | ||||
|         return ScriptUtils.readDirRecSync("./assets/themes") | ||||
|             .filter(path => path.endsWith(".json")) | ||||
|             .filter(path => path.indexOf("license_info.json") < 0) | ||||
|             .map(path => { | ||||
|                 try { | ||||
|                     return JSON.parse(readFileSync(path, "UTF8")); | ||||
|                 } catch (e) { | ||||
|                     console.error("Could not read file ", path, "due to ", e) | ||||
|                     throw e | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     loadThemesAndLayers(): LayersAndThemes { | ||||
| 
 | ||||
|         const layerFiles = this.getLayerFiles(); | ||||
| 
 | ||||
|         const themeFiles: any[] = this.getThemeFiles(); | ||||
| 
 | ||||
|         console.log("Discovered", layerFiles.length, "layers and", themeFiles.length, "themes\n") | ||||
|         return { | ||||
|             layers: layerFiles, | ||||
|             themes: themeFiles | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     writeFiles(lt: LayersAndThemes) { | ||||
|         writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({ | ||||
|             "layers": lt.layers.map(l => l.parsed), | ||||
|             "themes": lt.themes | ||||
|         })) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     validateLayer(layerJson: LayerConfigJson, path: string, knownPaths: Set<string>, context?: string): string[] { | ||||
|         let errorCount = []; | ||||
|         if (layerJson["overpassTags"] !== undefined) { | ||||
|             errorCount.push("Layer " + layerJson.id + "still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)") | ||||
|         } | ||||
|         try { | ||||
|             const layer = new LayerConfig(layerJson, "test", true) | ||||
|             const images = Array.from(layer.ExtractImages()) | ||||
|             const remoteImages = images.filter(img => img.indexOf("http") == 0) | ||||
|             for (const remoteImage of remoteImages) { | ||||
|                 errorCount.push("Found a remote image: " + remoteImage + " in layer " + layer.id + ", please download it. You can use the fixTheme script to automate this") | ||||
|             } | ||||
|             const expected: string = `assets/layers/${layer.id}/${layer.id}.json` | ||||
|             if (path != undefined && path.indexOf(expected) < 0) { | ||||
|                 errorCount.push("Layer is in an incorrect place. The path is " + path + ", but expected " + expected) | ||||
|             } | ||||
| 
 | ||||
|             for (const image of images) { | ||||
|                 if (image.indexOf("{") >= 0) { | ||||
|                     console.warn("Ignoring image with { in the path: ", image) | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 if (!knownPaths.has(image)) { | ||||
|                     const ctx = context === undefined ? "" : ` in a layer defined in the theme ${context}` | ||||
|                     errorCount.push(`Image with path ${image} not found or not attributed; it is used in ${layer.id}${ctx}`) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } catch (e) { | ||||
|             console.error(e) | ||||
|             return [`Layer ${layerJson.id}` ?? JSON.stringify(layerJson).substring(0, 50) + " is invalid: " + e] | ||||
|         } | ||||
|         return errorCount | ||||
|     } | ||||
| 
 | ||||
|     validateTranslationCompletenessOfObject(object: any, expectedLanguages: string[], context: string) { | ||||
|         const missingTranlations = [] | ||||
|         const translations: { tr: Translation, context: string }[] = []; | ||||
|         const queue: { object: any, context: string }[] = [{object: object, context: context}] | ||||
| 
 | ||||
|         while (queue.length > 0) { | ||||
|             const item = queue.pop(); | ||||
|             const o = item.object | ||||
|             for (const key in o) { | ||||
|                 const v = o[key]; | ||||
|                 if (v === undefined) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (v instanceof Translation || v?.translations !== undefined) { | ||||
|                     translations.push({tr: v, context: item.context}); | ||||
|                 } else if ( | ||||
|                     ["string", "function", "boolean", "number"].indexOf(typeof (v)) < 0) { | ||||
|                     queue.push({object: v, context: item.context + "." + key}) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const missing = {} | ||||
|         const present = {} | ||||
|         for (const ln of expectedLanguages) { | ||||
|             missing[ln] = 0; | ||||
|             present[ln] = 0; | ||||
|             for (const translation of translations) { | ||||
|                 if (translation.tr.translations["*"] !== undefined) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 const txt = translation.tr.translations[ln]; | ||||
|                 const isMissing = txt === undefined || txt === "" || txt.toLowerCase().indexOf("todo") >= 0; | ||||
|                 if (isMissing) { | ||||
|                     missingTranlations.push(`${translation.context},${ln},${translation.tr.txt}`) | ||||
|                     missing[ln]++ | ||||
|                 } else { | ||||
|                     present[ln]++; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let message = `Translation completeness for ${context}` | ||||
|         let isComplete = true; | ||||
|         for (const ln of expectedLanguages) { | ||||
|             const amiss = missing[ln]; | ||||
|             const ok = present[ln]; | ||||
|             const total = amiss + ok; | ||||
|             message += ` ${ln}: ${ok}/${total}` | ||||
|             if (ok !== total) { | ||||
|                 isComplete = false; | ||||
|             } | ||||
|         } | ||||
|         return missingTranlations | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     main(args: string[]) { | ||||
| 
 | ||||
|         const lt = this.loadThemesAndLayers(); | ||||
|         const layerFiles = lt.layers; | ||||
|         const themeFiles = lt.themes; | ||||
| 
 | ||||
|         console.log("   ---------- VALIDATING ---------") | ||||
|         const licensePaths = [] | ||||
|         for (const i in licenses) { | ||||
|             licensePaths.push(licenses[i].path) | ||||
|         } | ||||
|         const knownPaths = new Set<string>(licensePaths) | ||||
| 
 | ||||
|         let layerErrorCount = [] | ||||
|         const knownLayerIds = new Map<string, LayerConfig>(); | ||||
|         for (const layerFile of layerFiles) { | ||||
| 
 | ||||
|             layerErrorCount.push(...this.validateLayer(layerFile.parsed, layerFile.path, knownPaths)) | ||||
|             knownLayerIds.set(layerFile.parsed.id, new LayerConfig(layerFile.parsed)) | ||||
|         } | ||||
| 
 | ||||
|         let themeErrorCount = [] | ||||
|         let missingTranslations = [] | ||||
|         for (const themeFile of themeFiles) { | ||||
|             if (typeof themeFile.language === "string") { | ||||
|                 themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings") | ||||
|             } | ||||
|             for (const layer of themeFile.layers) { | ||||
|                 if (typeof layer === "string") { | ||||
|                     if (!knownLayerIds.has(layer)) { | ||||
|                         themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`) | ||||
|                     } else { | ||||
|                         const layerConfig = knownLayerIds.get(layer); | ||||
|                         missingTranslations.push(...this.validateTranslationCompletenessOfObject(layerConfig, themeFile.language, "Layer " + layer)) | ||||
| 
 | ||||
|                     } | ||||
|                 } else { | ||||
|                     // layer.builtin contains layer overrides - we can skip those
 | ||||
|                     layerErrorCount.push(...validateLayer(layer, undefined, knownPaths, themeFile.id)) | ||||
|                     if (layer.builtin !== undefined) { | ||||
|                         if (!knownLayerIds.has(layer.builtin)) { | ||||
|                             themeErrorCount.push("Unknown layer id: " + layer.builtin + "(which uses inheritance)") | ||||
|                         } | ||||
|                     } else { | ||||
|                         // layer.builtin contains layer overrides - we can skip those
 | ||||
|                         layerErrorCount.push(...this.validateLayer(layer, undefined, knownPaths, themeFile.id)) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         themeFile.layers = themeFile.layers | ||||
|             .filter(l => typeof l != "string") // We remove all the builtin layer references as they don't work with ts-node for some weird reason
 | ||||
|             .filter(l => l.builtin === undefined) | ||||
|             themeFile.layers = themeFile.layers | ||||
|                 .filter(l => typeof l != "string") // We remove all the builtin layer references as they don't work with ts-node for some weird reason
 | ||||
|                 .filter(l => l.builtin === undefined) | ||||
| 
 | ||||
|         missingTranslations.push(...validateTranslationCompletenessOfObject(themeFile, themeFile.language, "Theme " + themeFile.id)) | ||||
|             missingTranslations.push(...this.validateTranslationCompletenessOfObject(themeFile, themeFile.language, "Theme " + themeFile.id)) | ||||
| 
 | ||||
|         try { | ||||
|             const theme = new LayoutConfig(themeFile, true, "test") | ||||
|             if (theme.id !== theme.id.toLowerCase()) { | ||||
|                 themeErrorCount.push("Theme ids should be in lowercase, but it is " + theme.id) | ||||
|             try { | ||||
|                 const theme = new LayoutConfig(themeFile, true, "test") | ||||
|                 if (theme.id !== theme.id.toLowerCase()) { | ||||
|                     themeErrorCount.push("Theme ids should be in lowercase, but it is " + theme.id) | ||||
|                 } | ||||
|             } catch (e) { | ||||
|                 themeErrorCount.push("Could not parse theme " + themeFile["id"] + "due to", e) | ||||
|             } | ||||
|         } catch (e) { | ||||
|             themeErrorCount.push("Could not parse theme " + themeFile["id"] + "due to", e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if(missingTranslations.length > 0){ | ||||
|         console.log(missingTranslations.length, "missing translations") | ||||
|         writeFileSync("missing_translations.txt", missingTranslations.join("\n")) | ||||
|     } | ||||
|      | ||||
|     if (layerErrorCount.length + themeErrorCount.length == 0) { | ||||
|         console.log("All good!") | ||||
|          | ||||
|         // We load again from disc, as modifications were made above
 | ||||
|         const lt = loadThemesAndLayers(); | ||||
|         writeFiles(lt); | ||||
|     } else { | ||||
|         const errors = layerErrorCount.concat(themeErrorCount).join("\n") | ||||
|         console.log(errors) | ||||
|         const msg = (`Found ${layerErrorCount.length} errors in the layers; ${themeErrorCount.length} errors in the themes`) | ||||
|         console.log(msg) | ||||
|         if (process.argv.indexOf("--report") >= 0) { | ||||
|             console.log("Writing report!") | ||||
|             writeFileSync("layer_report.txt", errors) | ||||
|         } | ||||
| 
 | ||||
|         if (process.argv.indexOf("--no-fail") < 0) { | ||||
|             throw msg; | ||||
|         if (missingTranslations.length > 0) { | ||||
|             console.log(missingTranslations.length, "missing translations") | ||||
|             writeFileSync("missing_translations.txt", missingTranslations.join("\n")) | ||||
|         } | ||||
| 
 | ||||
|         if (layerErrorCount.length + themeErrorCount.length == 0) { | ||||
|             console.log("All good!") | ||||
| 
 | ||||
|             // We load again from disc, as modifications were made above
 | ||||
|             const lt = this.loadThemesAndLayers(); | ||||
|             this.writeFiles(lt); | ||||
|         } else { | ||||
|             const errors = layerErrorCount.concat(themeErrorCount).join("\n") | ||||
|             console.log(errors) | ||||
|             const msg = (`Found ${layerErrorCount.length} errors in the layers; ${themeErrorCount.length} errors in the themes`) | ||||
|             console.log(msg) | ||||
|             if (process.argv.indexOf("--report") >= 0) { | ||||
|                 console.log("Writing report!") | ||||
|                 writeFileSync("layer_report.txt", errors) | ||||
|             } | ||||
| 
 | ||||
|             if (process.argv.indexOf("--no-fail") < 0) { | ||||
|                 throw msg; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| main(process.argv) | ||||
| new LayerOverviewUtils().main(process.argv) | ||||
|  | @ -2,40 +2,130 @@ import * as fs from "fs"; | |||
| import {Utils} from "../Utils"; | ||||
| import ScriptUtils from "./ScriptUtils"; | ||||
| import {readFileSync, writeFileSync} from "fs"; | ||||
| import LayerConfig from "../Customizations/JSON/LayerConfig"; | ||||
| import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson"; | ||||
| import * as bookcases from "../assets/layers/public_bookcase/public_bookcase.json" | ||||
| import LayerOverviewUtils from "./generateLayerOverview"; | ||||
| 
 | ||||
| const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"]; | ||||
| 
 | ||||
| class TranslationPart { | ||||
| 
 | ||||
|     contents: Map<string, TranslationPart | string> = new Map<string, TranslationPart | string>() | ||||
| 
 | ||||
|     add(language: string, obj: any){ | ||||
|     add(language: string, obj: any) { | ||||
|         for (const key in obj) { | ||||
|             const v = obj[key] | ||||
|             if(!this.contents.has(key)){ | ||||
|             if (!this.contents.has(key)) { | ||||
|                 this.contents.set(key, new TranslationPart()) | ||||
|             } | ||||
|             const subpart = this.contents.get(key) as TranslationPart | ||||
| 
 | ||||
|             if(typeof v === "string"){ | ||||
|             if (typeof v === "string") { | ||||
|                 subpart.contents.set(language, v) | ||||
|             }else{ | ||||
|             } else { | ||||
|                 subpart.add(language, v) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     toJson(): string{ | ||||
|         const parts = [] | ||||
|         for (let key of Array.from(this.contents.keys()) ){ | ||||
|             const value = this.contents.get(key); | ||||
|     addTranslationObject(translations: any, context?: string) { | ||||
|         for (const translationsKey in translations) { | ||||
|             if (!translations.hasOwnProperty(translationsKey)) { | ||||
|                 continue; | ||||
|             } | ||||
|             const v = translations[translationsKey] | ||||
|             if (typeof (v) != "string") { | ||||
|                 console.error("Non-string object in translation: ", translations[translationsKey]) | ||||
|                 throw "Error in an object depicting a translation: a non-string object was found. (" + context + ")\n    You probably put some other section accidentally in the translation" | ||||
|             } | ||||
|             this.contents.set(translationsKey, v) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|             if(typeof  value === "string"){ | ||||
|                 parts.push(`\"${key}\": \"${value}\"`) | ||||
|             }else{ | ||||
|                 parts.push(`\"${key}\": ${(value as TranslationPart).toJson()}`); | ||||
|     isLeaf() { | ||||
|         for (let key of Array.from(this.contents.keys())) { | ||||
|             const value = this.contents.get(key); | ||||
|             if (typeof value !== "string") { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         return JSON.stringify(JSON.parse(`{${parts.join(",")}}`), null, "    "); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     recursiveAdd(object: any) { | ||||
| 
 | ||||
| 
 | ||||
|         const isProbablyTranslationObject = knownLanguages.map(l => object.hasOwnProperty(l)).filter(x => x).length > 0; | ||||
|         if (isProbablyTranslationObject) { | ||||
|             this.addTranslationObject(object) | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         for (const key in object) { | ||||
|             if (!object.hasOwnProperty(key)) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const v = object[key] | ||||
|             if (v == null) { | ||||
|                 console.warn("Got a null value for key ", key) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             if (typeof v !== "object") { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (!this.contents.get(key)) { | ||||
|                 this.contents.set(key, new TranslationPart()) | ||||
|             } | ||||
| 
 | ||||
|             (this.contents.get(key) as TranslationPart).recursiveAdd(v); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     knownLanguages(): string[] { | ||||
|         const languages = [] | ||||
|         for (let key of Array.from(this.contents.keys())) { | ||||
|             const value = this.contents.get(key); | ||||
| 
 | ||||
|             if (typeof value === "string") { | ||||
|                 languages.push(key) | ||||
|             } else { | ||||
|                 languages.push(...(value as TranslationPart).knownLanguages()) | ||||
|             } | ||||
|         } | ||||
|         return Utils.Dedup(languages); | ||||
|     } | ||||
| 
 | ||||
|     toJson(neededLanguage?: string): string { | ||||
|         const parts = [] | ||||
| 
 | ||||
|         for (let key of Array.from(this.contents.keys())) { | ||||
|             let value = this.contents.get(key); | ||||
| 
 | ||||
|             if (typeof value === "string") { | ||||
|                 value = value.replace(/"/g, "\\\"") | ||||
|                 if(neededLanguage === undefined){ | ||||
|                     parts.push(`\"${key}\": \"${value}\"`) | ||||
|                 }else if (key === neededLanguage){ | ||||
|                    return `"${value}"` | ||||
|                 } | ||||
|                 | ||||
|             } else { | ||||
|                 const sub = (value as TranslationPart).toJson(neededLanguage) | ||||
|                 if (sub !== "") { | ||||
|                     parts.push(`\"${key}\": ${sub}`); | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
|         if (parts.length === 0) { | ||||
|             return ""; | ||||
|         } | ||||
|         return `{${parts.join(",")}}`; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -57,11 +147,11 @@ function transformTranslation(obj: any, depth = 1) { | |||
| 
 | ||||
|     let values = "" | ||||
|     for (const key in obj) { | ||||
|         if(key === "#"){ | ||||
|         if (key === "#") { | ||||
|             continue; | ||||
|         } | ||||
|         if(key.match("^[a-zA-Z0-9_]*$") === null){ | ||||
|             throw "Invalid character in key: "+key | ||||
|         if (key.match("^[a-zA-Z0-9_]*$") === null) { | ||||
|             throw "Invalid character in key: " + key | ||||
|         } | ||||
|         values += (Utils.Times((_) => "  ", depth)) + key + ": " + transformTranslation(obj[key], depth + 1) + ",\n" | ||||
|     } | ||||
|  | @ -83,7 +173,7 @@ function genTranslations() { | |||
| } | ||||
| 
 | ||||
| // Read 'lang/*.json', writes to 'assets/generated/translations.json'
 | ||||
| function compileTranslationsFromWeblate(){ | ||||
| function compileTranslationsFromWeblate() { | ||||
|     const translations = ScriptUtils.readDirRecSync("./langs") | ||||
|         .filter(path => path.indexOf(".json") > 0) | ||||
| 
 | ||||
|  | @ -92,13 +182,48 @@ function compileTranslationsFromWeblate(){ | |||
|     for (const translationFile of translations) { | ||||
|         const contents = JSON.parse(readFileSync(translationFile, "utf-8")); | ||||
|         let language = translationFile.substring(translationFile.lastIndexOf("/") + 1) | ||||
|         language = language.substring(0, language.length-5) | ||||
|         language = language.substring(0, language.length - 5) | ||||
|         allTranslations.add(language, contents) | ||||
|     } | ||||
| 
 | ||||
|     writeFileSync("./assets/generated/translations.json", allTranslations.toJson()) | ||||
|     writeFileSync("./assets/generated/translations.json", JSON.stringify(JSON.parse(allTranslations.toJson()), null, "    ")) | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| function generateTranslationFromLayerConfig(layerConfig: LayerConfigJson): TranslationPart { | ||||
|     const tr = new TranslationPart(); | ||||
|     tr.recursiveAdd(layerConfig) | ||||
|     return tr; | ||||
| } | ||||
| 
 | ||||
| function generateLayerTranslationsObject() { | ||||
|     const layerFiles = new LayerOverviewUtils().getLayerFiles(); | ||||
| 
 | ||||
|     const tr = new TranslationPart(); | ||||
| 
 | ||||
|     for (const layerFile of layerFiles) { | ||||
|         const config: LayerConfigJson = layerFile.parsed; | ||||
|         const layerTr = generateTranslationFromLayerConfig(config) | ||||
|         tr.contents.set(config.id, layerTr) | ||||
|     } | ||||
| 
 | ||||
|     const langs = tr.knownLanguages(); | ||||
|     for (const lang of langs) { | ||||
|         console.log("Exporting ", lang) | ||||
|          | ||||
|         let json = tr.toJson(lang) | ||||
|         try{ | ||||
|             json = JSON.stringify(JSON.parse(json), null, "    "); | ||||
|         }catch (e) { | ||||
|             console.error(e) | ||||
|         } | ||||
|          | ||||
|         writeFileSync("langs/layers/" + lang + ".json", json) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| generateLayerTranslationsObject() | ||||
| 
 | ||||
| compileTranslationsFromWeblate(); | ||||
| genTranslations() | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue