Track language selection with Plausible Language Changed event.

Centralize UI language switches in cycleAppLanguage and document the event in plausible-events.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 16:38:10 +02:00
parent ffe6b19818
commit bd1edd89f3
10 changed files with 99 additions and 15 deletions
+64 -2
View File
@@ -1,7 +1,40 @@
import { describe, expect, it } from 'vitest'
import { getNextLanguage, normalizeAppLanguage, SUPPORTED_LANGUAGES } from './i18nLanguages.js'
import type { i18n as I18nInstance } from 'i18next'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PlausibleEvents } from '../services/analytics.js'
import {
changeAppLanguage,
cycleAppLanguage,
getNextLanguage,
normalizeAppLanguage,
SUPPORTED_LANGUAGES
} from './i18nLanguages.js'
const trackPlausibleEvent = vi.fn()
vi.mock('../services/analytics.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../services/analytics.js')>()
return {
...actual,
trackPlausibleEvent: (...args: unknown[]) => trackPlausibleEvent(...args)
}
})
function createMockI18n(language: string): I18nInstance {
let current = language
return {
language: current,
changeLanguage: vi.fn(async (lng: string) => {
current = lng
;(this as { language: string }).language = lng
})
} as unknown as I18nInstance
}
describe('i18nLanguages', () => {
beforeEach(() => {
trackPlausibleEvent.mockReset()
})
it('normalizes regional tags to supported base codes', () => {
expect(normalizeAppLanguage('de-DE')).toBe('de')
expect(normalizeAppLanguage('nb-NO')).toBe('nb')
@@ -18,4 +51,33 @@ describe('i18nLanguages', () => {
expect(seen.size).toBe(SUPPORTED_LANGUAGES.length)
expect(current).toBe('de')
})
it('tracks explicit language changes', () => {
const i18n = createMockI18n('de')
changeAppLanguage(i18n, 'sv')
expect(i18n.changeLanguage).toHaveBeenCalledWith('sv')
expect(trackPlausibleEvent).toHaveBeenCalledWith(PlausibleEvents.LANGUAGE_CHANGED, {
from: 'de',
to: 'sv'
})
})
it('does not track when language stays the same', () => {
const i18n = createMockI18n('en')
changeAppLanguage(i18n, 'en')
expect(i18n.changeLanguage).not.toHaveBeenCalled()
expect(trackPlausibleEvent).not.toHaveBeenCalled()
})
it('cycleAppLanguage tracks the next language', () => {
const i18n = createMockI18n('nb')
cycleAppLanguage(i18n)
expect(trackPlausibleEvent).toHaveBeenCalledWith(PlausibleEvents.LANGUAGE_CHANGED, {
from: 'nb',
to: 'de'
})
})
})
+17
View File
@@ -1,3 +1,6 @@
import type { i18n as I18nInstance } from 'i18next'
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
/** Supported UI languages (ISO 639-1, language-only). */
export const SUPPORTED_LANGUAGES = ['de', 'en', 'da', 'sv', 'nb'] as const
@@ -20,3 +23,17 @@ export function getNextLanguage(current?: string): AppLanguage {
export function isGermanLocale(language?: string): boolean {
return normalizeAppLanguage(language) === 'de'
}
/** Switch UI language and track explicit user choice (not auto-detection). */
export function changeAppLanguage(i18n: I18nInstance, language: AppLanguage): void {
const from = normalizeAppLanguage(i18n.language)
const to = normalizeAppLanguage(language)
if (from === to) return
void i18n.changeLanguage(to)
trackPlausibleEvent(PlausibleEvents.LANGUAGE_CHANGED, { from, to })
}
export function cycleAppLanguage(i18n: I18nInstance): void {
changeAppLanguage(i18n, getNextLanguage(i18n.language))
}