Fix: fix validation of question input; remove some obsolete logging; stabilize output of generate:translations wrt newlines

This commit is contained in:
Pieter Vander Vennet 2023-05-03 00:57:15 +02:00
parent 755f905c36
commit 1f39ba9ab5
26 changed files with 7328 additions and 71 deletions

View file

@ -15,7 +15,6 @@ export default class BBoxFeatureSource extends StaticFeatureSource {
if (mustTouch.data === undefined) { if (mustTouch.data === undefined) {
return features return features
} }
console.log("UPdating touching bbox")
const box = mustTouch.data const box = mustTouch.data
return features.filter((feature) => { return features.filter((feature) => {
if (feature.geometry.type === "Point") { if (feature.geometry.type === "Point") {

View file

@ -1,22 +1,20 @@
import { Translation, TypedTranslation } from "../../UI/i18n/Translation" import {Translation, TypedTranslation} from "../../UI/i18n/Translation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter" import {TagsFilter} from "../../Logic/Tags/TagsFilter"
import Translations from "../../UI/i18n/Translations" import Translations from "../../UI/i18n/Translations"
import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils" import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And" import {And} from "../../Logic/Tags/And"
import { Utils } from "../../Utils" import {Utils} from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag" import {Tag} from "../../Logic/Tags/Tag"
import BaseUIElement from "../../UI/BaseUIElement" import BaseUIElement from "../../UI/BaseUIElement"
import Combine from "../../UI/Base/Combine" import Combine from "../../UI/Base/Combine"
import Title from "../../UI/Base/Title" import Title from "../../UI/Base/Title"
import Link from "../../UI/Base/Link" import Link from "../../UI/Base/Link"
import List from "../../UI/Base/List" import List from "../../UI/Base/List"
import { import {MappingConfigJson, QuestionableTagRenderingConfigJson,} from "./Json/QuestionableTagRenderingConfigJson"
MappingConfigJson, import {FixedUiElement} from "../../UI/Base/FixedUiElement"
QuestionableTagRenderingConfigJson, import {Paragraph} from "../../UI/Base/Paragraph"
} from "./Json/QuestionableTagRenderingConfigJson"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import { Paragraph } from "../../UI/Base/Paragraph"
import Svg from "../../Svg" import Svg from "../../Svg"
import Validators, {ValidatorType} from "../../UI/InputElement/Validators";
export interface Mapping { export interface Mapping {
readonly if: UploadableTag readonly if: UploadableTag
@ -623,13 +621,19 @@ export default class TagRenderingConfig {
* *
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform * @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
* @param multiSelectedMapping (Only used if multiAnswer == true): all the mappings that must be applied. Set multiSelectedMapping[mappings.length] to use the freeform as well * @param multiSelectedMapping (Only used if multiAnswer == true): all the mappings that must be applied. Set multiSelectedMapping[mappings.length] to use the freeform as well
* @param currentProperties: The current properties of the object for which the question should be answered
*/ */
public constructChangeSpecification( public constructChangeSpecification(
freeformValue: string | undefined, freeformValue: string | undefined,
singleSelectedMapping: number, singleSelectedMapping: number,
multiSelectedMapping: boolean[] | undefined multiSelectedMapping: boolean[] | undefined,
currentProperties: Record<string, string>
): UploadableTag { ): UploadableTag {
freeformValue = freeformValue?.trim() freeformValue = freeformValue?.trim()
const validator = Validators.get(<ValidatorType> this.freeform?.type)
if(validator && freeformValue){
freeformValue = validator.reformat(freeformValue,() => currentProperties["_country"])
}
if (freeformValue === "") { if (freeformValue === "") {
freeformValue = undefined freeformValue = undefined
} }
@ -666,7 +670,7 @@ export default class TagRenderingConfig {
.filter((_, i) => !multiSelectedMapping[i]) .filter((_, i) => !multiSelectedMapping[i])
.map((m) => m.ifnot) .map((m) => m.ifnot)
if (multiSelectedMapping.at(-1)) { if (multiSelectedMapping.at(-1) && this.freeform) {
// The freeform value was selected as well // The freeform value was selected as well
selectedMappings.push( selectedMappings.push(
new And([ new And([
@ -677,22 +681,29 @@ export default class TagRenderingConfig {
} }
return TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings]) return TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
} else { } else {
if (singleSelectedMapping === undefined) { // Is at least one mapping shown in the answer?
return undefined const someMappingIsShown = this.mappings.some(m => {
} if(typeof m.hideInAnswer === "boolean"){
if (singleSelectedMapping === this.mappings.length) { return !m.hideInAnswer
if (freeformValue === undefined) {
return undefined
} }
const isHidden = m.hideInAnswer.matchesProperties(currentProperties)
return !isHidden
} )
// If all mappings are hidden for the current tags, we can safely assume that we should use the freeform key
const useFreeform = freeformValue !== undefined && (singleSelectedMapping === this.mappings.length || !someMappingIsShown)
if (useFreeform) {
return new And([ return new And([
new Tag(this.freeform.key, freeformValue), new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []), ...(this.freeform.addExtraTags ?? []),
]) ])
} else { } else if(singleSelectedMapping) {
return new And([ return new And([
this.mappings[singleSelectedMapping].if, this.mappings[singleSelectedMapping].if,
...(this.mappings[singleSelectedMapping].addExtraTags ?? []), ...(this.mappings[singleSelectedMapping].addExtraTags ?? []),
]) ])
}else{
console.log("TagRenderingConfig.ConstructSpecification has a weird fallback for", {freeformValue, singleSelectedMapping, multiSelectedMapping, currentProperties})
return undefined
} }
} }
} }

View file

@ -296,7 +296,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
) )
this.initActors() this.initActors()
this.drawSpecialLayers(lastClick) this.addLastClick(lastClick)
this.drawSpecialLayers()
this.initHotkeys() this.initHotkeys()
this.miscSetup() this.miscSetup()
console.log("State setup completed", this) console.log("State setup completed", this)
@ -424,10 +425,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
/** /**
* Add the special layers to the map * Add the special layers to the map
*/ */
private drawSpecialLayers(last_click: LastClickFeatureSource) { private drawSpecialLayers() {
type AddedByDefaultTypes = typeof Constants.added_by_default[number] type AddedByDefaultTypes = typeof Constants.added_by_default[number]
const empty = [] const empty = []
this.addLastClick(last_click)
/** /**
* A listing which maps the layerId onto the featureSource * A listing which maps the layerId onto the featureSource
*/ */

View file

@ -6,29 +6,43 @@
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"; import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Translation } from "../i18n/Translation"; import { Translation } from "../i18n/Translation";
import { createEventDispatcher, onDestroy } from "svelte"; import { createEventDispatcher, onDestroy } from "svelte";
import {Validator} from "./Validator";
export let value: UIEventSource<string>; export let value: UIEventSource<string>;
// Internal state, only copied to 'value' so that no invalid values leak outside // Internal state, only copied to 'value' so that no invalid values leak outside
let _value = new UIEventSource(value.data ?? ""); let _value = new UIEventSource(value.data ?? "");
onDestroy(value.addCallbackAndRunD(v => _value.setData(v ?? ""))); onDestroy(value.addCallbackAndRunD(v => _value.setData(v ?? "")));
export let type: ValidatorType; export let type: ValidatorType;
let validator = Validators.get(type);
export let feedback: UIEventSource<Translation> | undefined = undefined; export let feedback: UIEventSource<Translation> | undefined = undefined;
export let getCountry: () => string | undefined
let validator : Validator = Validators.get(type)
$: {
// The type changed -> reset some values
validator = Validators.get(type)
_value.setData("")
feedback = feedback?.setData(validator?.getFeedback(_value.data, getCountry));
}
onDestroy(value.addCallbackAndRun(v => {
if(v === undefined || v === ""){
_value.setData("")
}
}))
onDestroy(_value.addCallbackAndRun(v => { onDestroy(_value.addCallbackAndRun(v => {
if (validator.isValid(v)) { if (validator.isValid(v, getCountry)) {
feedback?.setData(undefined); feedback?.setData(undefined);
value.setData(v); value.setData(v);
return; return;
} }
value.setData(undefined); value.setData(undefined);
feedback?.setData(validator.getFeedback(v)); feedback?.setData(validator.getFeedback(v, getCountry));
})) }))
if (validator === undefined) { if (validator === undefined) {
throw "Not a valid type for a validator:" + type; throw "Not a valid type for a validator:" + type;
} }
const isValid = _value.map(v => validator.isValid(v)); const isValid = _value.map(v => validator.isValid(v, getCountry));
let htmlElem: HTMLInputElement; let htmlElem: HTMLInputElement;

View file

@ -44,9 +44,8 @@ export abstract class Validator {
/** /**
* Gets a piece of feedback. By default, validation.<type> will be used, resulting in a generic 'not a valid <type>'. * Gets a piece of feedback. By default, validation.<type> will be used, resulting in a generic 'not a valid <type>'.
* However, inheritors might overwrite this to give more specific feedback * However, inheritors might overwrite this to give more specific feedback
* @param s
*/ */
public getFeedback(s: string): Translation { public getFeedback(s: string, requestCountry?: () => string): Translation {
const tr = Translations.t.validation[this.name] const tr = Translations.t.validation[this.name]
if (tr !== undefined) { if (tr !== undefined) {
return tr["feedback"] return tr["feedback"]

View file

@ -1,7 +1,8 @@
import { Translation } from "../../i18n/Translation.js" import {Translation} from "../../i18n/Translation.js"
import Translations from "../../i18n/Translations.js" import Translations from "../../i18n/Translations.js"
import * as emailValidatorLibrary from "email-validator" import * as emailValidatorLibrary from "email-validator"
import { Validator } from "../Validator" import {Validator} from "../Validator"
export default class EmailValidator extends Validator { export default class EmailValidator extends Validator {
constructor() { constructor() {
super("email", "An email adress", "email") super("email", "An email adress", "email")

View file

@ -1,12 +1,28 @@
import { parsePhoneNumberFromString } from "libphonenumber-js" import {parsePhoneNumberFromString} from "libphonenumber-js"
import { Validator } from "../Validator" import {Validator} from "../Validator"
import {Translation} from "../../i18n/Translation";
import Translations from "../../i18n/Translations";
export default class PhoneValidator extends Validator { export default class PhoneValidator extends Validator {
constructor() { constructor() {
super("phone", "A phone number", "tel") super("phone", "A phone number", "tel")
} }
isValid(str, country: () => string): boolean {
getFeedback(s: string, requestCountry?: () => string): Translation {
const tr = Translations.t.validation.phone
const generic = tr.feedback
if(requestCountry){
const country = requestCountry()
if(country){
return tr.feedbackCountry.Subs({country})
}
}
return generic
}
public isValid(str, country: () => string): boolean {
if (str === undefined) { if (str === undefined) {
return false return false
} }
@ -20,13 +36,17 @@ export default class PhoneValidator extends Validator {
return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false
} }
reformat = (str, country: () => string) => { public reformat(str, country: () => string) {
if (str.startsWith("tel:")) { if (str.startsWith("tel:")) {
str = str.substring("tel:".length) str = str.substring("tel:".length)
} }
let countryCode = undefined
if(country){
countryCode = country()
}
return parsePhoneNumberFromString( return parsePhoneNumberFromString(
str, str,
country()?.toUpperCase() as any countryCode?.toUpperCase() as any
)?.formatInternational() )?.formatInternational()
} }
} }

View file

@ -498,7 +498,7 @@ export class OH {
lat: tags._lat, lat: tags._lat,
lon: tags._lon, lon: tags._lon,
address: { address: {
country_code: tags._country.toLowerCase(), country_code: tags._country?.toLowerCase(),
state: undefined, state: undefined,
}, },
}, },

View file

@ -10,6 +10,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
import Table from "../Base/Table" import Table from "../Base/Table"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Loading from "../Base/Loading";
export default class OpeningHoursVisualization extends Toggle { export default class OpeningHoursVisualization extends Toggle {
private static readonly weekdays: Translation[] = [ private static readonly weekdays: Translation[] = [
@ -29,6 +30,7 @@ export default class OpeningHoursVisualization extends Toggle {
prefix = "", prefix = "",
postfix = "" postfix = ""
) { ) {
const country = tags.map(tags => tags._country)
const ohTable = new VariableUiElement( const ohTable = new VariableUiElement(
tags tags
.map((tags) => { .map((tags) => {
@ -66,12 +68,12 @@ export default class OpeningHoursVisualization extends Toggle {
), ),
]) ])
} }
}) }, [country])
) )
super( super(
ohTable, ohTable,
Translations.t.general.opening_hours.loadingCountry.Clone(), new Loading(Translations.t.general.opening_hours.loadingCountry),
tags.map((tgs) => tgs._country !== undefined) tags.map((tgs) => tgs._country !== undefined)
) )
} }
@ -160,7 +162,7 @@ export default class OpeningHoursVisualization extends Toggle {
const weekdayStyles = [] const weekdayStyles = []
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
const day = OpeningHoursVisualization.weekdays[i].Clone() const day = OpeningHoursVisualization.weekdays[i].Clone()
day.SetClass("w-full h-full block") day.SetClass("w-full h-full flex")
const rangesForDay = ranges[i].map((range) => const rangesForDay = ranges[i].map((range) =>
OpeningHoursVisualization.CreateRangeElem( OpeningHoursVisualization.CreateRangeElem(

View file

@ -177,6 +177,7 @@ export default class MoveWizard extends Toggle {
state.featureProperties.getStore(id).ping() state.featureProperties.getStore(id).ping()
currentStep.setData("moved") currentStep.setData("moved")
state.mapProperties.location.setData(loc)
}) })
const zoomInFurhter = t.zoomInFurther.SetClass("alert block m-6") const zoomInFurhter = t.zoomInFurther.SetClass("alert block m-6")
return new Combine([ return new Combine([

View file

@ -10,6 +10,8 @@ import Lazy from "../Base/Lazy"
import { OsmServiceState } from "../../Logic/Osm/OsmConnection" import { OsmServiceState } from "../../Logic/Osm/OsmConnection"
/** /**
* @deprecated
* This element is getting stripped and is not used anymore
* Generates all the questions, one by one * Generates all the questions, one by one
*/ */
export default class QuestionBox extends VariableUiElement { export default class QuestionBox extends VariableUiElement {

View file

@ -19,17 +19,20 @@
let dispatch = createEventDispatcher<{ "selected" }>(); let dispatch = createEventDispatcher<{ "selected" }>();
onDestroy(value.addCallbackD(() => {dispatch("selected")})) onDestroy(value.addCallbackD(() => {dispatch("selected")}))
function getCountry() {
return tags.data["_country"]
}
</script> </script>
<div class="inline-flex flex-col"> <div class="inline-flex flex-col">
{#if config.freeform.inline} {#if config.freeform.inline}
<Inline key={config.freeform.key} {tags} template={config.render}> <Inline key={config.freeform.key} {tags} template={config.render}>
<ValidatedInput {feedback} on:selected={() => dispatch("selected")} <ValidatedInput {feedback} {getCountry} on:selected={() => dispatch("selected")}
type={config.freeform.type} {value}></ValidatedInput> type={config.freeform.type} {value}></ValidatedInput>
</Inline> </Inline>
{:else} {:else}
<ValidatedInput {feedback} on:selected={() => dispatch("selected")} <ValidatedInput {feedback} {getCountry} on:selected={() => dispatch("selected")}
type={config.freeform.type} {value}></ValidatedInput> type={config.freeform.type} {value}></ValidatedInput>
{/if} {/if}

View file

@ -16,6 +16,7 @@
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"; import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
import SpecialTranslation from "./SpecialTranslation.svelte"; import SpecialTranslation from "./SpecialTranslation.svelte";
import TagHint from "../TagHint.svelte"; import TagHint from "../TagHint.svelte";
import Validators from "../../InputElement/Validators";
export let config: TagRenderingConfig; export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>; export let tags: UIEventSource<Record<string, string>>;
@ -34,9 +35,12 @@
let selectedMapping: number = undefined; let selectedMapping: number = undefined;
let checkedMappings: boolean[]; let checkedMappings: boolean[];
$: { $: {
// We received a new config -> reinit
console.log("Initing checkedMappings for", config)
if (config.mappings?.length > 0 && (checkedMappings === undefined || checkedMappings?.length < config.mappings.length)) { if (config.mappings?.length > 0 && (checkedMappings === undefined || checkedMappings?.length < config.mappings.length)) {
checkedMappings = [...config.mappings.map(_ => false), false /*One element extra in case a freeform value is added*/]; checkedMappings = [...config.mappings.map(_ => false), false /*One element extra in case a freeform value is added*/];
} }
freeformInput.setData(undefined)
} }
let selectedTags: TagsFilter = undefined; let selectedTags: TagsFilter = undefined;
@ -54,9 +58,10 @@
$: { $: {
mappings = config.mappings?.filter(m => !mappingIsHidden(m)); mappings = config.mappings?.filter(m => !mappingIsHidden(m));
try { try {
selectedTags = config?.constructChangeSpecification($freeformInput, selectedMapping, checkedMappings); let freeformInputValue = $freeformInput
selectedTags = config?.constructChangeSpecification(freeformInputValue, selectedMapping, checkedMappings, tags.data);
} catch (e) { } catch (e) {
console.debug("Could not calculate changeSpecification:", e); console.error("Could not calculate changeSpecification:", e);
selectedTags = undefined; selectedTags = undefined;
} }
} }
@ -99,7 +104,7 @@
} }
); );
freeformInput.setData(undefined); freeformInput.setData(undefined);
selectedMapping = 0; selectedMapping = undefined;
selectedTags = undefined; selectedTags = undefined;
change.CreateChangeDescriptions().then(changes => change.CreateChangeDescriptions().then(changes =>
@ -139,14 +144,14 @@
{#each config.mappings as mapping, i (mapping.then)} {#each config.mappings as mapping, i (mapping.then)}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices--> <!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
{#if !mappingIsHidden(mapping) } {#if !mappingIsHidden(mapping) }
<label> <label class="flex">
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id} value={i}> <input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id} value={i}>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer}></TagRenderingMapping> <TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer}></TagRenderingMapping>
</label> </label>
{/if} {/if}
{/each} {/each}
{#if config.freeform?.key} {#if config.freeform?.key}
<label> <label class="flex">
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id} <input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id}
value={config.mappings.length}> value={config.mappings.length}>
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput} <FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
@ -159,13 +164,14 @@
<div class="flex flex-col"> <div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)} {#each config.mappings as mapping, i (mapping.then)}
{#if !mappingIsHidden(mapping)} {#if !mappingIsHidden(mapping)}
<label> <label class="flex">
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+i} bind:checked={checkedMappings[i]}> <input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+i} bind:checked={checkedMappings[i]}>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement}></TagRenderingMapping> <TagRenderingMapping {mapping} {tags} {state} {selectedElement}></TagRenderingMapping>
</label>{/if} </label>
{/if}
{/each} {/each}
{#if config.freeform?.key} {#if config.freeform?.key}
<label> <label class="flex">
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+config.mappings.length} <input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+config.mappings.length}
bind:checked={checkedMappings[config.mappings.length]}> bind:checked={checkedMappings[config.mappings.length]}>
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput} <FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
@ -184,7 +190,7 @@
<Tr t={Translations.t.general.save}></Tr> <Tr t={Translations.t.general.save}></Tr>
</button> </button>
{:else } {:else }
<div class="w-6 h-6"> <div class="inline-flex w-6 h-6">
<!-- Invalid value; show an inactive button or something like that--> <!-- Invalid value; show an inactive button or something like that-->
<ExclamationIcon/> <ExclamationIcon/>
</div> </div>

View file

@ -29,6 +29,7 @@ import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"
import { OsmTags } from "../../Models/OsmFeature" import { OsmTags } from "../../Models/OsmFeature"
/** /**
* @deprecated: getting stripped and getting ported
* Shows the question element. * Shows the question element.
* Note that the value _migh_ already be known, e.g. when selected or when changing the value * Note that the value _migh_ already be known, e.g. when selected or when changing the value
*/ */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1006,7 +1006,8 @@
}, },
"phone": { "phone": {
"description": "a phone number", "description": "a phone number",
"feedback": "This is not a valid phone number" "feedback": "This is not a valid phone number",
"feedbackCountry": "This is not a valid phone number (for country {country})"
}, },
"pnat": { "pnat": {
"description": "a positive, whole number", "description": "a positive, whole number",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.6 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
import * as fs from "fs" import * as fs from "fs"
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" import {existsSync, mkdirSync, readFileSync, writeFileSync} from "fs"
import { Utils } from "../Utils" import {Utils} from "../Utils"
import ScriptUtils from "./ScriptUtils" import ScriptUtils from "./ScriptUtils"
const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"] const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"]
@ -12,7 +12,7 @@ class TranslationPart {
const files = ScriptUtils.readDirRecSync(path, 1).filter((file) => file.endsWith(".json")) const files = ScriptUtils.readDirRecSync(path, 1).filter((file) => file.endsWith(".json"))
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) rootTranslation.addTranslation(file.substr(0, file.length - ".json".length), content)
} }
return rootTranslation return rootTranslation
@ -52,10 +52,10 @@ class TranslationPart {
if (typeof v != "string") { if (typeof v != "string") {
console.error( console.error(
`Non-string object at ${context} in translation while trying to add the translation ` + `Non-string object at ${context} in translation while trying to add the translation ` +
JSON.stringify(v) + JSON.stringify(v) +
` to '` + ` to '` +
translationsKey + translationsKey +
"'. The offending object which _should_ be a translation is: ", "'. The offending object which _should_ be a translation is: ",
v, v,
"\n\nThe current object is (only showing en):", "\n\nThe current object is (only showing en):",
this.toJson(), this.toJson(),
@ -94,9 +94,9 @@ class TranslationPart {
if (noTranslate !== undefined) { if (noTranslate !== undefined) {
console.log( console.log(
"Ignoring some translations for " + "Ignoring some translations for " +
context + context +
": " + ": " +
dontTranslateKeys.join(", ") dontTranslateKeys.join(", ")
) )
} }
} }
@ -243,14 +243,14 @@ class TranslationPart {
} }
subparts = subparts.map((p) => p.split(/\(.*\)/)[0]) subparts = subparts.map((p) => p.split(/\(.*\)/)[0])
for (const subpart of subparts) { for (const subpart of subparts) {
neededSubparts.add({ part: subpart, usedByLanguage: lang }) neededSubparts.add({part: subpart, usedByLanguage: lang})
} }
} }
}) })
// Actually check for the needed sub-parts, e.g. that {key} isn't translated into {sleutel} // Actually check for the needed sub-parts, e.g. that {key} isn't translated into {sleutel}
this.contents.forEach((value, key) => { this.contents.forEach((value, key) => {
neededSubparts.forEach(({ part, usedByLanguage }) => { neededSubparts.forEach(({part, usedByLanguage}) => {
if (typeof value !== "string") { if (typeof value !== "string") {
return return
} }
@ -444,6 +444,7 @@ function removeEmptyString(object: object) {
} }
return object return object
} }
/** /**
* Formats the specified file, helps to prevent merge conflicts * Formats the specified file, helps to prevent merge conflicts
* */ * */
@ -659,7 +660,8 @@ function mergeLayerTranslations() {
const layerFiles = ScriptUtils.getLayerFiles() const layerFiles = ScriptUtils.getLayerFiles()
for (const layerFile of layerFiles) { for (const layerFile of layerFiles) {
mergeLayerTranslation(layerFile.parsed, layerFile.path, loadTranslationFilesFrom("layers")) mergeLayerTranslation(layerFile.parsed, layerFile.path, loadTranslationFilesFrom("layers"))
writeFileSync(layerFile.path, JSON.stringify(layerFile.parsed, null, " ")) // layers use 2 spaces const endsWithNewline = readFileSync(layerFile.path, {encoding: "utf8"})?.endsWith("\n") ?? true
writeFileSync(layerFile.path, JSON.stringify(layerFile.parsed, null, " ") + (endsWithNewline ? "\n" : "")) // layers use 2 spaces
} }
} }
@ -674,7 +676,8 @@ function mergeThemeTranslations() {
const allTranslations = new TranslationPart() const allTranslations = new TranslationPart()
allTranslations.recursiveAdd(config, themeFile.path) allTranslations.recursiveAdd(config, themeFile.path)
writeFileSync(themeFile.path, JSON.stringify(config, null, " ")) // Themefiles use 2 spaces const endsWithNewline = readFileSync(themeFile.path, {encoding: "utf8"})?.endsWith("\n") ?? true
writeFileSync(themeFile.path, JSON.stringify(config, null, " ") + (endsWithNewline ? "\n" : "")) // Themefiles use 2 spaces
} }
} }
@ -693,7 +696,8 @@ if (!themeOverwritesWeblate) {
questionsPath, questionsPath,
loadTranslationFilesFrom("shared-questions") loadTranslationFilesFrom("shared-questions")
) )
writeFileSync(questionsPath, JSON.stringify(questionsParsed, null, " ")) const endsWithNewline = readFileSync(questionsPath, {encoding: "utf8"}).endsWith("\n")
writeFileSync(questionsPath, JSON.stringify(questionsParsed, null, " ") + (endsWithNewline ? "\n" : ""))
} else { } else {
console.log("Ignore weblate") console.log("Ignore weblate")
} }
@ -704,13 +708,13 @@ const l2 = generateTranslationsObjectFrom(
"themes" "themes"
) )
const l3 = generateTranslationsObjectFrom( const l3 = generateTranslationsObjectFrom(
[{ path: questionsPath, parsed: questionsParsed }], [{path: questionsPath, parsed: questionsParsed}],
"shared-questions" "shared-questions"
) )
const usedLanguages: string[] = Utils.Dedup(l1.concat(l2).concat(l3)).filter((v) => v !== "*") const usedLanguages: string[] = Utils.Dedup(l1.concat(l2).concat(l3)).filter((v) => v !== "*")
usedLanguages.sort() usedLanguages.sort()
fs.writeFileSync("./assets/used_languages.json", JSON.stringify({ languages: usedLanguages })) fs.writeFileSync("./assets/used_languages.json", JSON.stringify({languages: usedLanguages}))
if (!themeOverwritesWeblate) { if (!themeOverwritesWeblate) {
// Generates the core translations // Generates the core translations