diff --git a/assets/layers/questions/questions.json b/assets/layers/questions/questions.json index d5d4551f7..633d151e6 100644 --- a/assets/layers/questions/questions.json +++ b/assets/layers/questions/questions.json @@ -175,13 +175,23 @@ "cs": "Jaké je telefonní číslo {title()}?" }, "render": { - "*": "{phone}" + "special": { + "type": "link", + "href": "tel:{phone}", + "text": "{phone}" + } }, "icon": "./assets/layers/questions/phone.svg", "mappings": [ { "if": "contact:phone~*", - "then": "{contact:phone}", + "then": { + "special": { + "type": "link", + "href": "tel:{contact:phone}", + "text": "{contact:phone}" + } + }, "hideInAnswer": true, "icon": "./assets/layers/questions/phone.svg" } diff --git a/assets/layers/toilet/toilet.json b/assets/layers/toilet/toilet.json index 21a585d8c..c5c6a8619 100644 --- a/assets/layers/toilet/toilet.json +++ b/assets/layers/toilet/toilet.json @@ -231,6 +231,7 @@ }, { "if": "access=customers", + "icon": "key", "then": { "en": "Only access to customers", "de": "Der Zugang ist nur für Kunden", @@ -245,6 +246,7 @@ }, { "if": "access=no", + "icon": "lock", "alsoShowIf": "access=private", "then": { "en": "Not accessible", @@ -261,6 +263,7 @@ }, { "if": "access=key", + "icon": "key", "then": { "en": "Accessible, but one has to ask a key to enter", "de": "Der Zugang ist möglich, aber man muss nach einen Schlüssel fragen", diff --git a/assets/themes/ghostsigns/ghostsigns.json b/assets/themes/ghostsigns/ghostsigns.json index e58696d19..8c1eb728c 100644 --- a/assets/themes/ghostsigns/ghostsigns.json +++ b/assets/themes/ghostsigns/ghostsigns.json @@ -91,7 +91,7 @@ }, "freeform": { "key": "inscription", - "type": "string", + "type": "text", "placeholder": { "en": "Text on the sign", "de": "Text auf dem Schild", diff --git a/langs/en.json b/langs/en.json index 3a82c6068..72b2d8cc3 100644 --- a/langs/en.json +++ b/langs/en.json @@ -193,6 +193,7 @@ }, "josmNotOpened": "JOSM could not be reached. Make sure it is opened and remote control is enabled", "josmOpened": "JOSM is opened", + "madeBy": "Mady by {author}", "mapContributionsBy": "The current visible data has edits made by {contributors}", "mapContributionsByAndHidden": "The current visible data has edits made by {contributors} and {hiddenCount} more contributors", "mapDataByOsm": "Map data: OpenStreetMap", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index e6a1f97f1..75c2e89fd 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -2077,6 +2077,12 @@ video { margin-bottom: calc(0px * var(--tw-space-y-reverse)); } +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + .space-x-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.5rem * var(--tw-space-x-reverse)); @@ -2101,12 +2107,6 @@ video { margin-left: calc(-1px * calc(1 - var(--tw-space-x-reverse))); } -.space-x-4 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(1rem * var(--tw-space-x-reverse)); - margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); -} - .space-y-1 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); @@ -2435,6 +2435,10 @@ video { border-top-right-radius: 0.25rem; } +.rounded-bl-full { + border-bottom-left-radius: 9999px; +} + .rounded-tl { border-top-left-radius: 0.25rem; } @@ -3458,6 +3462,14 @@ video { padding-top: 0px; } +.pl-3 { + padding-left: 0.75rem; +} + +.pb-3 { + padding-bottom: 0.75rem; +} + .pl-4 { padding-left: 1rem; } @@ -3466,14 +3478,6 @@ video { padding-right: 1rem; } -.pl-3 { - padding-left: 0.75rem; -} - -.pr-3 { - padding-right: 0.75rem; -} - .pl-1 { padding-left: 0.25rem; } @@ -4908,6 +4912,10 @@ a:hover { background-color: #f2f2f2; } +.no-bold b { + font-weight: normal; +} + /************************* MISC ELEMENTS *************************/ .selected svg:not(.noselect *) path.selectable { diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index 7add3c57b..901518039 100644 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -6,6 +6,7 @@ import Script from "./Script" const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"] const ignoreTerms = ["searchTerms"] + class TranslationPart { contents: Map = new Map() @@ -14,7 +15,8 @@ class TranslationPart { const rootTranslation = new TranslationPart() for (const file of files) { 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 } @@ -46,18 +48,14 @@ class TranslationPart { return } for (const translationsKey in translations) { - if (!translations.hasOwnProperty(translationsKey)) { - continue - } - const v = translations[translationsKey] 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(), @@ -96,17 +94,14 @@ class TranslationPart { if (noTranslate !== undefined) { console.log( "Ignoring some translations for " + - context + - ": " + - dontTranslateKeys.join(", ") + context + + ": " + + dontTranslateKeys.join(", ") ) } } for (let key in object) { - if (!object.hasOwnProperty(key)) { - continue - } if (ignoreTerms.indexOf(key) >= 0) { continue } @@ -155,13 +150,13 @@ class 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[] { 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) if (typeof value === "string") { @@ -180,20 +175,20 @@ class TranslationPart { const parts = [] let keys = Array.from(this.contents.keys()) keys = keys.sort() - for (let key of keys) { + for (const key of keys) { let value = this.contents.get(key) if (typeof value === "string") { - value = value.replace(/"/g, '\\"').replace(/\n/g, "\\n") + value = value.replace(/"/g, "\\\"").replace(/\n/g, "\\n") if (neededLanguage === undefined) { - parts.push(`\"${key}\": \"${value}\"`) + parts.push(`"${key}": "${value}"`) } else if (key === neededLanguage) { return `"${value}"` } } else { const sub = (value as TranslationPart).toJson(neededLanguage) if (sub !== "") { - parts.push(`\"${key}\": ${sub}`) + parts.push(`"${key}": ${sub}`) } } } @@ -234,7 +229,7 @@ class TranslationPart { } else if (!isLeaf) { errors.push({ error: "Mixed node: non-leaf node has translation strings", - path: path, + path: path }) } @@ -285,7 +280,7 @@ class TranslationPart { value + "\n" + fixLink, - path: path, + path: path }) } return @@ -297,7 +292,7 @@ class TranslationPart { error: `The translation for ${key} does not have the required subpart ${part} (in ${usedByLanguage}). \tThe full translation is ${value} \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'. @@ -361,9 +338,10 @@ function isTranslation(tr: any): boolean { function transformTranslation( obj: any, path: string[] = [], - languageWhitelist: string[] = undefined + languageWhitelist: string[] = undefined, + shortNotation = false ) { - if (isTranslation(obj)) { + if (GenerateTranslations.isTranslation(obj)) { return `new Translation( ${JSON.stringify(obj)} )` } @@ -380,7 +358,7 @@ function transformTranslation( } let value = obj[key] - if (isTranslation(value)) { + if (GenerateTranslations.isTranslation(value)) { if (languageWhitelist !== undefined) { const nv = {} for (const ln of languageWhitelist) { @@ -395,7 +373,7 @@ function transformTranslation( )}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}` } 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}")` if (subParts !== null) { @@ -409,12 +387,16 @@ function transformTranslation( "." )}: A subpart contains invalid characters: ${subParts.join(", ")}` } - expr = `return new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify( + expr = `new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify( value )}, "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 { values.push( spaces + key + ": " + transformTranslation(value, [...path, key], languageWhitelist) @@ -469,54 +451,11 @@ function formatFile(path) { 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'. * 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 @@ -608,7 +547,7 @@ function MergeTranslation(source: any, target: any, language: string, context: s if (targetV[language] !== undefined && targetV[language] !== sourceV) { was = " (overwritten " + targetV[language] + ")" } - console.log(" + ", context + "." + language, "-->", sourceV, was) + // console.log(" + ", context + "." + language, "-->", sourceV, was) continue } if (typeof sourceV === "object") { @@ -697,7 +636,7 @@ function removeNonEnglishTranslations(object: any) { leaf["en"] = en }, (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") } + /** + * Checks that the given object only contains string-values + * @param tr + */ + static isTranslation(tr: Record): 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 */ @@ -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 { if (!existsSync("./langs/themes")) { mkdirSync("./langs/themes") } const themeOverwritesWeblate = args[0] === "--ignore-weblate" const englishOnly = args[0] === "--english-only" + if (englishOnly) { + console.log("ENGLISH ONLY") + } if (!themeOverwritesWeblate) { - mergeLayerTranslations() - mergeThemeTranslations() - compileTranslationsFromWeblate() + mergeLayerTranslations(englishOnly) + mergeThemeTranslations(englishOnly) + this.compileTranslationsFromWeblate(englishOnly) } else { console.log("Ignore weblate") } this.detectUsedLanguages() - genTranslations() + this.genTranslations(englishOnly) { const allTranslationFiles = ScriptUtils.readDirRecSync("langs").filter((path) => path.endsWith(".json") diff --git a/src/UI/Base/DynLink.svelte b/src/UI/Base/DynLink.svelte index 9337a5ff6..91deac7e9 100644 --- a/src/UI/Base/DynLink.svelte +++ b/src/UI/Base/DynLink.svelte @@ -1,6 +1,7 @@ + import { Utils } from "../../Utils" + export let text: string export let href: string + + export let classnames: string = undefined export let download: string = undefined export let ariaLabel: string = undefined @@ -9,7 +13,7 @@ } } @@ -45,35 +46,38 @@ let enableLogin = state.featureSwitches.featureSwitchEnableLogin -{#if !$sourceUrl || !$enableLogin} - -{:else if $externalData === undefined} - -{:else if $externalData["error"] !== undefined} -
- -
-{:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0} - -{:else if !$hasDifferencesAtStart} + + + {#if !$sourceUrl || !$enableLogin} + + {:else if $externalData === undefined} + + {:else if $externalData["error"] !== undefined} +
+ +
+ {:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0} + + {:else if !$hasDifferencesAtStart} -{:else if $comparisonState !== undefined} - + {:else if $comparisonState !== undefined} + - - -{/if} + + + {/if} +
diff --git a/src/UI/Image/AttributedImage.svelte b/src/UI/Image/AttributedImage.svelte index 5c64ccfdc..91f8bfd86 100644 --- a/src/UI/Image/AttributedImage.svelte +++ b/src/UI/Image/AttributedImage.svelte @@ -6,6 +6,7 @@ import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" import { Mapillary } from "../../Logic/ImageProviders/Mapillary" import { UIEventSource } from "../../Logic/UIEventSource" + import { MagnifyingGlassPlusIcon } from "@babeard/svelte-heroicons/outline" export let image: Partial let fallbackImage: string = undefined @@ -16,25 +17,37 @@ let imgEl: HTMLImageElement export let imgClass: string = undefined export let previewedImage: UIEventSource = undefined + export let attributionFormat: "minimal" | "medium" | "large" = "medium" + let canZoom = previewedImage !== undefined // We check if there is a SOURCE, not if there is data in it! + let loaded = false
- { +
+ loaded = true} + class={imgClass ?? ""} + class:cursor-zoom-in={previewedImage !== undefined} + on:click={() => { previewedImage?.setData(image) }} - on:error={() => { + on:error={() => { if (fallbackImage) { imgEl.src = fallbackImage } }} - src={image.url} - /> + src={image.url} + /> + {#if canZoom && loaded} +
+ +
+ {/if} + +
- +
diff --git a/src/UI/Image/ImageAttribution.svelte b/src/UI/Image/ImageAttribution.svelte index b62ce1baf..cd7c9847a 100644 --- a/src/UI/Image/ImageAttribution.svelte +++ b/src/UI/Image/ImageAttribution.svelte @@ -4,11 +4,15 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" import ToSvelte from "../Base/ToSvelte.svelte" import { EyeIcon } from "@rgossiaux/svelte-heroicons/solid" + import Tr from "../Base/Tr.svelte" + import Translations from "../i18n/Translations" /** * A small element showing the attribution of a single image */ export let image: Partial & { id: string; url: string } + export let attributionFormat: "minimal" | "medium" | "large" = "medium" + let license: Store = UIEventSource.FromPromise( image.provider?.DownloadAttribution(image) ) @@ -16,50 +20,59 @@ {#if $license !== undefined} -
+
{#if icon !== undefined}
{/if} -
- {#if $license.title} - {#if $license.informationLocation} - - {$license.title} - - {:else} - $license.title +
+ {#if attributionFormat !== "minimal" } + {#if $license.title} + {#if $license.informationLocation} + + {$license.title} + + {:else} + $license.title + {/if} {/if} {/if} {#if $license.artist} -
- {@html $license.artist} -
+ {#if attributionFormat === "large"} + + {:else} +
+ {@html $license.artist} +
+ {/if} {/if} -
- {#if $license.license !== undefined || $license.licenseShortName !== undefined} -
- {$license?.license ?? $license?.licenseShortName} -
- {/if} - - {#if $license.views} -
- - {$license.views} -
- {/if} -
- {#if $license.date}
{$license.date.toLocaleDateString()}
{/if} + + {#if attributionFormat !== "minimal"} +
+ {#if ($license.license !== undefined || $license.licenseShortName !== undefined)} +
+ {$license?.license ?? $license?.licenseShortName} +
+ {/if} + + {#if $license.views} +
+ + {$license.views} +
+ {/if} +
+ {/if} +
{/if} diff --git a/src/UI/Image/ImageOperations.svelte b/src/UI/Image/ImageOperations.svelte index 0152e9cf5..7d131e2e0 100644 --- a/src/UI/Image/ImageOperations.svelte +++ b/src/UI/Image/ImageOperations.svelte @@ -38,9 +38,9 @@ class="pointer-events-none absolute bottom-0 left-0 flex w-full flex-wrap items-end justify-between" >
- +