diff --git a/src/components/GrampsjsMap.js b/src/components/GrampsjsMap.js index b6e8aae5..4a93747e 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' @@ -264,7 +264,13 @@ 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 ohmLocale = normalizeOhmLocale(this.appState.i18n?.lang) + const styleUrl = new URL(config.mapOhmStyle) + styleUrl.searchParams.set('language', ohmLocale) + return styleUrl.toString() + } + return mapBaseStyle } } diff --git a/src/util.js b/src/util.js index ce831f92..3b5bc3c5 100644 --- a/src/util.js +++ b/src/util.js @@ -976,4 +976,85 @@ 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. + * + * 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) { + 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..a5aff2c7 --- /dev/null +++ b/test/unit/mapOhmLocale.test.js @@ -0,0 +1,108 @@ +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', () => { + 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', () => { + // 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) + }) + }) +})