feat: add multilingual interface support
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
(function initializeI18n(globalScope) {
|
||||
"use strict";
|
||||
|
||||
const DEFAULT_LANGUAGE = "en";
|
||||
const STORAGE_KEY = "xprov-language";
|
||||
const TRANSLATIONS = {
|
||||
en: {
|
||||
languageName: "English",
|
||||
pageTitle: "XPro Map Visualizer",
|
||||
applicationMessages: "Application messages",
|
||||
openMapControls: "Open map controls",
|
||||
mapControls: "Map controls",
|
||||
closeMapControls: "Close map controls",
|
||||
mapVisualizer: "Map Visualizer",
|
||||
openSettings: "Open settings",
|
||||
closeSettings: "Close settings",
|
||||
settings: "Settings",
|
||||
applySettings: "Apply",
|
||||
useBrowserDefault: "Use browser default",
|
||||
find: "Find",
|
||||
parcelExample: "e.g. 57 or 2669.1",
|
||||
parcelNumber: "Parcel number",
|
||||
findOnMap: "Find on map",
|
||||
layers: "Layers",
|
||||
waitingForConnection: "Waiting for connection.",
|
||||
loadingPublishedLayers: "Loading published layers.",
|
||||
baseMap: "Base map",
|
||||
softMinimal: "Soft minimal",
|
||||
openStreetMap: "OpenStreetMap",
|
||||
satelliteImagery: "Satellite imagery",
|
||||
language: "Language",
|
||||
applicationLanguage: "Application language",
|
||||
mapView: "Map view",
|
||||
closeFeatureInformation: "Close feature information",
|
||||
resetNorth: "Reset map orientation to north",
|
||||
zoomVisibleLayers: "Zoom to visible layers",
|
||||
copyMapLink: "Copy link to this map view",
|
||||
clipboardUnavailable: "Clipboard access is unavailable.",
|
||||
mapLinkCopied: "Link to this map view copied to your clipboard.",
|
||||
mapLinkCopyFailed:
|
||||
"Unable to copy the map link. Please copy the address from your browser.",
|
||||
zoomArea: "Zoom to area",
|
||||
exitAreaZoom: "Exit area zoom (Esc)",
|
||||
compassRequiresHttps: "Device compass requires HTTPS; HTTP works only on localhost.",
|
||||
compassUnavailable: "Device compass is unavailable on this device.",
|
||||
compassPermissionDenied: "Device compass permission was not granted.",
|
||||
compassOn: "Device compass orientation is on.",
|
||||
compassStartFailed: "Unable to start the device compass.",
|
||||
compassOff: "Device compass orientation is off.",
|
||||
compassTurnOff: "Turn off device compass orientation",
|
||||
compassAlign: "Align map with device compass",
|
||||
dismissMessage: "Dismiss message",
|
||||
enterParcel: "Enter a parcel number to search.",
|
||||
searchingParcel: "Searching for parcel…",
|
||||
searchFailedStatus: "Parcel search failed (status {status}).",
|
||||
noParcelFound: "No parcel found with number “{parcelNumber}”.",
|
||||
foundParcel: "Found parcel {parcelNumber}.",
|
||||
foundParcels: "Found {count} parcels matching {parcelNumber}.",
|
||||
parcelSearchFailed: "Unable to search for that parcel.",
|
||||
loadingWmsLayers: "Loading WMS layers…",
|
||||
wmsLoadFailed: "Unable to load the WMS service.",
|
||||
geoserverConnectionFailed: "GeoServer connection failed (status {status}).",
|
||||
noPublishedLayers: "No published layers were returned by the service.",
|
||||
selectLayerToZoom: "Select a layer with a published extent to zoom to it.",
|
||||
parcel: "Parcel",
|
||||
building: "Building",
|
||||
area: "Area",
|
||||
areaUnavailable: "Area unavailable",
|
||||
inhabited: "Inhabited",
|
||||
viewCorrespondingBuilding: "View corresponding building",
|
||||
associatedParcels: "Associated parcels",
|
||||
viewParcel: "View parcel {parcelNumber}",
|
||||
parcelWithNumber: "Parcel {parcelNumber}",
|
||||
unavailable: "Unavailable",
|
||||
noAssociatedParcels: "No associated parcels were found.",
|
||||
zoomIn: "Zoom in",
|
||||
zoomOut: "Zoom out",
|
||||
attribution: "Attributions",
|
||||
softAttribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
satelliteAttribution:
|
||||
'Tiles © <a href="https://www.esri.com/en-us/legal/terms/full-master-agreement">Esri</a> — Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community',
|
||||
},
|
||||
es: {
|
||||
languageName: "Español",
|
||||
pageTitle: "Visualizador de Mapas XPro",
|
||||
applicationMessages: "Mensajes de la aplicación",
|
||||
openMapControls: "Abrir controles del mapa",
|
||||
mapControls: "Controles del mapa",
|
||||
closeMapControls: "Cerrar controles del mapa",
|
||||
mapVisualizer: "Visualizador de Mapas",
|
||||
openSettings: "Abrir configuración",
|
||||
closeSettings: "Cerrar configuración",
|
||||
settings: "Configuración",
|
||||
applySettings: "Aplicar",
|
||||
useBrowserDefault: "Usar el idioma predeterminado del navegador",
|
||||
find: "Buscar",
|
||||
parcelExample: "p. ej., 57 o 2669.1",
|
||||
parcelNumber: "Número de parcela",
|
||||
findOnMap: "Localizar en el mapa",
|
||||
layers: "Capas",
|
||||
waitingForConnection: "Esperando conexión.",
|
||||
loadingPublishedLayers: "Cargando capas publicadas.",
|
||||
baseMap: "Mapa base",
|
||||
softMinimal: "Minimalista suave",
|
||||
openStreetMap: "OpenStreetMap",
|
||||
satelliteImagery: "Imágenes por satélite",
|
||||
language: "Idioma",
|
||||
applicationLanguage: "Idioma de la aplicación",
|
||||
mapView: "Vista del mapa",
|
||||
closeFeatureInformation: "Cerrar información del elemento",
|
||||
resetNorth: "Restablecer la orientación del mapa hacia el norte",
|
||||
zoomVisibleLayers: "Ajustar a las capas visibles",
|
||||
copyMapLink: "Copiar enlace a esta vista del mapa",
|
||||
clipboardUnavailable: "El acceso al portapapeles no está disponible.",
|
||||
mapLinkCopied: "El enlace a esta vista se ha copiado al portapapeles.",
|
||||
mapLinkCopyFailed:
|
||||
"No se pudo copiar el enlace. Copie la dirección desde el navegador.",
|
||||
zoomArea: "Ampliar un área",
|
||||
exitAreaZoom: "Salir de la ampliación de área (Esc)",
|
||||
compassRequiresHttps:
|
||||
"La brújula del dispositivo requiere HTTPS; HTTP solo funciona en localhost.",
|
||||
compassUnavailable: "La brújula no está disponible en este dispositivo.",
|
||||
compassPermissionDenied: "No se concedió permiso para usar la brújula.",
|
||||
compassOn: "La orientación mediante la brújula está activada.",
|
||||
compassStartFailed: "No se pudo iniciar la brújula del dispositivo.",
|
||||
compassOff: "La orientación mediante la brújula está desactivada.",
|
||||
compassTurnOff: "Desactivar la orientación mediante la brújula",
|
||||
compassAlign: "Alinear el mapa con la brújula",
|
||||
dismissMessage: "Descartar mensaje",
|
||||
enterParcel: "Introduzca un número de parcela para buscar.",
|
||||
searchingParcel: "Buscando la parcela…",
|
||||
searchFailedStatus: "La búsqueda de la parcela falló (estado {status}).",
|
||||
noParcelFound: "No se encontró la parcela con el número «{parcelNumber}».",
|
||||
foundParcel: "Se encontró la parcela {parcelNumber}.",
|
||||
foundParcels: "Se encontraron {count} parcelas que coinciden con {parcelNumber}.",
|
||||
parcelSearchFailed: "No se pudo buscar esa parcela.",
|
||||
loadingWmsLayers: "Cargando capas WMS…",
|
||||
wmsLoadFailed: "No se pudo cargar el servicio WMS.",
|
||||
geoserverConnectionFailed: "La conexión con GeoServer falló (estado {status}).",
|
||||
noPublishedLayers: "El servicio no devolvió ninguna capa publicada.",
|
||||
selectLayerToZoom: "Seleccione una capa con extensión publicada para encuadrarla.",
|
||||
parcel: "Parcela",
|
||||
building: "Edificio",
|
||||
area: "Área",
|
||||
areaUnavailable: "Área no disponible",
|
||||
inhabited: "Habitada",
|
||||
viewCorrespondingBuilding: "Ver el edificio correspondiente",
|
||||
associatedParcels: "Parcelas asociadas",
|
||||
viewParcel: "Ver parcela {parcelNumber}",
|
||||
parcelWithNumber: "Parcela {parcelNumber}",
|
||||
unavailable: "No disponible",
|
||||
noAssociatedParcels: "No se encontraron parcelas asociadas.",
|
||||
zoomIn: "Acercar",
|
||||
zoomOut: "Alejar",
|
||||
attribution: "Atribuciones",
|
||||
softAttribution:
|
||||
'© Colaboradores de <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
satelliteAttribution:
|
||||
'Mosaicos © <a href="https://www.esri.com/en-us/legal/terms/full-master-agreement">Esri</a> — Fuente: Esri, Maxar, Earthstar Geographics y la comunidad de usuarios de SIG',
|
||||
},
|
||||
"pt-PT": {
|
||||
languageName: "Português",
|
||||
pageTitle: "Visualizador de Mapas XPro",
|
||||
applicationMessages: "Mensagens da aplicação",
|
||||
openMapControls: "Abrir controlos do mapa",
|
||||
mapControls: "Controlos do mapa",
|
||||
closeMapControls: "Fechar controlos do mapa",
|
||||
mapVisualizer: "Visualizador de Mapas",
|
||||
openSettings: "Abrir definições",
|
||||
closeSettings: "Fechar definições",
|
||||
settings: "Definições",
|
||||
applySettings: "Aplicar",
|
||||
useBrowserDefault: "Usar predefinição do navegador",
|
||||
find: "Pesquisar",
|
||||
parcelExample: "p. ex. 57 ou 2669.1",
|
||||
parcelNumber: "Número da parcela",
|
||||
findOnMap: "Localizar no mapa",
|
||||
layers: "Camadas",
|
||||
waitingForConnection: "A aguardar ligação.",
|
||||
loadingPublishedLayers: "A carregar camadas publicadas.",
|
||||
baseMap: "Mapa base",
|
||||
softMinimal: "Minimalista suave",
|
||||
openStreetMap: "OpenStreetMap",
|
||||
satelliteImagery: "Imagem de satélite",
|
||||
language: "Idioma",
|
||||
applicationLanguage: "Idioma da aplicação",
|
||||
mapView: "Vista do mapa",
|
||||
closeFeatureInformation: "Fechar informação do elemento",
|
||||
resetNorth: "Repor a orientação do mapa para norte",
|
||||
zoomVisibleLayers: "Ajustar às camadas visíveis",
|
||||
copyMapLink: "Copiar ligação para esta vista do mapa",
|
||||
clipboardUnavailable: "O acesso à área de transferência não está disponível.",
|
||||
mapLinkCopied: "A ligação para esta vista foi copiada para a área de transferência.",
|
||||
mapLinkCopyFailed:
|
||||
"Não foi possível copiar a ligação. Copie o endereço a partir do navegador.",
|
||||
zoomArea: "Ampliar uma área",
|
||||
exitAreaZoom: "Sair da ampliação de área (Esc)",
|
||||
compassRequiresHttps:
|
||||
"A bússola do dispositivo requer HTTPS; HTTP funciona apenas em localhost.",
|
||||
compassUnavailable: "A bússola não está disponível neste dispositivo.",
|
||||
compassPermissionDenied: "A permissão para usar a bússola não foi concedida.",
|
||||
compassOn: "A orientação pela bússola está ativa.",
|
||||
compassStartFailed: "Não foi possível iniciar a bússola do dispositivo.",
|
||||
compassOff: "A orientação pela bússola está desativada.",
|
||||
compassTurnOff: "Desativar orientação pela bússola",
|
||||
compassAlign: "Alinhar mapa com a bússola",
|
||||
dismissMessage: "Dispensar mensagem",
|
||||
enterParcel: "Introduza um número de parcela para pesquisar.",
|
||||
searchingParcel: "A pesquisar a parcela…",
|
||||
searchFailedStatus: "A pesquisa da parcela falhou (estado {status}).",
|
||||
noParcelFound: "Não foi encontrada a parcela com o número “{parcelNumber}”.",
|
||||
foundParcel: "Parcela {parcelNumber} encontrada.",
|
||||
foundParcels: "Foram encontradas {count} parcelas correspondentes a {parcelNumber}.",
|
||||
parcelSearchFailed: "Não foi possível pesquisar essa parcela.",
|
||||
loadingWmsLayers: "A carregar camadas WMS…",
|
||||
wmsLoadFailed: "Não foi possível carregar o serviço WMS.",
|
||||
geoserverConnectionFailed: "A ligação ao GeoServer falhou (estado {status}).",
|
||||
noPublishedLayers: "O serviço não devolveu camadas publicadas.",
|
||||
selectLayerToZoom: "Selecione uma camada com extensão publicada para a enquadrar.",
|
||||
parcel: "Parcela",
|
||||
building: "Prédio",
|
||||
area: "Área",
|
||||
areaUnavailable: "Área indisponível",
|
||||
inhabited: "Habitada",
|
||||
viewCorrespondingBuilding: "Ver prédio correspondente",
|
||||
associatedParcels: "Parcelas associadas",
|
||||
viewParcel: "Ver parcela {parcelNumber}",
|
||||
parcelWithNumber: "Parcela {parcelNumber}",
|
||||
unavailable: "Indisponível",
|
||||
noAssociatedParcels: "Não foram encontradas parcelas associadas.",
|
||||
zoomIn: "Ampliar",
|
||||
zoomOut: "Reduzir",
|
||||
attribution: "Atribuições",
|
||||
softAttribution:
|
||||
'© Colaboradores do <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
satelliteAttribution:
|
||||
'Mosaicos © <a href="https://www.esri.com/en-us/legal/terms/full-master-agreement">Esri</a> — Fonte: Esri, Maxar, Earthstar Geographics e comunidade de utilizadores de SIG',
|
||||
},
|
||||
};
|
||||
|
||||
function normalizeLanguage(language) {
|
||||
if (typeof language !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = language.replace("_", "-").toLowerCase();
|
||||
return Object.keys(TRANSLATIONS).find((candidate) => {
|
||||
const candidateNormalized = candidate.toLowerCase();
|
||||
return candidateNormalized === normalized || candidateNormalized.split("-")[0] === normalized.split("-")[0];
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function readStoredLanguage(storage) {
|
||||
try {
|
||||
return normalizeLanguage(storage?.getItem(STORAGE_KEY));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function detectLanguage({ storage, languages = [] } = {}) {
|
||||
const storedLanguage = readStoredLanguage(storage);
|
||||
if (storedLanguage) {
|
||||
return storedLanguage;
|
||||
}
|
||||
|
||||
for (const language of languages) {
|
||||
const supportedLanguage = normalizeLanguage(language);
|
||||
if (supportedLanguage) {
|
||||
return supportedLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
function detectBrowserLanguage(languages = []) {
|
||||
for (const language of languages) {
|
||||
const supportedLanguage = normalizeLanguage(language);
|
||||
if (supportedLanguage) {
|
||||
return supportedLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
function createI18n({
|
||||
storage = globalScope.localStorage,
|
||||
languages = globalScope.navigator?.languages || [globalScope.navigator?.language],
|
||||
} = {}) {
|
||||
const browserLanguage = detectBrowserLanguage(languages);
|
||||
let preference = readStoredLanguage(storage);
|
||||
let language = preference || browserLanguage;
|
||||
|
||||
function translate(key, values = {}) {
|
||||
const template = TRANSLATIONS[language]?.[key]
|
||||
?? TRANSLATIONS[DEFAULT_LANGUAGE]?.[key]
|
||||
?? key;
|
||||
return template.replace(/\{(\w+)\}/g, (match, name) =>
|
||||
Object.hasOwn(values, name) ? String(values[name]) : match,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
get language() {
|
||||
return language;
|
||||
},
|
||||
get locale() {
|
||||
return language;
|
||||
},
|
||||
get preference() {
|
||||
return preference;
|
||||
},
|
||||
get supportedLanguages() {
|
||||
return Object.keys(TRANSLATIONS);
|
||||
},
|
||||
setLanguage(nextLanguage) {
|
||||
const useBrowserDefault = nextLanguage == null || nextLanguage === "browser";
|
||||
preference = useBrowserDefault
|
||||
? null
|
||||
: normalizeLanguage(nextLanguage) || DEFAULT_LANGUAGE;
|
||||
language = preference || browserLanguage;
|
||||
try {
|
||||
if (preference) {
|
||||
storage?.setItem(STORAGE_KEY, preference);
|
||||
} else {
|
||||
storage?.removeItem(STORAGE_KEY);
|
||||
}
|
||||
} catch {
|
||||
// A blocked storage API must not prevent language selection.
|
||||
}
|
||||
return language;
|
||||
},
|
||||
t: translate,
|
||||
tp(singularKey, pluralKey, count, values = {}) {
|
||||
return translate(count === 1 ? singularKey : pluralKey, { ...values, count });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const exported = {
|
||||
DEFAULT_LANGUAGE,
|
||||
STORAGE_KEY,
|
||||
TRANSLATIONS,
|
||||
normalizeLanguage,
|
||||
detectLanguage,
|
||||
detectBrowserLanguage,
|
||||
createI18n,
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = exported;
|
||||
}
|
||||
|
||||
globalScope.XProI18n = exported;
|
||||
})(typeof window === "undefined" ? globalThis : window);
|
||||
Reference in New Issue
Block a user