From 3ef6a73791a3f6870833121fff1062ac3456322b Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Tue, 12 May 2026 16:47:40 +0200 Subject: [PATCH 1/2] Fix #11930 Add title translations and tests --- web/client/utils/GeoNodeUtils.js | 30 ++- web/client/utils/LocaleUtils.js | 33 +++ .../utils/__tests__/GeoNodeUtils-test.js | 228 ++++++++++++++++++ .../utils/__tests__/LocaleUtils-test.js | 10 + 4 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 web/client/utils/__tests__/GeoNodeUtils-test.js diff --git a/web/client/utils/GeoNodeUtils.js b/web/client/utils/GeoNodeUtils.js index 3e4971b61f3..cc23c6697fd 100644 --- a/web/client/utils/GeoNodeUtils.js +++ b/web/client/utils/GeoNodeUtils.js @@ -8,6 +8,7 @@ import { isImageServerUrl } from './ArcGISUtils'; import { getConfigProp } from './ConfigUtils'; +import { getSupportedLocales, shortLocale } from './LocaleUtils'; import uuid from 'uuid'; import { isEmpty } from 'lodash'; import queryString from 'query-string'; @@ -278,6 +279,31 @@ function getExtentFromResource({ extent }) { return bbox; } +const getLocalizedValue = (resource, key, locale = '') => { + if (resource[`${key}_${locale}`]) { + return resource[`${key}_${locale}`]; + } + const lang = shortLocale(locale); + if (lang && resource[`${key}_${lang}`]) { + return resource[`${key}_${lang}`]; + } + return null; +}; + +const getLocalizedValues = (resource, key, defaultValue) => { + const supportedLocales = getSupportedLocales() || {}; + const translations = Object.values(supportedLocales) + .map(({ code }) => { + const value = getLocalizedValue(resource, key, code); + return value ? [code, value] : null; + }) + .filter(value => value !== null); + if (translations.length) { + return { ...Object.fromEntries(translations), 'default': defaultValue }; + } + return defaultValue; +}; + /** * convert resource layer configuration to a mapstore layer object * @param {object} resource geonode layer resource @@ -289,7 +315,7 @@ export const resourceToLayerConfig = (resource, options) => { alternate, links = [], featureinfo_custom_template: template, - title, + title: defaultTitle, perms, pk, default_style: defaultStyle, @@ -301,6 +327,8 @@ export const resourceToLayerConfig = (resource, options) => { const layerSettings = data?.layerSettings ?? data; + const title = getLocalizedValues(resource, 'title', defaultTitle); + const bbox = getExtentFromResource(resource); const defaultStyleParams = defaultStyle && { defaultStyle: { diff --git a/web/client/utils/LocaleUtils.js b/web/client/utils/LocaleUtils.js index 65c536a75b4..b3e6e3e034e 100644 --- a/web/client/utils/LocaleUtils.js +++ b/web/client/utils/LocaleUtils.js @@ -201,6 +201,39 @@ export const getErrorMessage = (e, service, section) => { */ export const getLocalizedProp = (locale, prop) => isObject(prop) ? prop[locale] || prop.default : prop || ''; +/** + * Returns the normalized locale code in language-region format (e.g. 'en' → 'en-US', 'en-GB' → 'en-GB'). + * Returns an empty string if the code is invalid or missing. + * @param {string} code locale code + * @returns {string} + */ +export const longLocale = (code) => { + if (!code) return ''; + try { + const loc = new Intl.Locale(code); + if (loc.region) return `${loc.language}-${loc.region}`; + const maximized = loc.maximize(); + return `${maximized.language}-${maximized.region}`; + } catch { + return ''; + } +}; + +/** + * Returns the language component of a locale code (e.g. 'en-US' → 'en'). + * Returns an empty string if the code is invalid or missing. + * @param {string} code locale code + * @returns {string} + */ +export const shortLocale = (code) => { + if (!code) return ''; + try { + return new Intl.Locale(code).language; + } catch { + return ''; + } +}; + LocaleUtils = { getLocale, normalizeLocaleCode diff --git a/web/client/utils/__tests__/GeoNodeUtils-test.js b/web/client/utils/__tests__/GeoNodeUtils-test.js new file mode 100644 index 00000000000..fd280598625 --- /dev/null +++ b/web/client/utils/__tests__/GeoNodeUtils-test.js @@ -0,0 +1,228 @@ +/* + * Copyright 2025, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import expect from 'expect'; + +import { setSupportedLocales } from '../LocaleUtils'; +import { resourceToLayerConfig, getDimensions } from '../GeoNodeUtils'; + +describe('GeoNodeUtils', () => { + describe('resourceToLayerConfig', () => { + it('should keep the wms params from the url if available', () => { + const newLayer = resourceToLayerConfig({ + alternate: 'geonode:layer_name', + links: [{ + extension: 'html', + link_type: 'OGC:WMS', + name: 'OGC WMS Service', + mime: 'text/html', + url: 'http://localhost:8080/geoserver/wms?map=name&map_resolution=91' + }], + title: 'Layer title', + perms: [], + pk: 1 + }); + expect(newLayer.params).toEqual({ map: 'name', map_resolution: '91' }); + }); + + it('should apply layer settings from dataset data', () => { + const newLayer = resourceToLayerConfig({ + alternate: 'geonode:layer_name', + links: [{ + extension: 'html', + link_type: 'OGC:WMS', + name: 'OGC WMS Service', + mime: 'text/html', + url: 'http://localhost:8080/geoserver/wms?map=name&map_resolution=91' + }], + title: 'Layer title', + perms: [], + pk: 1, + data: {opacity: 0.8} + }); + expect(newLayer.opacity).toBe(0.8); + }); + + it('should parse arcgis dataset', () => { + const newLayer = resourceToLayerConfig({ + alternate: 'remoteWorkspace:1', + title: 'Layer title', + perms: [], + links: [{ + extension: 'html', + link_type: 'image', + mime: 'text/html', + name: 'ArcGIS REST ImageServer', + url: 'http://localhost:8080/MapServer' + }], + pk: 1, + ptype: 'gxp_arcrestsource' + }); + expect(newLayer.type).toBe('arcgis'); + expect(newLayer.name).toBe('1'); + expect(newLayer.url).toBe('http://localhost:8080/MapServer'); + }); + + it('should return localized title object when supported locales have translations', () => { + setSupportedLocales({ + 'en': { code: 'en-US', description: 'English' }, + 'it': { code: 'it-IT', description: 'Italiano' }, + 'fr': { code: 'fr-FR', description: 'Français' } + }); + const newLayer = resourceToLayerConfig({ + alternate: 'geonode:layer_numtilangue', + title: 'Default title', + title_en: 'Layer title', + title_it: 'Titolo del layer', + title_fr: 'Titre de la couche', + perms: [], + pk: 1 + }); + expect(newLayer.title).toEqual({ + 'en-US': 'Layer title', + 'it-IT': 'Titolo del layer', + 'fr-FR': 'Titre de la couche', + 'default': 'Default title' + }); + }); + + it('should return plain title string when no locale translations exist', () => { + setSupportedLocales({}); + const newLayer = resourceToLayerConfig({ + alternate: 'geonode:layer_name', + title: 'Plain title', + links: [{ link_type: 'OGC:WMS', url: '/geoserver/wms' }], + perms: [], + pk: 1 + }); + expect(newLayer.title).toBe('Plain title'); + }); + + describe('alternate in extendedParams', () => { + it('WMS layer includes alternate in extendedParams', () => { + const newLayer = resourceToLayerConfig({ + alternate: 'geonode:layer_name', + links: [{ + extension: 'html', + link_type: 'OGC:WMS', + name: 'OGC WMS Service', + mime: 'text/html', + url: '/geoserver/wms' + }], + title: 'Layer title', + perms: [], + pk: 1 + }); + expect(newLayer.extendedParams).toEqual({ pk: 1, alternate: 'geonode:layer_name' }); + }); + + it('3dtiles layer includes alternate in extendedParams', () => { + const newLayer = resourceToLayerConfig({ + alternate: 'geonode:tileset', + subtype: '3dtiles', + links: [{ extension: '3dtiles', url: '/tileset.json' }], + title: 'Tileset', + perms: [], + pk: 2 + }); + expect(newLayer.extendedParams).toEqual({ pk: 2, alternate: 'geonode:tileset' }); + }); + + it('cog layer includes alternate in extendedParams', () => { + const newLayer = resourceToLayerConfig({ + alternate: 'geonode:cog_layer', + subtype: 'cog', + links: [{ extension: 'cog', url: '/raster.tif' }], + title: 'COG', + perms: [], + pk: 3 + }); + expect(newLayer.extendedParams).toEqual({ pk: 3, alternate: 'geonode:cog_layer' }); + }); + + it('flatgeobuf layer includes alternate in extendedParams', () => { + const newLayer = resourceToLayerConfig({ + alternate: 'geonode:fgb_layer', + subtype: 'flatgeobuf', + links: [{ extension: 'flatgeobuf', url: '/data.fgb' }], + title: 'FGB', + perms: [], + pk: 4 + }); + expect(newLayer.extendedParams).toEqual({ pk: 4, alternate: 'geonode:fgb_layer' }); + }); + + it('arcgis layer includes alternate in extendedParams', () => { + const newLayer = resourceToLayerConfig({ + alternate: 'remoteWorkspace:1', + title: 'Layer title', + perms: [], + links: [{ + extension: 'html', + link_type: 'image', + mime: 'text/html', + name: 'ArcGIS REST ImageServer', + url: '/MapServer' + }], + pk: 5, + ptype: 'gxp_arcrestsource' + }); + expect(newLayer.extendedParams).toEqual({ pk: 5, alternate: 'remoteWorkspace:1' }); + }); + }); + }); + + describe('getDimensions', () => { + it('should return empty array if no links and has_time is false', () => { + const result = getDimensions(); + expect(result).toEqual([]); + }); + + it('should return dimensions with time if has_time is true and WMTS link is present', () => { + const links = [{ link_type: 'OGC:WMTS', url: 'http://example.com/wmts' }]; + const result = getDimensions({ links, has_time: true }); + expect(result).toEqual([{ + name: 'time', + source: { + type: 'multidim-extension', + url: 'http://example.com/wmts' + } + }]); + }); + + it('should return dimensions with time if has_time is true and only WMS link is present', () => { + const links = [{ link_type: 'OGC:WMS', url: 'http://example.com/geoserver/wms' }]; + const result = getDimensions({ links, has_time: true }); + expect(result).toEqual([{ + name: 'time', + source: { + type: 'multidim-extension', + url: 'http://example.com/geoserver/gwc/service/wmts' + } + }]); + }); + + it('should return empty array if has_time is false', () => { + const links = [{ link_type: 'OGC:WMTS', url: 'http://example.com/wmts' }]; + const result = getDimensions({ links, has_time: false }); + expect(result).toEqual([]); + }); + + it('should return default url if no matching link types are found', () => { + const links = [{ link_type: 'OGC:OTHER', url: 'http://example.com/other' }]; + const result = getDimensions({ links, has_time: true }); + expect(result).toEqual([{ + name: 'time', + source: { + type: 'multidim-extension', + url: '/geoserver/gwc/service/wmts' + } + }]); + }); + }); +}); diff --git a/web/client/utils/__tests__/LocaleUtils-test.js b/web/client/utils/__tests__/LocaleUtils-test.js index 74a07511c58..194e615d392 100644 --- a/web/client/utils/__tests__/LocaleUtils-test.js +++ b/web/client/utils/__tests__/LocaleUtils-test.js @@ -96,4 +96,14 @@ describe('LocaleUtils', () => { expect(Object.keys(LocaleUtils.DATE_FORMATS).length).toBe(10); expect(Object.keys(LocaleUtils.DATE_FORMATS)).toEqual(["default", "en-US", "it-IT", "nl-NL", "zh-ZH", "hr-HR", "pt-PT", "pt-BR", "vi-VN", "fi-FI"]); }); + it('longLocale should return language-region format', () => { + expect(LocaleUtils.longLocale('en')).toMatch(/en-[A-Z]{2}/); + expect(LocaleUtils.longLocale('en-GB')).toBe('en-GB'); + expect(LocaleUtils.longLocale('invalid code')).toBe(''); + }); + it('shortLocale should return the language component of a locale code', () => { + expect(LocaleUtils.shortLocale('en-US')).toBe('en'); + expect(LocaleUtils.shortLocale('it-IT')).toBe('it'); + expect(LocaleUtils.shortLocale('invalid code')).toBe(''); + }); }); From dedac263bedf4977fffb296fd859eaf40ff2f405 Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Tue, 12 May 2026 18:05:51 +0200 Subject: [PATCH 2/2] fix failing tests --- web/client/utils/__tests__/GeoNodeUtils-test.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/client/utils/__tests__/GeoNodeUtils-test.js b/web/client/utils/__tests__/GeoNodeUtils-test.js index fd280598625..027f14e3448 100644 --- a/web/client/utils/__tests__/GeoNodeUtils-test.js +++ b/web/client/utils/__tests__/GeoNodeUtils-test.js @@ -8,11 +8,20 @@ import expect from 'expect'; -import { setSupportedLocales } from '../LocaleUtils'; +import { setSupportedLocales, getSupportedLocales } from '../LocaleUtils'; import { resourceToLayerConfig, getDimensions } from '../GeoNodeUtils'; describe('GeoNodeUtils', () => { describe('resourceToLayerConfig', () => { + + let originalLocales; + beforeEach(() => { + originalLocales = getSupportedLocales(); + }); + afterEach(() => { + setSupportedLocales(originalLocales); + }); + it('should keep the wms params from the url if available', () => { const newLayer = resourceToLayerConfig({ alternate: 'geonode:layer_name',