From ea1e914bd3189901bfc1c5fef25295a88d2ec902 Mon Sep 17 00:00:00 2001 From: mahula Date: Wed, 8 Apr 2026 09:43:58 +0200 Subject: [PATCH 1/6] feat(map): use frontend language for OpenHistoricalMap labels When the OpenHistoricalMap layer is selected, append the language parameter (?language=) to the style URL so that map labels are displayed in the user's preferred language matching the frontend locale setting. Fixes gramps-project/gramps-web#493 --- src/components/GrampsjsMap.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/GrampsjsMap.js b/src/components/GrampsjsMap.js index b6e8aae5..f280d218 100644 --- a/src/components/GrampsjsMap.js +++ b/src/components/GrampsjsMap.js @@ -264,7 +264,11 @@ class GrampsjsMap extends GrampsjsAppStateMixin(LitElement) { const theme = this.appState.getCurrentTheme() const mapBaseStyle = theme === 'dark' ? config.mapBaseStyleDark : config.mapBaseStyleLight - return style === 'base' ? mapBaseStyle : config.mapOhmStyle + if (style === 'ohm') { + const lang = this.appState.i18n?.lang || 'en' + return `${config.mapOhmStyle}?language=${lang}` + } + return mapBaseStyle } } From 4d102cab0d8d56a8bdb4116477aef13496549ac5 Mon Sep 17 00:00:00 2001 From: mahula Date: Wed, 8 Apr 2026 10:16:59 +0200 Subject: [PATCH 2/6] feat(map): add robust language handling for OpenHistoricalMap labels - Add OHM_LOCALE_MAP in util.js for mapping frontend locales (de_AT, en_GB, pt_BR, etc.) to OpenHistoricalMap-compatible ISO 639-1 codes (de, en, pt) - Add normalizeOhmLocale() function with explicit fallback chain - Add debug logging when OHM style is requested showing the locale mapping - Add comprehensive unit tests covering: - Base language code mapping - Regional variant normalization - Fallback behavior for undefined/null/empty/unknown locales - OHM_LOCALE_MAP completeness validation Fixes gramps-project/gramps-web#493 --- src/components/GrampsjsMap.js | 12 ++- src/util.js | 75 ++++++++++++++++ test/unit/mapOhmLocale.test.js | 158 +++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 test/unit/mapOhmLocale.test.js diff --git a/src/components/GrampsjsMap.js b/src/components/GrampsjsMap.js index f280d218..8dc3b27b 100644 --- a/src/components/GrampsjsMap.js +++ b/src/components/GrampsjsMap.js @@ -11,7 +11,7 @@ import './GrampsjsMapOverlay.js' import './GrampsjsMapMarker.js' import './GrampsjsMapLayerSwitcher.js' import './GrampsjsIcon.js' -import {fireEvent} from '../util.js' +import {fireEvent, normalizeOhmLocale} from '../util.js' import {sharedStyles} from '../SharedStyles.js' import {GrampsjsAppStateMixin} from '../mixins/GrampsjsAppStateMixin.js' @@ -265,8 +265,14 @@ class GrampsjsMap extends GrampsjsAppStateMixin(LitElement) { const mapBaseStyle = theme === 'dark' ? config.mapBaseStyleDark : config.mapBaseStyleLight if (style === 'ohm') { - const lang = this.appState.i18n?.lang || 'en' - return `${config.mapOhmStyle}?language=${lang}` + const locale = this.appState.i18n?.lang + const ohmLocale = normalizeOhmLocale(locale) + // Log the language request for debugging purposes + // eslint-disable-next-line no-console + console.debug( + `OpenHistoricalMap: requesting labels in locale "${ohmLocale}" (frontend locale: "${locale}")` + ) + return `${config.mapOhmStyle}?language=${ohmLocale}` } return mapBaseStyle } diff --git a/src/util.js b/src/util.js index ce831f92..d545eaef 100644 --- a/src/util.js +++ b/src/util.js @@ -976,4 +976,79 @@ export function apiVersionAtLeast(dbInfo, major, minor, patch = 0) { return pat >= patch } +/** + * Mapping from frontend locale codes to OpenHistoricalMap-supported ISO 639-1 codes. + * OpenHistoricalMap does not support regional variants (e.g., de_AT, en_GB, pt_BR), + * so we map them to their base language codes. + * @see https://github.com/gramps-project/gramps-web/issues/493 + */ +export const OHM_LOCALE_MAP = { + ar: 'ar', + ba: 'ba', + bg: 'bg', + br: 'br', + ca: 'ca', + cs: 'cs', + da: 'da', + de_AT: 'de', + de: 'de', + el: 'el', + en_GB: 'en', + en: 'en', + eo: 'eo', + es: 'es', + fi: 'fi', + fr: 'fr', + ga: 'ga', + he: 'he', + hr: 'hr', + hu: 'hu', + is: 'is', + id: 'id', + it: 'it', + ja: 'ja', + ko: 'ko', + lt: 'lt', + lv: 'lv', + mk: 'mk', + nb: 'nb', + nl: 'nl', + nn: 'nn', + pl: 'pl', + pt_BR: 'pt', + pt_PT: 'pt', + ro: 'ro', + ru: 'ru', + sk: 'sk', + sl: 'sl', + sq: 'sq', + sr: 'sr', + sv: 'sv', + ta: 'ta', + tr: 'tr', + uk: 'uk', + vi: 'vi', + zh_CN: 'zh', + zh_HK: 'zh', + zh_TW: 'zh', +} + +/** + * Converts a frontend locale code to an OpenHistoricalMap-compatible ISO 639-1 code. + * Falls back to 'en' if the locale is unknown or unsupported. + * @param {string|undefined} locale - Frontend locale code (e.g., 'de_AT', 'en_GB') + * @returns {string} OHM-compatible language code (e.g., 'de', 'en') + */ +export function normalizeOhmLocale(locale) { + if (!locale) { + return 'en' + } + const mapped = OHM_LOCALE_MAP[locale] + if (mapped !== undefined) { + return mapped + } + const baseCode = locale.split('_')[0] + return baseCode || 'en' +} + // diff --git a/test/unit/mapOhmLocale.test.js b/test/unit/mapOhmLocale.test.js new file mode 100644 index 00000000..22551b3b --- /dev/null +++ b/test/unit/mapOhmLocale.test.js @@ -0,0 +1,158 @@ +import {describe, it, expect} from 'vitest' +import {normalizeOhmLocale, OHM_LOCALE_MAP} from '../../src/util.js' + +describe('normalizeOhmLocale', () => { + describe('basic mapping', () => { + it('maps base language codes to themselves', () => { + expect(normalizeOhmLocale('de')).to.equal('de') + expect(normalizeOhmLocale('en')).to.equal('en') + expect(normalizeOhmLocale('fr')).to.equal('fr') + expect(normalizeOhmLocale('es')).to.equal('es') + expect(normalizeOhmLocale('it')).to.equal('it') + expect(normalizeOhmLocale('pl')).to.equal('pl') + expect(normalizeOhmLocale('ru')).to.equal('ru') + expect(normalizeOhmLocale('ja')).to.equal('ja') + expect(normalizeOhmLocale('zh')).to.equal('zh') + }) + + it('maps regional variants to base language codes', () => { + expect(normalizeOhmLocale('de_AT')).to.equal('de') + expect(normalizeOhmLocale('de_DE')).to.equal('de') + expect(normalizeOhmLocale('en_GB')).to.equal('en') + expect(normalizeOhmLocale('en_US')).to.equal('en') + expect(normalizeOhmLocale('pt_BR')).to.equal('pt') + expect(normalizeOhmLocale('pt_PT')).to.equal('pt') + expect(normalizeOhmLocale('zh_CN')).to.equal('zh') + expect(normalizeOhmLocale('zh_HK')).to.equal('zh') + expect(normalizeOhmLocale('zh_TW')).to.equal('zh') + }) + + it('maps all supported frontend locales listed in OHM_LOCALE_MAP', () => { + Object.entries(OHM_LOCALE_MAP).forEach( + ([frontendLocale, expectedOhmLocale]) => { + const result = normalizeOhmLocale(frontendLocale) + expect(result).to.equal(expectedOhmLocale) + } + ) + }) + }) + + describe('fallback behavior', () => { + it('defaults to "en" when locale is undefined', () => { + expect(normalizeOhmLocale(undefined)).to.equal('en') + }) + + it('defaults to "en" when locale is null', () => { + expect(normalizeOhmLocale(null)).to.equal('en') + }) + + it('defaults to "en" when locale is empty string', () => { + expect(normalizeOhmLocale('')).to.equal('en') + }) + + it('extracts base code from unknown regional variants', () => { + expect(normalizeOhmLocale('xx_YY')).to.equal('xx') + expect(normalizeOhmLocale('zz_XY')).to.equal('zz') + }) + + it('defaults to "en" when locale is completely unknown with no base code', () => { + // Testing edge case with non-standard input + expect(normalizeOhmLocale('_')).to.equal('en') + expect(normalizeOhmLocale('')).to.equal('en') + }) + }) + + describe('edge cases', () => { + it('handles locales with multiple underscores', () => { + // Should extract the part before the first underscore + expect(normalizeOhmLocale('sr__Latn')).to.equal('sr') + expect(normalizeOhmLocale('xx_Y_Z')).to.equal('xx') + }) + + it('handles locales not in the map with regional suffix', () => { + // If a locale isn't in the map but has a base code, use it + expect(normalizeOhmLocale('xy_AB')).to.equal('xy') + }) + }) +}) + +describe('OHM_LOCALE_MAP', () => { + it('contains entries for all frontendLanguages that need mapping', () => { + const frontendLanguages = [ + 'ar', + 'ba', + 'bg', + 'br', + 'ca', + 'cs', + 'da', + 'de_AT', + 'de', + 'el', + 'en_GB', + 'en', + 'eo', + 'es', + 'fi', + 'fr', + 'ga', + 'he', + 'hr', + 'hu', + 'is', + 'id', + 'it', + 'ja', + 'ko', + 'lt', + 'lv', + 'mk', + 'nb', + 'nl', + 'nn', + 'pl', + 'pt_BR', + 'pt_PT', + 'ro', + 'ru', + 'sk', + 'sl', + 'sq', + 'sr', + 'sv', + 'ta', + 'tr', + 'uk', + 'vi', + 'zh_CN', + 'zh_HK', + 'zh_TW', + ] + + // All languages from frontendLanguages should either: + // 1. Be in OHM_LOCALE_MAP with a mapping to base code + // 2. Or map to themselves (base code = same code) + frontendLanguages.forEach(lang => { + const mapped = OHM_LOCALE_MAP[lang] + expect(mapped).to.not.be.undefined + }) + }) + + it('maps all regional variants to their base language codes', () => { + // Verify specific important mappings + expect(OHM_LOCALE_MAP.de_AT).to.equal('de') + expect(OHM_LOCALE_MAP.en_GB).to.equal('en') + expect(OHM_LOCALE_MAP.pt_BR).to.equal('pt') + expect(OHM_LOCALE_MAP.pt_PT).to.equal('pt') + expect(OHM_LOCALE_MAP.zh_CN).to.equal('zh') + expect(OHM_LOCALE_MAP.zh_HK).to.equal('zh') + expect(OHM_LOCALE_MAP.zh_TW).to.equal('zh') + }) + + it('does not contain any entries that map to undefined', () => { + Object.entries(OHM_LOCALE_MAP).forEach(([locale, mappedLocale]) => { + expect(mappedLocale).to.be.a('string') + expect(mappedLocale.length).to.be.greaterThan(0) + }) + }) +}) From ee15747b65afe9a688830868aaf64d3ed1925eff Mon Sep 17 00:00:00 2001 From: mahula Date: Wed, 8 Apr 2026 11:24:58 +0200 Subject: [PATCH 3/6] remove debug logging from GrampsjsMap._getStyleUrl() --- src/components/GrampsjsMap.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/GrampsjsMap.js b/src/components/GrampsjsMap.js index 8dc3b27b..dee4002c 100644 --- a/src/components/GrampsjsMap.js +++ b/src/components/GrampsjsMap.js @@ -265,13 +265,7 @@ class GrampsjsMap extends GrampsjsAppStateMixin(LitElement) { const mapBaseStyle = theme === 'dark' ? config.mapBaseStyleDark : config.mapBaseStyleLight if (style === 'ohm') { - const locale = this.appState.i18n?.lang - const ohmLocale = normalizeOhmLocale(locale) - // Log the language request for debugging purposes - // eslint-disable-next-line no-console - console.debug( - `OpenHistoricalMap: requesting labels in locale "${ohmLocale}" (frontend locale: "${locale}")` - ) + const ohmLocale = normalizeOhmLocale(this.appState.i18n?.lang) return `${config.mapOhmStyle}?language=${ohmLocale}` } return mapBaseStyle From 550020742f1d46c7887737b25d8cce6c89b43b39 Mon Sep 17 00:00:00 2001 From: mahula Date: Wed, 8 Apr 2026 11:53:35 +0200 Subject: [PATCH 4/6] refactor(map): use URL API for OHM style URL construction Use URL.searchParams.set() instead of string interpolation to properly handle OHM style URLs that may already contain query parameters. Fixes potential issue noted in PR review. --- src/components/GrampsjsMap.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/GrampsjsMap.js b/src/components/GrampsjsMap.js index dee4002c..4a93747e 100644 --- a/src/components/GrampsjsMap.js +++ b/src/components/GrampsjsMap.js @@ -266,7 +266,9 @@ class GrampsjsMap extends GrampsjsAppStateMixin(LitElement) { theme === 'dark' ? config.mapBaseStyleDark : config.mapBaseStyleLight if (style === 'ohm') { const ohmLocale = normalizeOhmLocale(this.appState.i18n?.lang) - return `${config.mapOhmStyle}?language=${ohmLocale}` + const styleUrl = new URL(config.mapOhmStyle) + styleUrl.searchParams.set('language', ohmLocale) + return styleUrl.toString() } return mapBaseStyle } From 3e6025034f98aaedfb507af4aa719887c5491428 Mon Sep 17 00:00:00 2001 From: mahula Date: Wed, 8 Apr 2026 11:56:47 +0200 Subject: [PATCH 5/6] refactor(test): import frontendLanguages from strings.js instead of duplicating Removes hardcoded frontendLanguages list from test file to avoid maintenance duplication. Now imports from src/strings.js directly. --- test/unit/mapOhmLocale.test.js | 52 +--------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/test/unit/mapOhmLocale.test.js b/test/unit/mapOhmLocale.test.js index 22551b3b..a5aff2c7 100644 --- a/test/unit/mapOhmLocale.test.js +++ b/test/unit/mapOhmLocale.test.js @@ -1,5 +1,6 @@ import {describe, it, expect} from 'vitest' import {normalizeOhmLocale, OHM_LOCALE_MAP} from '../../src/util.js' +import {frontendLanguages} from '../../src/strings.js' describe('normalizeOhmLocale', () => { describe('basic mapping', () => { @@ -78,57 +79,6 @@ describe('normalizeOhmLocale', () => { describe('OHM_LOCALE_MAP', () => { it('contains entries for all frontendLanguages that need mapping', () => { - const frontendLanguages = [ - 'ar', - 'ba', - 'bg', - 'br', - 'ca', - 'cs', - 'da', - 'de_AT', - 'de', - 'el', - 'en_GB', - 'en', - 'eo', - 'es', - 'fi', - 'fr', - 'ga', - 'he', - 'hr', - 'hu', - 'is', - 'id', - 'it', - 'ja', - 'ko', - 'lt', - 'lv', - 'mk', - 'nb', - 'nl', - 'nn', - 'pl', - 'pt_BR', - 'pt_PT', - 'ro', - 'ru', - 'sk', - 'sl', - 'sq', - 'sr', - 'sv', - 'ta', - 'tr', - 'uk', - 'vi', - 'zh_CN', - 'zh_HK', - 'zh_TW', - ] - // All languages from frontendLanguages should either: // 1. Be in OHM_LOCALE_MAP with a mapping to base code // 2. Or map to themselves (base code = same code) From 34fca7687d96fcb8c276c634e9c39ec6dd200df2 Mon Sep 17 00:00:00 2001 From: mahula Date: Wed, 8 Apr 2026 12:00:15 +0200 Subject: [PATCH 6/6] docs(util): improve JSDoc for normalizeOhmLocale fallback chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarify the actual fallback chain in the JSDoc comment: 1. Falsy locale → 'en' 2. In OHM_LOCALE_MAP → use mapped value 3. Unknown locale with base code → extract base code 4. No base code available → 'en' Also add null to @param type since function handles it in practice. --- src/util.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/util.js b/src/util.js index d545eaef..3b5bc3c5 100644 --- a/src/util.js +++ b/src/util.js @@ -1035,8 +1035,14 @@ export const OHM_LOCALE_MAP = { /** * Converts a frontend locale code to an OpenHistoricalMap-compatible ISO 639-1 code. - * Falls back to 'en' if the locale is unknown or unsupported. - * @param {string|undefined} locale - Frontend locale code (e.g., 'de_AT', 'en_GB') + * + * Fallback chain: + * 1. If locale is falsy (null, undefined, empty string) → 'en' + * 2. If locale is in OHM_LOCALE_MAP → use the mapped value + * 3. If locale is not in map but has a base code (e.g., 'xx_YY') → extract base code ('xx') + * 4. If no base code is available → 'en' + * + * @param {string|null|undefined} locale - Frontend locale code (e.g., 'de_AT', 'en_GB') * @returns {string} OHM-compatible language code (e.g., 'de', 'en') */ export function normalizeOhmLocale(locale) {