forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
d1e7eba2db
19 changed files with 554 additions and 448 deletions
|
@ -175,13 +175,23 @@
|
||||||
"cs": "Jaké je telefonní číslo {title()}?"
|
"cs": "Jaké je telefonní číslo {title()}?"
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"*": "<a href='tel:{phone}'>{phone}</a>"
|
"special": {
|
||||||
|
"type": "link",
|
||||||
|
"href": "tel:{phone}",
|
||||||
|
"text": "{phone}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"icon": "./assets/layers/questions/phone.svg",
|
"icon": "./assets/layers/questions/phone.svg",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
"if": "contact:phone~*",
|
"if": "contact:phone~*",
|
||||||
"then": "<a href='tel:{contact:phone}'>{contact:phone}</a>",
|
"then": {
|
||||||
|
"special": {
|
||||||
|
"type": "link",
|
||||||
|
"href": "tel:{contact:phone}",
|
||||||
|
"text": "{contact:phone}"
|
||||||
|
}
|
||||||
|
},
|
||||||
"hideInAnswer": true,
|
"hideInAnswer": true,
|
||||||
"icon": "./assets/layers/questions/phone.svg"
|
"icon": "./assets/layers/questions/phone.svg"
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,6 +231,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": "access=customers",
|
"if": "access=customers",
|
||||||
|
"icon": "key",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "Only access to customers",
|
"en": "Only access to customers",
|
||||||
"de": "Der Zugang ist nur für Kunden",
|
"de": "Der Zugang ist nur für Kunden",
|
||||||
|
@ -245,6 +246,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": "access=no",
|
"if": "access=no",
|
||||||
|
"icon": "lock",
|
||||||
"alsoShowIf": "access=private",
|
"alsoShowIf": "access=private",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "Not accessible",
|
"en": "Not accessible",
|
||||||
|
@ -261,6 +263,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": "access=key",
|
"if": "access=key",
|
||||||
|
"icon": "key",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "Accessible, but one has to ask a key to enter",
|
"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",
|
"de": "Der Zugang ist möglich, aber man muss nach einen Schlüssel fragen",
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
},
|
},
|
||||||
"freeform": {
|
"freeform": {
|
||||||
"key": "inscription",
|
"key": "inscription",
|
||||||
"type": "string",
|
"type": "text",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"en": "Text on the sign",
|
"en": "Text on the sign",
|
||||||
"de": "Text auf dem Schild",
|
"de": "Text auf dem Schild",
|
||||||
|
|
|
@ -193,6 +193,7 @@
|
||||||
},
|
},
|
||||||
"josmNotOpened": "JOSM could not be reached. Make sure it is opened and remote control is enabled",
|
"josmNotOpened": "JOSM could not be reached. Make sure it is opened and remote control is enabled",
|
||||||
"josmOpened": "JOSM is opened",
|
"josmOpened": "JOSM is opened",
|
||||||
|
"madeBy": "Mady by <b>{author}</b>",
|
||||||
"mapContributionsBy": "The current visible data has edits made by {contributors}",
|
"mapContributionsBy": "The current visible data has edits made by {contributors}",
|
||||||
"mapContributionsByAndHidden": "The current visible data has edits made by {contributors} and {hiddenCount} more contributors",
|
"mapContributionsByAndHidden": "The current visible data has edits made by {contributors} and {hiddenCount} more contributors",
|
||||||
"mapDataByOsm": "Map data: OpenStreetMap",
|
"mapDataByOsm": "Map data: OpenStreetMap",
|
||||||
|
|
|
@ -2077,6 +2077,12 @@ video {
|
||||||
margin-bottom: calc(0px * var(--tw-space-y-reverse));
|
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]) {
|
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-space-x-reverse: 0;
|
--tw-space-x-reverse: 0;
|
||||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
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)));
|
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]) {
|
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-space-y-reverse: 0;
|
--tw-space-y-reverse: 0;
|
||||||
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
|
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
@ -2435,6 +2435,10 @@ video {
|
||||||
border-top-right-radius: 0.25rem;
|
border-top-right-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rounded-bl-full {
|
||||||
|
border-bottom-left-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded-tl {
|
.rounded-tl {
|
||||||
border-top-left-radius: 0.25rem;
|
border-top-left-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
@ -3458,6 +3462,14 @@ video {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pl-3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-3 {
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pl-4 {
|
.pl-4 {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -3466,14 +3478,6 @@ video {
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-3 {
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pr-3 {
|
|
||||||
padding-right: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-1 {
|
.pl-1 {
|
||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
@ -4908,6 +4912,10 @@ a:hover {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-bold b {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/************************* MISC ELEMENTS *************************/
|
/************************* MISC ELEMENTS *************************/
|
||||||
|
|
||||||
.selected svg:not(.noselect *) path.selectable {
|
.selected svg:not(.noselect *) path.selectable {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Script from "./Script"
|
||||||
|
|
||||||
const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"]
|
const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"]
|
||||||
const ignoreTerms = ["searchTerms"]
|
const ignoreTerms = ["searchTerms"]
|
||||||
|
|
||||||
class TranslationPart {
|
class TranslationPart {
|
||||||
contents: Map<string, TranslationPart | string> = new Map<string, TranslationPart | string>()
|
contents: Map<string, TranslationPart | string> = new Map<string, TranslationPart | string>()
|
||||||
|
|
||||||
|
@ -14,7 +15,8 @@ class TranslationPart {
|
||||||
const rootTranslation = new TranslationPart()
|
const rootTranslation = new TranslationPart()
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const content = JSON.parse(readFileSync(file, { encoding: "utf8" }))
|
const content = JSON.parse(readFileSync(file, { encoding: "utf8" }))
|
||||||
rootTranslation.addTranslation(file.substr(0, file.length - ".json".length), content)
|
const language = file.substr(0, file.length - ".json".length)
|
||||||
|
rootTranslation.addTranslation(language, content)
|
||||||
}
|
}
|
||||||
return rootTranslation
|
return rootTranslation
|
||||||
}
|
}
|
||||||
|
@ -46,18 +48,14 @@ class TranslationPart {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (const translationsKey in translations) {
|
for (const translationsKey in translations) {
|
||||||
if (!translations.hasOwnProperty(translationsKey)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = translations[translationsKey]
|
const v = translations[translationsKey]
|
||||||
if (typeof v != "string") {
|
if (typeof v != "string") {
|
||||||
console.error(
|
console.error(
|
||||||
`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(),
|
||||||
|
@ -96,17 +94,14 @@ 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(", ")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let key in object) {
|
for (let key in object) {
|
||||||
if (!object.hasOwnProperty(key)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ignoreTerms.indexOf(key) >= 0) {
|
if (ignoreTerms.indexOf(key) >= 0) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -155,13 +150,13 @@ class TranslationPart {
|
||||||
this.contents.set(key, new TranslationPart())
|
this.contents.set(key, new TranslationPart())
|
||||||
}
|
}
|
||||||
|
|
||||||
;(this.contents.get(key) as TranslationPart).recursiveAdd(v, context + "." + key)
|
(this.contents.get(key) as TranslationPart).recursiveAdd(v, context + "." + key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
knownLanguages(): string[] {
|
knownLanguages(): string[] {
|
||||||
const languages = []
|
const languages = []
|
||||||
for (let key of Array.from(this.contents.keys())) {
|
for (const key of Array.from(this.contents.keys())) {
|
||||||
const value = this.contents.get(key)
|
const value = this.contents.get(key)
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
|
@ -180,20 +175,20 @@ class TranslationPart {
|
||||||
const parts = []
|
const parts = []
|
||||||
let keys = Array.from(this.contents.keys())
|
let keys = Array.from(this.contents.keys())
|
||||||
keys = keys.sort()
|
keys = keys.sort()
|
||||||
for (let key of keys) {
|
for (const key of keys) {
|
||||||
let value = this.contents.get(key)
|
let value = this.contents.get(key)
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
value = value.replace(/"/g, '\\"').replace(/\n/g, "\\n")
|
value = value.replace(/"/g, "\\\"").replace(/\n/g, "\\n")
|
||||||
if (neededLanguage === undefined) {
|
if (neededLanguage === undefined) {
|
||||||
parts.push(`\"${key}\": \"${value}\"`)
|
parts.push(`"${key}": "${value}"`)
|
||||||
} else if (key === neededLanguage) {
|
} else if (key === neededLanguage) {
|
||||||
return `"${value}"`
|
return `"${value}"`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const sub = (value as TranslationPart).toJson(neededLanguage)
|
const sub = (value as TranslationPart).toJson(neededLanguage)
|
||||||
if (sub !== "") {
|
if (sub !== "") {
|
||||||
parts.push(`\"${key}\": ${sub}`)
|
parts.push(`"${key}": ${sub}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,7 +229,7 @@ class TranslationPart {
|
||||||
} else if (!isLeaf) {
|
} else if (!isLeaf) {
|
||||||
errors.push({
|
errors.push({
|
||||||
error: "Mixed node: non-leaf node has translation strings",
|
error: "Mixed node: non-leaf node has translation strings",
|
||||||
path: path,
|
path: path
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,7 +280,7 @@ class TranslationPart {
|
||||||
value +
|
value +
|
||||||
"\n" +
|
"\n" +
|
||||||
fixLink,
|
fixLink,
|
||||||
path: path,
|
path: path
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -297,7 +292,7 @@ class TranslationPart {
|
||||||
error: `The translation for ${key} does not have the required subpart ${part} (in ${usedByLanguage}).
|
error: `The translation for ${key} does not have the required subpart ${part} (in ${usedByLanguage}).
|
||||||
\tThe full translation is ${value}
|
\tThe full translation is ${value}
|
||||||
\t${fixLink}`,
|
\t${fixLink}`,
|
||||||
path: path,
|
path: path
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -334,24 +329,6 @@ class TranslationPart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks that the given object only contains string-values
|
|
||||||
* @param tr
|
|
||||||
*/
|
|
||||||
function isTranslation(tr: any): boolean {
|
|
||||||
if (tr["#"] === "no-translations") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (tr["special"]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for (const key in tr) {
|
|
||||||
if (typeof tr[key] !== "string") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a translation object into something that can be added to the 'generated translations'.
|
* Converts a translation object into something that can be added to the 'generated translations'.
|
||||||
|
@ -361,9 +338,10 @@ function isTranslation(tr: any): boolean {
|
||||||
function transformTranslation(
|
function transformTranslation(
|
||||||
obj: any,
|
obj: any,
|
||||||
path: string[] = [],
|
path: string[] = [],
|
||||||
languageWhitelist: string[] = undefined
|
languageWhitelist: string[] = undefined,
|
||||||
|
shortNotation = false
|
||||||
) {
|
) {
|
||||||
if (isTranslation(obj)) {
|
if (GenerateTranslations.isTranslation(obj)) {
|
||||||
return `new Translation( ${JSON.stringify(obj)} )`
|
return `new Translation( ${JSON.stringify(obj)} )`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,7 +358,7 @@ function transformTranslation(
|
||||||
}
|
}
|
||||||
let value = obj[key]
|
let value = obj[key]
|
||||||
|
|
||||||
if (isTranslation(value)) {
|
if (GenerateTranslations.isTranslation(value)) {
|
||||||
if (languageWhitelist !== undefined) {
|
if (languageWhitelist !== undefined) {
|
||||||
const nv = {}
|
const nv = {}
|
||||||
for (const ln of languageWhitelist) {
|
for (const ln of languageWhitelist) {
|
||||||
|
@ -395,7 +373,7 @@ function transformTranslation(
|
||||||
)}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}`
|
)}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}`
|
||||||
}
|
}
|
||||||
const subParts: string[] = value["en"].match(/{[^}]*}/g)
|
const subParts: string[] = value["en"].match(/{[^}]*}/g)
|
||||||
let expr = `return new Translation(${JSON.stringify(value)}, "core:${path.join(
|
let expr = `new Translation(${JSON.stringify(value)}, "core:${path.join(
|
||||||
"."
|
"."
|
||||||
)}.${key}")`
|
)}.${key}")`
|
||||||
if (subParts !== null) {
|
if (subParts !== null) {
|
||||||
|
@ -409,12 +387,16 @@ function transformTranslation(
|
||||||
"."
|
"."
|
||||||
)}: A subpart contains invalid characters: ${subParts.join(", ")}`
|
)}: A subpart contains invalid characters: ${subParts.join(", ")}`
|
||||||
}
|
}
|
||||||
expr = `return new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify(
|
expr = `new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify(
|
||||||
value
|
value
|
||||||
)}, "core:${path.join(".")}.${key}")`
|
)}, "core:${path.join(".")}.${key}")`
|
||||||
}
|
}
|
||||||
|
if (shortNotation) {
|
||||||
|
values.push(`${spaces} ${key}: ${expr}`)
|
||||||
|
|
||||||
values.push(`${spaces}get ${key}() { ${expr} }`)
|
} else {
|
||||||
|
values.push(`${spaces}get ${key}() { return ${expr} }`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
values.push(
|
values.push(
|
||||||
spaces + key + ": " + transformTranslation(value, [...path, key], languageWhitelist)
|
spaces + key + ": " + transformTranslation(value, [...path, key], languageWhitelist)
|
||||||
|
@ -469,54 +451,11 @@ function formatFile(path) {
|
||||||
writeFileSync(path, JSON.stringify(contents, null, " ") + (endsWithNewline ? "\n" : ""))
|
writeFileSync(path, JSON.stringify(contents, null, " ") + (endsWithNewline ? "\n" : ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates the big compiledTranslations file
|
|
||||||
*/
|
|
||||||
function genTranslations() {
|
|
||||||
if (!fs.existsSync("./src/assets/generated/")) {
|
|
||||||
fs.mkdirSync("./src/assets/generated/")
|
|
||||||
}
|
|
||||||
const translations = JSON.parse(
|
|
||||||
fs.readFileSync("./src/assets/generated/translations.json", "utf-8")
|
|
||||||
)
|
|
||||||
const transformed = transformTranslation(translations)
|
|
||||||
|
|
||||||
let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`
|
|
||||||
module += " public static t = " + transformed
|
|
||||||
module += "\n }"
|
|
||||||
|
|
||||||
fs.writeFileSync("./src/assets/generated/CompiledTranslations.ts", module)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads 'lang/*.json', writes them into to 'assets/generated/translations.json'.
|
* Reads 'lang/*.json', writes them into to 'assets/generated/translations.json'.
|
||||||
* This is only for the core translations
|
* This is only for the core translations
|
||||||
*/
|
*/
|
||||||
function compileTranslationsFromWeblate() {
|
|
||||||
const translations = ScriptUtils.readDirRecSync("./langs", 1).filter(
|
|
||||||
(path) => path.indexOf(".json") > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
const allTranslations = new TranslationPart()
|
|
||||||
|
|
||||||
allTranslations.validateStrict()
|
|
||||||
|
|
||||||
for (const translationFile of translations) {
|
|
||||||
try {
|
|
||||||
const contents = JSON.parse(readFileSync(translationFile, "utf-8"))
|
|
||||||
let language = translationFile.substring(translationFile.lastIndexOf("/") + 1)
|
|
||||||
language = language.substring(0, language.length - 5)
|
|
||||||
allTranslations.add(language, contents)
|
|
||||||
} catch (e) {
|
|
||||||
throw "Could not read file " + translationFile + " due to " + e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileSync(
|
|
||||||
"./src/assets/generated/translations.json",
|
|
||||||
JSON.stringify(JSON.parse(allTranslations.toJson()), null, " ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the strings out of the layers; writes them onto the weblate paths
|
* Get all the strings out of the layers; writes them onto the weblate paths
|
||||||
|
@ -608,7 +547,7 @@ function MergeTranslation(source: any, target: any, language: string, context: s
|
||||||
if (targetV[language] !== undefined && targetV[language] !== sourceV) {
|
if (targetV[language] !== undefined && targetV[language] !== sourceV) {
|
||||||
was = " (overwritten " + targetV[language] + ")"
|
was = " (overwritten " + targetV[language] + ")"
|
||||||
}
|
}
|
||||||
console.log(" + ", context + "." + language, "-->", sourceV, was)
|
// console.log(" + ", context + "." + language, "-->", sourceV, was)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (typeof sourceV === "object") {
|
if (typeof sourceV === "object") {
|
||||||
|
@ -697,7 +636,7 @@ function removeNonEnglishTranslations(object: any) {
|
||||||
leaf["en"] = en
|
leaf["en"] = en
|
||||||
},
|
},
|
||||||
(possibleLeaf) =>
|
(possibleLeaf) =>
|
||||||
possibleLeaf !== null && typeof possibleLeaf === "object" && isTranslation(possibleLeaf)
|
possibleLeaf !== null && typeof possibleLeaf === "object" && GenerateTranslations.isTranslation(possibleLeaf)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -732,6 +671,25 @@ class GenerateTranslations extends Script {
|
||||||
super("Syncs translations from/to the theme and layer files")
|
super("Syncs translations from/to the theme and layer files")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the given object only contains string-values
|
||||||
|
* @param tr
|
||||||
|
*/
|
||||||
|
static isTranslation(tr: Record<string, string | object>): boolean {
|
||||||
|
if (tr["#"] === "no-translations") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (tr["special"]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (const key in tr) {
|
||||||
|
if (typeof tr[key] !== "string") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OUtputs the 'used_languages.json'-file
|
* OUtputs the 'used_languages.json'-file
|
||||||
*/
|
*/
|
||||||
|
@ -754,22 +712,74 @@ class GenerateTranslations extends Script {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the big compiledTranslations file based on 'translations.json'
|
||||||
|
*/
|
||||||
|
genTranslations(englishOnly?: boolean) {
|
||||||
|
if (!fs.existsSync("./src/assets/generated/")) {
|
||||||
|
fs.mkdirSync("./src/assets/generated/")
|
||||||
|
}
|
||||||
|
const translations = JSON.parse(
|
||||||
|
fs.readFileSync("./src/assets/generated/translations.json", "utf-8")
|
||||||
|
)
|
||||||
|
const transformed = transformTranslation(translations, undefined, englishOnly ? ["en"] : undefined, englishOnly)
|
||||||
|
|
||||||
|
let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`
|
||||||
|
module += " public static t = " + transformed
|
||||||
|
module += "\n }"
|
||||||
|
|
||||||
|
fs.writeFileSync("./src/assets/generated/CompiledTranslations.ts", module)
|
||||||
|
}
|
||||||
|
|
||||||
|
compileTranslationsFromWeblate(englishOnly: boolean) {
|
||||||
|
const translations = ScriptUtils.readDirRecSync("./langs", 1).filter(
|
||||||
|
(path) => path.indexOf(".json") > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const allTranslations = new TranslationPart()
|
||||||
|
|
||||||
|
allTranslations.validateStrict()
|
||||||
|
|
||||||
|
for (const translationFile of translations) {
|
||||||
|
try {
|
||||||
|
const contents = JSON.parse(readFileSync(translationFile, "utf-8"))
|
||||||
|
let language = translationFile.substring(translationFile.lastIndexOf("/") + 1)
|
||||||
|
language = language.substring(0, language.length - 5)
|
||||||
|
if (englishOnly && language !== "en") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allTranslations.add(language, contents)
|
||||||
|
} catch (e) {
|
||||||
|
throw "Could not read file " + translationFile + " due to " + e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
"./src/assets/generated/translations.json",
|
||||||
|
JSON.stringify(JSON.parse(allTranslations.toJson()), null, " ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async main(args: string[]): Promise<void> {
|
async main(args: string[]): Promise<void> {
|
||||||
if (!existsSync("./langs/themes")) {
|
if (!existsSync("./langs/themes")) {
|
||||||
mkdirSync("./langs/themes")
|
mkdirSync("./langs/themes")
|
||||||
}
|
}
|
||||||
const themeOverwritesWeblate = args[0] === "--ignore-weblate"
|
const themeOverwritesWeblate = args[0] === "--ignore-weblate"
|
||||||
const englishOnly = args[0] === "--english-only"
|
const englishOnly = args[0] === "--english-only"
|
||||||
|
if (englishOnly) {
|
||||||
|
console.log("ENGLISH ONLY")
|
||||||
|
}
|
||||||
if (!themeOverwritesWeblate) {
|
if (!themeOverwritesWeblate) {
|
||||||
mergeLayerTranslations()
|
mergeLayerTranslations(englishOnly)
|
||||||
mergeThemeTranslations()
|
mergeThemeTranslations(englishOnly)
|
||||||
compileTranslationsFromWeblate()
|
this.compileTranslationsFromWeblate(englishOnly)
|
||||||
} else {
|
} else {
|
||||||
console.log("Ignore weblate")
|
console.log("Ignore weblate")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.detectUsedLanguages()
|
this.detectUsedLanguages()
|
||||||
genTranslations()
|
this.genTranslations(englishOnly)
|
||||||
{
|
{
|
||||||
const allTranslationFiles = ScriptUtils.readDirRecSync("langs").filter((path) =>
|
const allTranslationFiles = ScriptUtils.readDirRecSync("langs").filter((path) =>
|
||||||
path.endsWith(".json")
|
path.endsWith(".json")
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||||
import Icon from "../Map/Icon.svelte"
|
import Icon from "../Map/Icon.svelte"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
export let text: Store<string>
|
export let text: Store<string>
|
||||||
export let href: Store<string>
|
export let href: Store<string>
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={$href}
|
href={Utils.prepareHref($href)}
|
||||||
aria-label={$ariaLabel}
|
aria-label={$ariaLabel}
|
||||||
title={$ariaLabel}
|
title={$ariaLabel}
|
||||||
target={$newTab ? "_blank" : undefined}
|
target={$newTab ? "_blank" : undefined}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
export let text: string
|
export let text: string
|
||||||
export let href: string
|
export let href: string
|
||||||
|
|
||||||
|
|
||||||
export let classnames: string = undefined
|
export let classnames: string = undefined
|
||||||
export let download: string = undefined
|
export let download: string = undefined
|
||||||
export let ariaLabel: string = undefined
|
export let ariaLabel: string = undefined
|
||||||
|
@ -9,7 +13,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
{href}
|
href={Utils.prepareHref(href)}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
title={ariaLabel}
|
title={ariaLabel}
|
||||||
target={newTab ? "_blank" : undefined}
|
target={newTab ? "_blank" : undefined}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||||
import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt"
|
import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt"
|
||||||
import { ComparisonState } from "./ComparisonState"
|
import { ComparisonState } from "./ComparisonState"
|
||||||
|
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||||
|
|
||||||
export let externalData: Store<
|
export let externalData: Store<
|
||||||
| { success: { content: Record<string, string> } }
|
| { success: { content: Record<string, string> } }
|
||||||
|
@ -45,35 +46,38 @@
|
||||||
let enableLogin = state.featureSwitches.featureSwitchEnableLogin
|
let enableLogin = state.featureSwitches.featureSwitchEnableLogin
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !$sourceUrl || !$enableLogin}
|
<LoginToggle {state} silentFail>
|
||||||
<!-- empty block -->
|
|
||||||
{:else if $externalData === undefined}
|
{#if !$sourceUrl || !$enableLogin}
|
||||||
<Loading />
|
<!-- empty block -->
|
||||||
{:else if $externalData["error"] !== undefined}
|
{:else if $externalData === undefined}
|
||||||
<div class="subtle low-interaction rounded p-2 px-4 italic">
|
<Loading />
|
||||||
<Tr t={Translations.t.external.error} />
|
{:else if $externalData["error"] !== undefined}
|
||||||
</div>
|
<div class="subtle low-interaction rounded p-2 px-4 italic">
|
||||||
{:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0}
|
<Tr t={Translations.t.external.error} />
|
||||||
<Tr cls="subtle" t={t.noDataLoaded} />
|
</div>
|
||||||
{:else if !$hasDifferencesAtStart}
|
{:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0}
|
||||||
|
<Tr cls="subtle" t={t.noDataLoaded} />
|
||||||
|
{:else if !$hasDifferencesAtStart}
|
||||||
<span class="subtle text-sm">
|
<span class="subtle text-sm">
|
||||||
<Tr t={t.allIncluded.Subs({ source: $sourceUrl })} />
|
<Tr t={t.allIncluded.Subs({ source: $sourceUrl })} />
|
||||||
</span>
|
</span>
|
||||||
{:else if $comparisonState !== undefined}
|
{:else if $comparisonState !== undefined}
|
||||||
<AccordionSingle expanded={!collapsed}>
|
<AccordionSingle expanded={!collapsed}>
|
||||||
<span slot="header" class="flex">
|
<span slot="header" class="flex">
|
||||||
<GlobeAlt class="h-6 w-6" />
|
<GlobeAlt class="h-6 w-6" />
|
||||||
<Tr t={Translations.t.external.title} />
|
<Tr t={Translations.t.external.title} />
|
||||||
</span>
|
</span>
|
||||||
<ComparisonTable
|
<ComparisonTable
|
||||||
externalProperties={$externalData["success"]}
|
externalProperties={$externalData["success"]}
|
||||||
{state}
|
{state}
|
||||||
{feature}
|
{feature}
|
||||||
{layer}
|
{layer}
|
||||||
{tags}
|
{tags}
|
||||||
{readonly}
|
{readonly}
|
||||||
sourceUrl={$sourceUrl}
|
sourceUrl={$sourceUrl}
|
||||||
comparisonState={$comparisonState}
|
comparisonState={$comparisonState}
|
||||||
/>
|
/>
|
||||||
</AccordionSingle>
|
</AccordionSingle>
|
||||||
{/if}
|
{/if}
|
||||||
|
</LoginToggle>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
||||||
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
|
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import { MagnifyingGlassPlusIcon } from "@babeard/svelte-heroicons/outline"
|
||||||
|
|
||||||
export let image: Partial<ProvidedImage>
|
export let image: Partial<ProvidedImage>
|
||||||
let fallbackImage: string = undefined
|
let fallbackImage: string = undefined
|
||||||
|
@ -16,25 +17,37 @@
|
||||||
let imgEl: HTMLImageElement
|
let imgEl: HTMLImageElement
|
||||||
export let imgClass: string = undefined
|
export let imgClass: string = undefined
|
||||||
export let previewedImage: UIEventSource<ProvidedImage> = undefined
|
export let previewedImage: UIEventSource<ProvidedImage> = 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
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative shrink-0">
|
<div class="relative shrink-0">
|
||||||
<img
|
<div class="relative w-fit">
|
||||||
bind:this={imgEl}
|
<img
|
||||||
class={imgClass ?? ""}
|
bind:this={imgEl}
|
||||||
class:cursor-pointer={previewedImage !== undefined}
|
on:load={() => loaded = true}
|
||||||
on:click={() => {
|
class={imgClass ?? ""}
|
||||||
|
class:cursor-zoom-in={previewedImage !== undefined}
|
||||||
|
on:click={() => {
|
||||||
previewedImage?.setData(image)
|
previewedImage?.setData(image)
|
||||||
}}
|
}}
|
||||||
on:error={() => {
|
on:error={() => {
|
||||||
if (fallbackImage) {
|
if (fallbackImage) {
|
||||||
imgEl.src = fallbackImage
|
imgEl.src = fallbackImage
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
src={image.url}
|
src={image.url}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if canZoom && loaded}
|
||||||
|
<div class="absolute right-0 top-0 bg-black-transparent rounded-bl-full">
|
||||||
|
<MagnifyingGlassPlusIcon class="w-8 h-8 pl-3 pb-3 cursor-zoom-in" color="white" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="absolute bottom-0 left-0">
|
<div class="absolute bottom-0 left-0">
|
||||||
<ImageAttribution {image} />
|
<ImageAttribution {image} {attributionFormat} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,11 +4,15 @@
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||||
import { EyeIcon } from "@rgossiaux/svelte-heroicons/solid"
|
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
|
* A small element showing the attribution of a single image
|
||||||
*/
|
*/
|
||||||
export let image: Partial<ProvidedImage> & { id: string; url: string }
|
export let image: Partial<ProvidedImage> & { id: string; url: string }
|
||||||
|
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
|
||||||
|
|
||||||
let license: Store<LicenseInfo> = UIEventSource.FromPromise(
|
let license: Store<LicenseInfo> = UIEventSource.FromPromise(
|
||||||
image.provider?.DownloadAttribution(image)
|
image.provider?.DownloadAttribution(image)
|
||||||
)
|
)
|
||||||
|
@ -16,50 +20,59 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $license !== undefined}
|
{#if $license !== undefined}
|
||||||
<div class="no-images flex items-center rounded-lg bg-black p-0.5 pl-3 pr-3 text-sm text-white">
|
<div class="no-images flex items-center rounded-lg bg-black-transparent p-0.5 px-3 text-sm text-white">
|
||||||
{#if icon !== undefined}
|
{#if icon !== undefined}
|
||||||
<div class="mr-2 h-6 w-6">
|
<div class="mr-2 h-6 w-6">
|
||||||
<ToSvelte construct={icon} />
|
<ToSvelte construct={icon} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex gap-x-2" class:flex-col={attributionFormat !== "minimal"}>
|
||||||
{#if $license.title}
|
{#if attributionFormat !== "minimal" }
|
||||||
{#if $license.informationLocation}
|
{#if $license.title}
|
||||||
<a href={$license.informationLocation.href} target="_blank" rel="noopener nofollower">
|
{#if $license.informationLocation}
|
||||||
{$license.title}
|
<a href={$license.informationLocation.href} target="_blank" rel="noopener nofollower">
|
||||||
</a>
|
{$license.title}
|
||||||
{:else}
|
</a>
|
||||||
$license.title
|
{:else}
|
||||||
|
$license.title
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $license.artist}
|
{#if $license.artist}
|
||||||
<div class="font-bold">
|
{#if attributionFormat === "large"}
|
||||||
{@html $license.artist}
|
<Tr t={Translations.t.general.attribution.madeBy.Subs({author: $license.artist})} />
|
||||||
</div>
|
{:else}
|
||||||
|
<div class="font-bold">
|
||||||
|
{@html $license.artist}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex w-full justify-between gap-x-1">
|
|
||||||
{#if $license.license !== undefined || $license.licenseShortName !== undefined}
|
|
||||||
<div>
|
|
||||||
{$license?.license ?? $license?.licenseShortName}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $license.views}
|
|
||||||
<div class="flex justify-around self-center">
|
|
||||||
<EyeIcon class="h-4 w-4 pr-1" />
|
|
||||||
{$license.views}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $license.date}
|
{#if $license.date}
|
||||||
<div>
|
<div>
|
||||||
{$license.date.toLocaleDateString()}
|
{$license.date.toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if attributionFormat !== "minimal"}
|
||||||
|
<div class="flex w-full justify-between gap-x-1">
|
||||||
|
{#if ($license.license !== undefined || $license.licenseShortName !== undefined)}
|
||||||
|
<div>
|
||||||
|
{$license?.license ?? $license?.licenseShortName}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $license.views}
|
||||||
|
<div class="flex justify-around self-center text-xs">
|
||||||
|
<EyeIcon class="h-4 w-4 pr-1" />
|
||||||
|
{$license.views}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -38,9 +38,9 @@
|
||||||
class="pointer-events-none absolute bottom-0 left-0 flex w-full flex-wrap items-end justify-between"
|
class="pointer-events-none absolute bottom-0 left-0 flex w-full flex-wrap items-end justify-between"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-auto m-1 w-fit opacity-50 transition-colors duration-200 hover:opacity-100"
|
class="pointer-events-auto m-1 w-fit transition-colors duration-200"
|
||||||
>
|
>
|
||||||
<ImageAttribution {image} />
|
<ImageAttribution {image} attributionFormat="large"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
import AttributedImage from "./AttributedImage.svelte"
|
import AttributedImage from "./AttributedImage.svelte"
|
||||||
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
|
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
|
||||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||||
|
import ImagePreview from "./ImagePreview.svelte"
|
||||||
|
import FloatOver from "../Base/FloatOver.svelte"
|
||||||
|
|
||||||
export let tags: UIEventSource<OsmTags>
|
export let tags: UIEventSource<OsmTags>
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
|
@ -31,7 +33,7 @@
|
||||||
key: undefined,
|
key: undefined,
|
||||||
provider: AllImageProviders.byName(image.provider),
|
provider: AllImageProviders.byName(image.provider),
|
||||||
date: new Date(image.date),
|
date: new Date(image.date),
|
||||||
id: Object.values(image.osmTags)[0],
|
id: Object.values(image.osmTags)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyLink(isLinked: boolean) {
|
async function applyLink(isLinked: boolean) {
|
||||||
|
@ -42,7 +44,7 @@
|
||||||
if (isLinked) {
|
if (isLinked) {
|
||||||
const action = new LinkImageAction(currentTags.id, key, url, tags, {
|
const action = new LinkImageAction(currentTags.id, key, url, tags, {
|
||||||
theme: tags.data._orig_theme ?? state.layout.id,
|
theme: tags.data._orig_theme ?? state.layout.id,
|
||||||
changeType: "link-image",
|
changeType: "link-image"
|
||||||
})
|
})
|
||||||
await state.changes.applyAction(action)
|
await state.changes.applyAction(action)
|
||||||
} else {
|
} else {
|
||||||
|
@ -51,24 +53,26 @@
|
||||||
if (v === url) {
|
if (v === url) {
|
||||||
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
|
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
|
||||||
theme: tags.data._orig_theme ?? state.layout.id,
|
theme: tags.data._orig_theme ?? state.layout.id,
|
||||||
changeType: "remove-image",
|
changeType: "remove-image"
|
||||||
})
|
})
|
||||||
state.changes.applyAction(action)
|
state.changes.applyAction(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isLinked.addCallback((isLinked) => applyLink(isLinked))
|
isLinked.addCallback((isLinked) => applyLink(isLinked))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex w-fit shrink-0 flex-col">
|
<div class="flex w-fit shrink-0 flex-col rounded-lg overflow-hidden" class:border-interactive={$isLinked}
|
||||||
<div class="cursor-zoom-in" on:click={() => state.previewedImage.setData(providedImage)}>
|
style="border-width: 2px">
|
||||||
<AttributedImage
|
<AttributedImage
|
||||||
image={providedImage}
|
image={providedImage}
|
||||||
imgClass="max-h-64 w-auto"
|
imgClass="max-h-64 w-auto"
|
||||||
previewedImage={state.previewedImage}
|
previewedImage={state.previewedImage}
|
||||||
/>
|
attributionFormat="minimal"
|
||||||
</div>
|
/>
|
||||||
<LoginToggle {state} silentFail={true}>
|
<LoginToggle {state} silentFail={true}>
|
||||||
{#if linkable}
|
{#if linkable}
|
||||||
<label>
|
<label>
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
<Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" />
|
<Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity">
|
<div class="flex w-full space-x-4 overflow-x-auto" style="scroll-snap-type: x proximity">
|
||||||
{#each $result as image (image.pictureUrl)}
|
{#each $result as image (image.pictureUrl)}
|
||||||
<span class="w-fit shrink-0" style="scroll-snap-align: start">
|
<span class="w-fit shrink-0" style="scroll-snap-align: start">
|
||||||
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
|
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
|
||||||
|
|
|
@ -352,6 +352,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
|
<!-- Search menu -->
|
||||||
{#if config.mappings?.length >= 8 || hideMappingsUnlessSearchedFor}
|
{#if config.mappings?.length >= 8 || hideMappingsUnlessSearchedFor}
|
||||||
<div class="sticky flex w-full" aria-hidden="true">
|
<div class="sticky flex w-full" aria-hidden="true">
|
||||||
<Search class="h-6 w-6" />
|
<Search class="h-6 w-6" />
|
||||||
|
@ -369,6 +370,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Actual options-->
|
||||||
{#if config?.freeform?.key && !(config?.mappings?.filter((m) => m.hideInAnswer != true)?.length > 0)}
|
{#if config?.freeform?.key && !(config?.mappings?.filter((m) => m.hideInAnswer != true)?.length > 0)}
|
||||||
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
|
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
|
||||||
<FreeformInput
|
<FreeformInput
|
||||||
|
@ -384,7 +386,7 @@
|
||||||
/>
|
/>
|
||||||
{:else if config.mappings !== undefined && !config.multiAnswer}
|
{:else if config.mappings !== undefined && !config.multiAnswer}
|
||||||
<!-- Simple radiobuttons as mapping -->
|
<!-- Simple radiobuttons as mapping -->
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col no-bold">
|
||||||
{#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-->
|
||||||
<TagRenderingMappingInput
|
<TagRenderingMappingInput
|
||||||
|
@ -432,7 +434,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if config.mappings !== undefined && config.multiAnswer}
|
{:else if config.mappings !== undefined && config.multiAnswer}
|
||||||
<!-- Multiple answers can be chosen: checkboxes -->
|
<!-- Multiple answers can be chosen: checkboxes -->
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col no-bold">
|
||||||
{#each config.mappings as mapping, i (mapping.then)}
|
{#each config.mappings as mapping, i (mapping.then)}
|
||||||
<TagRenderingMappingInput
|
<TagRenderingMappingInput
|
||||||
{mapping}
|
{mapping}
|
||||||
|
@ -475,6 +477,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Save and cancel buttons, in a logintoggle -->
|
||||||
<LoginToggle {state}>
|
<LoginToggle {state}>
|
||||||
<Loading slot="loading" />
|
<Loading slot="loading" />
|
||||||
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
|
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,6 @@ import { Translation, TypedTranslation } from "./Translation"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import CompiledTranslations from "../../assets/generated/CompiledTranslations"
|
import CompiledTranslations from "../../assets/generated/CompiledTranslations"
|
||||||
import LanguageUtils from "../../Utils/LanguageUtils"
|
import LanguageUtils from "../../Utils/LanguageUtils"
|
||||||
import { ClickableToggle } from "../Input/Toggle"
|
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
import Locale from "./Locale"
|
import Locale from "./Locale"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
|
44
src/Utils.ts
44
src/Utils.ts
|
@ -960,11 +960,15 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
if (!result["error"]) {
|
if (!result["error"]) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
console.log(result)
|
const error = result.error
|
||||||
if(result["error"]?.statuscode === 410){
|
if (error.statuscode === 410) {
|
||||||
// Gone permanently is not recoverable
|
// Gone permanently is not recoverable
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
if (error.statuscode === 429 || error.statuscode === 509) {
|
||||||
|
// rate limited
|
||||||
|
return result
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`Request to ${url} failed, Trying again in a moment. Attempt ${
|
`Request to ${url} failed, Trying again in a moment. Attempt ${
|
||||||
i + 1
|
i + 1
|
||||||
|
@ -1774,7 +1778,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static NoNullInplace<T>(items: T[]): T[] {
|
|
||||||
|
public static NoNullInplace<T>(items: T[]): T[] {
|
||||||
for (let i = items.length - 1; i >= 0; i--) {
|
for (let i = items.length - 1; i >= 0; i--) {
|
||||||
if (items[i] === null || items[i] === undefined) {
|
if (items[i] === null || items[i] === undefined) {
|
||||||
items.splice(i, 1)
|
items.splice(i, 1)
|
||||||
|
@ -1783,6 +1788,27 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes or rewrites some characters in links, as some blink/chromium based browsers are picky about them
|
||||||
|
*
|
||||||
|
* Utils.prepareHref("tel:+32 123 456") // => "tel:+32123456"
|
||||||
|
* Utils.prepareHref("https://osm.org/user/User Name") // => "https://osm.org/user/User%20Name"
|
||||||
|
*/
|
||||||
|
static prepareHref(href: string): string {
|
||||||
|
if (href.startsWith("tel:")) {
|
||||||
|
// Telephone numbers are not allowed to contain spaces in chromium-based browsers
|
||||||
|
href = "tel:" + href.replaceAll(/[^+0-9]/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chromium based browsers eat the spaces */
|
||||||
|
href = href.replaceAll(
|
||||||
|
/ /g,
|
||||||
|
"%20"
|
||||||
|
)
|
||||||
|
return href
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static emojiRegex = /[\p{Extended_Pictographic}🛰️]/u
|
private static emojiRegex = /[\p{Extended_Pictographic}🛰️]/u
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1793,7 +1819,15 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
* Utils.isEmoji("🍕") // => true
|
* Utils.isEmoji("🍕") // => true
|
||||||
*/
|
*/
|
||||||
public static isEmoji(string: string) {
|
public static isEmoji(string: string) {
|
||||||
return Utils.emojiRegex.test(string) ||
|
return Utils.emojiRegex.test(string) || Utils.isEmojiFlag(string)
|
||||||
/[🇦-🇿]{2}/u.test(string) // flags, see https://stackoverflow.com/questions/53360006/detect-with-regex-if-emoji-is-country-flag
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils.isEmoji("🍕") // => false
|
||||||
|
* Utils.isEmojiFlag("🇧🇪") // => true
|
||||||
|
*/
|
||||||
|
public static isEmojiFlag(string: string) {
|
||||||
|
return /[🇦-🇿]{2}/u.test(string) // flags, see https://stackoverflow.com/questions/53360006/detect-with-regex-if-emoji-is-country-flag
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -568,6 +568,10 @@ a:hover {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-bold b {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/************************* MISC ELEMENTS *************************/
|
/************************* MISC ELEMENTS *************************/
|
||||||
|
|
||||||
.selected svg:not(.noselect *) path.selectable {
|
.selected svg:not(.noselect *) path.selectable {
|
||||||
|
|
Loading…
Reference in a new issue