mirror of
https://github.com/suitenumerique/docs.git
synced 2026-10-19 14:31:49 +02:00
🐛(frontend) select a single language in the language picker
The language picker marked an option as selected when it matched the current language, falling back to a match on the language alone. With the current language zh-tw, the zh-cn option matched too, so several regions of a language were selected at once, and the legacy picker could show the label of the wrong one. Now that zh-cn and zh-tw are separate frontend translations, this shows up for the Chinese locales, and it would for any language with several regions (en-us/en-gb, pt-pt/pt-br). Add getClosestLocale, which returns the one available locale closest to the current language (the exact match first, then the first region of the language), and use it in both pickers to select the option and to display the current label.
This commit is contained in:
@@ -10,10 +10,7 @@ import { css } from 'styled-components';
|
||||
import { Box, DropdownMenu, Icon } from '@/components/';
|
||||
import { useConfig } from '@/core';
|
||||
import { useAuthQuery } from '@/features/auth';
|
||||
import {
|
||||
getMatchingLocales,
|
||||
useSynchronizedLanguage,
|
||||
} from '@/features/language';
|
||||
import { getClosestLocale, useSynchronizedLanguage } from '@/features/language';
|
||||
import { useResponsiveStore } from '@/stores/useResponsiveStore';
|
||||
|
||||
/**
|
||||
@@ -35,12 +32,16 @@ export const LanguagePickerLegacy = () => {
|
||||
// Compute options for dropdown
|
||||
const optionsPicker = useMemo(() => {
|
||||
const backendOptions = conf?.LANGUAGES ?? [[language, language]];
|
||||
const currentLocale = getClosestLocale(
|
||||
backendOptions.map(([locale]) => locale),
|
||||
language,
|
||||
);
|
||||
return backendOptions.map(([backendLocale, backendLabel]) => {
|
||||
return {
|
||||
label: backendLabel,
|
||||
lang: toLangTag(backendLocale),
|
||||
value: backendLocale,
|
||||
isSelected: getMatchingLocales([backendLocale], [language]).length > 0,
|
||||
isSelected: backendLocale === currentLocale,
|
||||
callback: async () => {
|
||||
await changeLanguageSynchronized(backendLocale, user);
|
||||
announce(
|
||||
@@ -56,8 +57,12 @@ export const LanguagePickerLegacy = () => {
|
||||
}, [changeLanguageSynchronized, conf?.LANGUAGES, language, t, user]);
|
||||
|
||||
// Extract current language label for display
|
||||
const currentLocale = getClosestLocale(
|
||||
conf?.LANGUAGES.map(([code]) => code) ?? [],
|
||||
language,
|
||||
);
|
||||
const [currentLanguageCode, currentLanguageLabel] = conf?.LANGUAGES.find(
|
||||
([code]) => getMatchingLocales([code], [language]).length > 0,
|
||||
([code]) => code === currentLocale,
|
||||
) ?? [language, language];
|
||||
|
||||
return (
|
||||
@@ -109,10 +114,14 @@ export const LanguagePicker = () => {
|
||||
|
||||
const languages: LanguagesOption[] = useMemo(() => {
|
||||
const backendOptions = conf?.LANGUAGES ?? [[language, language]];
|
||||
const currentLocale = getClosestLocale(
|
||||
backendOptions.map(([locale]) => locale),
|
||||
language,
|
||||
);
|
||||
return backendOptions.map(([backendLocale, backendLabel]) => ({
|
||||
label: backendLabel,
|
||||
value: backendLocale,
|
||||
isChecked: getMatchingLocales([backendLocale], [language]).length > 0,
|
||||
isChecked: backendLocale === currentLocale,
|
||||
}));
|
||||
}, [conf?.LANGUAGES, language]);
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getClosestLocale, getMatchingLocales } from '../locale';
|
||||
|
||||
const LOCALES = ['en-us', 'fr-fr', 'zh-cn', 'zh-tw'];
|
||||
|
||||
describe('getMatchingLocales', () => {
|
||||
it('returns every region of a language on a soft match', () => {
|
||||
expect(getMatchingLocales(LOCALES, ['zh'])).toEqual(['zh-cn', 'zh-tw']);
|
||||
});
|
||||
|
||||
it('only returns the exact match when there is one', () => {
|
||||
expect(getMatchingLocales(LOCALES, ['zh-TW'])).toEqual(['zh-tw']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getClosestLocale', () => {
|
||||
it.each([
|
||||
['zh-tw', 'zh-tw'],
|
||||
['zh-cn', 'zh-cn'],
|
||||
['zh-TW', 'zh-tw'],
|
||||
['fr', 'fr-fr'],
|
||||
['fr-CA', 'fr-fr'],
|
||||
['en', 'en-us'],
|
||||
])('resolves %s to %s', (language, expected) => {
|
||||
expect(getClosestLocale(LOCALES, language)).toBe(expected);
|
||||
});
|
||||
|
||||
it('prefers the exact match over the first region of the language', () => {
|
||||
expect(getClosestLocale(['en-us', 'en-gb'], 'en-gb')).toBe('en-gb');
|
||||
});
|
||||
|
||||
it('returns a single locale, never every region', () => {
|
||||
const selected = LOCALES.filter(
|
||||
(locale) => locale === getClosestLocale(LOCALES, 'zh-tw'),
|
||||
);
|
||||
expect(selected).toEqual(['zh-tw']);
|
||||
});
|
||||
|
||||
it('returns undefined when no locale shares the language', () => {
|
||||
expect(getClosestLocale(LOCALES, 'ja')).toBeUndefined();
|
||||
expect(getClosestLocale([], 'fr')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -47,3 +47,19 @@ export function getMatchingLocales(
|
||||
}
|
||||
return matchingLocales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the single available locale closest to the given one. Unlike
|
||||
* getMatchingLocales, which returns every region of a language on a soft
|
||||
* match, this is what to use to mark one entry as selected.
|
||||
*
|
||||
* @param {readonly string[]} localesAvailable - The list of available locale strings.
|
||||
* @param {string} locale - The locale to search for.
|
||||
* @returns {string | undefined} The exact match, else the first locale sharing the language.
|
||||
*/
|
||||
export function getClosestLocale(
|
||||
localesAvailable: readonly string[],
|
||||
locale: string,
|
||||
): string | undefined {
|
||||
return getMatchingLocales(localesAvailable, [locale])[0];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user