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) {
return features
}
console.log("UPdating touching bbox")
const box = mustTouch.data
return features.filter((feature) => {
if (feature.geometry.type === "Point") {

View file

@ -1,22 +1,20 @@
import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import {Translation, TypedTranslation} from "../../UI/i18n/Translation"
import {TagsFilter} from "../../Logic/Tags/TagsFilter"
import Translations from "../../UI/i18n/Translations"
import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils"
import {And} from "../../Logic/Tags/And"
import {Utils} from "../../Utils"
import {Tag} from "../../Logic/Tags/Tag"
import BaseUIElement from "../../UI/BaseUIElement"
import Combine from "../../UI/Base/Combine"
import Title from "../../UI/Base/Title"
import Link from "../../UI/Base/Link"
import List from "../../UI/Base/List"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "./Json/QuestionableTagRenderingConfigJson"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import { Paragraph } from "../../UI/Base/Paragraph"
import {MappingConfigJson, QuestionableTagRenderingConfigJson,} from "./Json/QuestionableTagRenderingConfigJson"
import {FixedUiElement} from "../../UI/Base/FixedUiElement"
import {Paragraph} from "../../UI/Base/Paragraph"
import Svg from "../../Svg"
import Validators, {ValidatorType} from "../../UI/InputElement/Validators";
export interface Mapping {
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 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(
freeformValue: string | undefined,
singleSelectedMapping: number,
multiSelectedMapping: boolean[] | undefined
multiSelectedMapping: boolean[] | undefined,
currentProperties: Record<string, string>
): UploadableTag {
freeformValue = freeformValue?.trim()
const validator = Validators.get(<ValidatorType> this.freeform?.type)
if(validator && freeformValue){
freeformValue = validator.reformat(freeformValue,() => currentProperties["_country"])
}
if (freeformValue === "") {
freeformValue = undefined
}
@ -666,7 +670,7 @@ export default class TagRenderingConfig {
.filter((_, i) => !multiSelectedMapping[i])
.map((m) => m.ifnot)
if (multiSelectedMapping.at(-1)) {
if (multiSelectedMapping.at(-1) && this.freeform) {
// The freeform value was selected as well
selectedMappings.push(
new And([
@ -677,22 +681,29 @@ export default class TagRenderingConfig {
}
return TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
} else {
if (singleSelectedMapping === undefined) {
return undefined
}
if (singleSelectedMapping === this.mappings.length) {
if (freeformValue === undefined) {
return undefined
// Is at least one mapping shown in the answer?
const someMappingIsShown = this.mappings.some(m => {
if(typeof m.hideInAnswer === "boolean"){
return !m.hideInAnswer
}
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([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []),
])
} else {
} else if(singleSelectedMapping) {
return new And([
this.mappings[singleSelectedMapping].if,
...(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.drawSpecialLayers(lastClick)
this.addLastClick(lastClick)
this.drawSpecialLayers()
this.initHotkeys()
this.miscSetup()
console.log("State setup completed", this)
@ -424,10 +425,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
/**
* Add the special layers to the map
*/
private drawSpecialLayers(last_click: LastClickFeatureSource) {
private drawSpecialLayers() {
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
const empty = []
this.addLastClick(last_click)
/**
* A listing which maps the layerId onto the featureSource
*/

View file

@ -6,29 +6,43 @@
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Translation } from "../i18n/Translation";
import { createEventDispatcher, onDestroy } from "svelte";
import {Validator} from "./Validator";
export let value: UIEventSource<string>;
// Internal state, only copied to 'value' so that no invalid values leak outside
let _value = new UIEventSource(value.data ?? "");
onDestroy(value.addCallbackAndRunD(v => _value.setData(v ?? "")));
export let type: ValidatorType;
let validator = Validators.get(type);
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 => {
if (validator.isValid(v)) {
if (validator.isValid(v, getCountry)) {
feedback?.setData(undefined);
value.setData(v);
return;
}
value.setData(undefined);
feedback?.setData(validator.getFeedback(v));
feedback?.setData(validator.getFeedback(v, getCountry));
}))
if (validator === undefined) {
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;

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>'.
* 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]
if (tr !== undefined) {
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 * as emailValidatorLibrary from "email-validator"
import { Validator } from "../Validator"
import {Validator} from "../Validator"
export default class EmailValidator extends Validator {
constructor() {
super("email", "An email adress", "email")

View file

@ -1,12 +1,28 @@
import { parsePhoneNumberFromString } from "libphonenumber-js"
import { Validator } from "../Validator"
import {parsePhoneNumberFromString} from "libphonenumber-js"
import {Validator} from "../Validator"
import {Translation} from "../../i18n/Translation";
import Translations from "../../i18n/Translations";
export default class PhoneValidator extends Validator {
constructor() {
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) {
return false
}
@ -20,13 +36,17 @@ export default class PhoneValidator extends Validator {
return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false
}
reformat = (str, country: () => string) => {
public reformat(str, country: () => string) {
if (str.startsWith("tel:")) {
str = str.substring("tel:".length)
}
let countryCode = undefined
if(country){
countryCode = country()
}
return parsePhoneNumberFromString(
str,
country()?.toUpperCase() as any
countryCode?.toUpperCase() as any
)?.formatInternational()
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
import SpecialTranslation from "./SpecialTranslation.svelte";
import TagHint from "../TagHint.svelte";
import Validators from "../../InputElement/Validators";
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
@ -34,9 +35,12 @@
let selectedMapping: number = undefined;
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)) {
checkedMappings = [...config.mappings.map(_ => false), false /*One element extra in case a freeform value is added*/];
}
freeformInput.setData(undefined)
}
let selectedTags: TagsFilter = undefined;
@ -54,9 +58,10 @@
$: {
mappings = config.mappings?.filter(m => !mappingIsHidden(m));
try {
selectedTags = config?.constructChangeSpecification($freeformInput, selectedMapping, checkedMappings);
let freeformInputValue = $freeformInput
selectedTags = config?.constructChangeSpecification(freeformInputValue, selectedMapping, checkedMappings, tags.data);
} catch (e) {
console.debug("Could not calculate changeSpecification:", e);
console.error("Could not calculate changeSpecification:", e);
selectedTags = undefined;
}
}
@ -99,7 +104,7 @@
}
);
freeformInput.setData(undefined);
selectedMapping = 0;
selectedMapping = undefined;
selectedTags = undefined;
change.CreateChangeDescriptions().then(changes =>
@ -139,14 +144,14 @@
{#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-->
{#if !mappingIsHidden(mapping) }
<label>
<label class="flex">
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id} value={i}>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer}></TagRenderingMapping>
</label>
{/if}
{/each}
{#if config.freeform?.key}
<label>
<label class="flex">
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id}
value={config.mappings.length}>
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
@ -159,13 +164,14 @@
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
{#if !mappingIsHidden(mapping)}
<label>
<label class="flex">
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+i} bind:checked={checkedMappings[i]}>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement}></TagRenderingMapping>
</label>{/if}
</label>
{/if}
{/each}
{#if config.freeform?.key}
<label>
<label class="flex">
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+config.mappings.length}
bind:checked={checkedMappings[config.mappings.length]}>
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
@ -184,7 +190,7 @@
<Tr t={Translations.t.general.save}></Tr>
</button>
{: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-->
<ExclamationIcon/>
</div>

View file

@ -29,6 +29,7 @@ import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"
import { OsmTags } from "../../Models/OsmFeature"
/**
* @deprecated: getting stripped and getting ported
* Shows the question element.
* 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": {
"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": {
"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 { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
import { Utils } from "../Utils"
import {existsSync, mkdirSync, readFileSync, writeFileSync} from "fs"
import {Utils} from "../Utils"
import ScriptUtils from "./ScriptUtils"
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 rootTranslation = new TranslationPart()
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)
}
return rootTranslation
@ -52,10 +52,10 @@ class TranslationPart {
if (typeof v != "string") {
console.error(
`Non-string object at ${context} in translation while trying to add the translation ` +
JSON.stringify(v) +
` to '` +
translationsKey +
"'. The offending object which _should_ be a translation is: ",
JSON.stringify(v) +
` to '` +
translationsKey +
"'. The offending object which _should_ be a translation is: ",
v,
"\n\nThe current object is (only showing en):",
this.toJson(),
@ -94,9 +94,9 @@ class TranslationPart {
if (noTranslate !== undefined) {
console.log(
"Ignoring some translations for " +
context +
": " +
dontTranslateKeys.join(", ")
context +
": " +
dontTranslateKeys.join(", ")
)
}
}
@ -243,14 +243,14 @@ class TranslationPart {
}
subparts = subparts.map((p) => p.split(/\(.*\)/)[0])
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}
this.contents.forEach((value, key) => {
neededSubparts.forEach(({ part, usedByLanguage }) => {
neededSubparts.forEach(({part, usedByLanguage}) => {
if (typeof value !== "string") {
return
}
@ -444,6 +444,7 @@ function removeEmptyString(object: object) {
}
return object
}
/**
* Formats the specified file, helps to prevent merge conflicts
* */
@ -659,7 +660,8 @@ function mergeLayerTranslations() {
const layerFiles = ScriptUtils.getLayerFiles()
for (const layerFile of layerFiles) {
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()
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,
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 {
console.log("Ignore weblate")
}
@ -704,13 +708,13 @@ const l2 = generateTranslationsObjectFrom(
"themes"
)
const l3 = generateTranslationsObjectFrom(
[{ path: questionsPath, parsed: questionsParsed }],
[{path: questionsPath, parsed: questionsParsed}],
"shared-questions"
)
const usedLanguages: string[] = Utils.Dedup(l1.concat(l2).concat(l3)).filter((v) => v !== "*")
usedLanguages.sort()
fs.writeFileSync("./assets/used_languages.json", JSON.stringify({ languages: usedLanguages }))
fs.writeFileSync("./assets/used_languages.json", JSON.stringify({languages: usedLanguages}))
if (!themeOverwritesWeblate) {
// Generates the core translations