diff --git a/geonode_mapstore_client/client/js/api/geonode/v2/index.js b/geonode_mapstore_client/client/js/api/geonode/v2/index.js index 3608748402..3de8d7f533 100644 --- a/geonode_mapstore_client/client/js/api/geonode/v2/index.js +++ b/geonode_mapstore_client/client/js/api/geonode/v2/index.js @@ -258,7 +258,8 @@ export const setFavoriteResource = (pk, favorite) => { export const getResourceByPk = (pk) => { return axios.get(getEndpointUrl(RESOURCES, `/${pk}`), { params: { - api_preset: API_PRESET.VIEWER_COMMON + api_preset: API_PRESET.VIEWER_COMMON, + include_i18n: true } }) .then(({ data }) => data.resource); @@ -305,7 +306,8 @@ export const getResourceByUuid = (uuid) => { export const getDatasetByPk = (pk) => { return axios.get(getEndpointUrl(DATASETS, `/${pk}`), { params: { - api_preset: [API_PRESET.VIEWER_COMMON, API_PRESET.DATASET] + api_preset: [API_PRESET.VIEWER_COMMON, API_PRESET.DATASET], + include_i18n: true }, ...paramsSerializer() }) @@ -570,7 +572,8 @@ export const getDatasetByName = name => { return axios.get(url, { params: { exclude: ['*'], - include: ['pk', 'perms', 'alternate'] + include: ['pk', 'perms', 'alternate'], + include_i18n: true } }) .then(({data}) => data?.datasets[0]); diff --git a/geonode_mapstore_client/client/js/epics/__tests__/gnsave-test.js b/geonode_mapstore_client/client/js/epics/__tests__/gnsave-test.js index d75436f0db..02e0f5e8eb 100644 --- a/geonode_mapstore_client/client/js/epics/__tests__/gnsave-test.js +++ b/geonode_mapstore_client/client/js/epics/__tests__/gnsave-test.js @@ -257,6 +257,26 @@ describe('gnsave epics', () => { {layers: {flat: [{name: "testLayer", id: "test_id", perms: ['download_resourcebase']}], selected: ["test_id"]}}); }); + it('test gnSetDatasetsPermissions trigger updateNode for ADD_LAYER and set title in multilanguage', (done) => { + mockAxios.onGet().reply(() => [200, + {datasets: [{title: 'testLayer Default', title_en: 'testLayer EN', title_it: 'testLayerIT'}]}]); + const NUM_ACTIONS = 1; + testEpic(gnSetDatasetsPermissions, NUM_ACTIONS, addLayer({name: "testLayer", pk: "1", extendedParams: {pk: "1"}}), (actions) => { + try { + expect(actions.map(({type}) => type)).toEqual(["UPDATE_NODE"]); + expect(actions[0].options.title).toEqual({ + "default": 'testLayer Default', + 'en-US': 'testLayer EN', + 'it-IT': 'testLayerIT' + }); + done(); + } catch (error) { + done(error); + } + }, + {layers: {flat: [{name: "testLayer", id: "test_id", perms: ['download_resourcebase']}], selected: ["test_id"]}}); + }); + it('should trigger saveResource (gnSaveDirectContent)', (done) => { const NUM_ACTIONS = 3; const pk = 1; diff --git a/geonode_mapstore_client/client/js/utils/AppUtils.js b/geonode_mapstore_client/client/js/utils/AppUtils.js index 5d5ac3551c..0cccd73088 100644 --- a/geonode_mapstore_client/client/js/utils/AppUtils.js +++ b/geonode_mapstore_client/client/js/utils/AppUtils.js @@ -321,14 +321,16 @@ export function setupConfiguration({ settings: localConfig.geoNodeSettings, MapStoreAPI: window.MapStoreAPI, onStoreInit: (store) => { - store.addActionListener((action) => { - const act = action.type === 'PERFORM_ACTION' && action.action || action; // Needed to works also in debug - (actionListeners[act.type] || []) - .concat(actionListeners['*'] || []) - .forEach((listener) => { - listener.call(null, act); - }); - }); + if (store.addActionListener) { + store.addActionListener((action) => { + const act = action.type === 'PERFORM_ACTION' && action.action || action; // Needed to works also in debug + (actionListeners[act.type] || []) + .concat(actionListeners['*'] || []) + .forEach((listener) => { + listener.call(null, act); + }); + }); + } }, configEpics: { gnMapStoreApiEpic: actionTrigger.epic diff --git a/geonode_mapstore_client/client/js/utils/LocaleUtils.js b/geonode_mapstore_client/client/js/utils/LocaleUtils.js new file mode 100644 index 0000000000..2408b55bfb --- /dev/null +++ b/geonode_mapstore_client/client/js/utils/LocaleUtils.js @@ -0,0 +1,62 @@ +/* + * Copyright 2026, 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. + */ + + +/** +* @module utils/LocaleUtils +*/ + +/** + * Normalizes a locale code to a standard format (language-region). + * If the locale code has a region, it returns the language-region combination. + * Otherwise, it maximizes the locale to include region information. + * + * @param {string} code - The locale code to normalize (e.g., 'en', 'en-US', 'it') + * @returns {string} The normalized locale code in language-region format, or an empty string if normalization fails + * + * @example + * longLocale('en'); // Returns 'en-US' (or similar based on system locale) + * longLocale('en-GB'); // Returns 'en-GB' + * longLocale('invalid code'); // Returns '' + * + * shortLocale('en-US'); // Returns 'en' + * shortLocale('it-IT'); // Returns 'it' + * shortLocale('invalid code'); // Returns '' + */ + +/** + * return the normalized locale code in language-region format, 5 chars length e.g. 'en-US', 'it-IT' + * @param {*} code + * @returns {string} The normalized locale code in language-region format, or an empty string if normalization fails + */ +export function 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 ''; + } +} + +/** + * return the language component of a locale code, or an empty string if shortening fails, length is 2 chars e.g. 'en', 'it' + * @param {string} code - The locale code to shorten (e.g., 'en-US', 'it-IT') + * @returns {string} The language component of the locale code, or an empty string if shortening fails + */ +export function shortLocale(code) { + if (!code) return ''; + try { + const loc = new Intl.Locale(code); + return loc.language; + } catch { + return ''; + } +} \ No newline at end of file diff --git a/geonode_mapstore_client/client/js/utils/ResourceUtils.js b/geonode_mapstore_client/client/js/utils/ResourceUtils.js index c411fd8740..487e77fa75 100644 --- a/geonode_mapstore_client/client/js/utils/ResourceUtils.js +++ b/geonode_mapstore_client/client/js/utils/ResourceUtils.js @@ -18,6 +18,7 @@ import { getGeoNodeLocalConfig, parseDevHostname } from '@js/utils/APIUtils'; import { ProcessTypes, ProcessStatus } from '@js/utils/ResourceServiceUtils'; import { determineResourceType } from '@js/utils/FileUtils'; import { createDefaultStyle } from '@mapstore/framework/utils/StyleUtils'; +import { getSupportedLocales } from '@mapstore/framework/utils/LocaleUtils'; /** * @module utils/ResourceUtils */ @@ -153,6 +154,35 @@ export const getDimensions = ({links, has_time: hasTime} = {}) => { return dimensions; }; +const getLocalizedValue = (resource, key, locale = '') => { + if (resource[`${key}_${locale}`]) { + return resource[`${key}_${locale}`]; + } + const languageCode = locale.split('-')[0]; + if (resource[`${key}_${languageCode}`]) { + return resource[`${key}_${languageCode}`]; + } + return null; +}; + +export 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 @@ -165,7 +195,7 @@ export const resourceToLayerConfig = (resource) => { attribute_set: attributeSet = [], links = [], featureinfo_custom_template: template, - title, + title: defaultTitle, perms, pk, default_style: defaultStyle, @@ -175,6 +205,8 @@ export const resourceToLayerConfig = (resource) => { data: layerSettings } = resource; + const title = getLocalizedValues(resource, 'title', defaultTitle); + const bbox = getExtentFromResource(resource); const defaultStyleParams = defaultStyle && { defaultStyle: {