Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/components/GrampsjsMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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()
}
Comment thread
mahula marked this conversation as resolved.
return mapBaseStyle
Comment thread
mahula marked this conversation as resolved.
}
}

Expand Down
81 changes: 81 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Comment thread
mahula marked this conversation as resolved.
}

//
108 changes: 108 additions & 0 deletions test/unit/mapOhmLocale.test.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
Loading