Set default locale according to browser locale (#11805)
We didn't get much complaints on Localization so I guess we can expand it more and make it the default behavior to use the browser's locale when you signup --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@ -59,6 +59,12 @@ export const LocalePicker = () => {
|
||||
await updateWorkspaceMember({ locale: value });
|
||||
|
||||
await dynamicActivate(value);
|
||||
try {
|
||||
localStorage.setItem('locale', value);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Failed to save locale to localStorage:', error);
|
||||
}
|
||||
await refreshObjectMetadataItems();
|
||||
};
|
||||
|
||||
|
||||
@ -10,10 +10,4 @@ export const dynamicActivate = async (locale: keyof typeof APP_LOCALES) => {
|
||||
const { messages } = await import(`../../locales/generated/${locale}.ts`);
|
||||
i18n.load(locale, messages);
|
||||
i18n.activate(locale);
|
||||
try {
|
||||
localStorage.setItem('locale', locale);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Failed to save locale to localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { fromNavigator, fromStorage, fromUrl } from '@lingui/detect-locale';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { isDefined, isValidLocale, normalizeLocale } from 'twenty-shared/utils';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined, isValidLocale } from 'twenty-shared/utils';
|
||||
|
||||
export const initialI18nActivate = () => {
|
||||
const urlLocale = fromUrl('locale');
|
||||
@ -10,20 +10,34 @@ export const initialI18nActivate = () => {
|
||||
|
||||
let locale: keyof typeof APP_LOCALES = APP_LOCALES.en;
|
||||
|
||||
if (isDefined(urlLocale) && isValidLocale(urlLocale)) {
|
||||
locale = urlLocale;
|
||||
const normalizedUrlLocale = isDefined(urlLocale)
|
||||
? normalizeLocale(urlLocale)
|
||||
: null;
|
||||
const normalizedStorageLocale = isDefined(storageLocale)
|
||||
? normalizeLocale(storageLocale)
|
||||
: null;
|
||||
const normalizedNavigatorLocale = isDefined(navigatorLocale)
|
||||
? normalizeLocale(navigatorLocale)
|
||||
: null;
|
||||
|
||||
if (isDefined(normalizedUrlLocale) && isValidLocale(normalizedUrlLocale)) {
|
||||
locale = normalizedUrlLocale;
|
||||
try {
|
||||
localStorage.setItem('locale', urlLocale);
|
||||
localStorage.setItem('locale', normalizedUrlLocale);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Failed to save locale to localStorage:', error);
|
||||
}
|
||||
} else if (isDefined(storageLocale) && isValidLocale(storageLocale)) {
|
||||
locale = storageLocale;
|
||||
} else if (isDefined(navigatorLocale) && isValidLocale(navigatorLocale)) {
|
||||
// TODO: remove when we're ready to launch
|
||||
// locale = navigatorLocale;
|
||||
locale = SOURCE_LOCALE;
|
||||
} else if (
|
||||
isDefined(normalizedStorageLocale) &&
|
||||
isValidLocale(normalizedStorageLocale)
|
||||
) {
|
||||
locale = normalizedStorageLocale;
|
||||
} else if (
|
||||
isDefined(normalizedNavigatorLocale) &&
|
||||
isValidLocale(normalizedNavigatorLocale)
|
||||
) {
|
||||
locale = normalizedNavigatorLocale;
|
||||
}
|
||||
|
||||
dynamicActivate(locale);
|
||||
|
||||
@ -23,3 +23,4 @@ export { isValidUrl } from './url/isValidUrl';
|
||||
export { isDefined } from './validation/isDefined';
|
||||
export { isValidLocale } from './validation/isValidLocale';
|
||||
export { isValidUuid } from './validation/isValidUuid';
|
||||
export { normalizeLocale } from './validation/normalizeLocale';
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { SOURCE_LOCALE } from '@/translations';
|
||||
import { normalizeLocale } from '../normalizeLocale';
|
||||
|
||||
describe('normalizeLocale', () => {
|
||||
it('should return SOURCE_LOCALE when the input is null', () => {
|
||||
expect(normalizeLocale(null)).toBe(SOURCE_LOCALE);
|
||||
});
|
||||
|
||||
it('should return the locale when there is a direct match in APP_LOCALES', () => {
|
||||
// Test a few valid locales
|
||||
expect(normalizeLocale('en')).toBe('en');
|
||||
expect(normalizeLocale('fr-FR')).toBe('fr-FR');
|
||||
expect(normalizeLocale('es-ES')).toBe('es-ES');
|
||||
});
|
||||
|
||||
it('should handle case-insensitive matches', () => {
|
||||
// Test with lowercase variants of the locales
|
||||
expect(normalizeLocale('fr-fr')).toBe('fr-FR');
|
||||
expect(normalizeLocale('es-es')).toBe('es-ES');
|
||||
expect(normalizeLocale('DE-de')).toBe('de-DE');
|
||||
});
|
||||
|
||||
it('should match just the language part if full locale not found', () => {
|
||||
// Test with just the language code
|
||||
expect(normalizeLocale('fr')).toBe('fr-FR');
|
||||
expect(normalizeLocale('es')).toBe('es-ES');
|
||||
expect(normalizeLocale('de')).toBe('de-DE');
|
||||
});
|
||||
|
||||
it('should handle language codes that might map to multiple locales', () => {
|
||||
// Test for language codes that might have multiple possible mappings
|
||||
// For example, 'pt' could map to either 'pt-PT' or 'pt-BR'
|
||||
// The implementation should map consistently to one of them
|
||||
expect(normalizeLocale('pt')).toBeTruthy();
|
||||
// Verify it's one of the expected values
|
||||
expect(['pt-PT', 'pt-BR']).toContain(normalizeLocale('pt'));
|
||||
});
|
||||
|
||||
it('should return SOURCE_LOCALE for unsupported or invalid locales', () => {
|
||||
expect(normalizeLocale('invalid-locale')).toBe(SOURCE_LOCALE);
|
||||
expect(normalizeLocale('xx-XX')).toBe(SOURCE_LOCALE);
|
||||
expect(normalizeLocale('')).toBe(SOURCE_LOCALE);
|
||||
});
|
||||
|
||||
it('should handle SOURCE_LOCALE and its variants correctly', () => {
|
||||
expect(normalizeLocale(SOURCE_LOCALE)).toBe(SOURCE_LOCALE);
|
||||
// If SOURCE_LOCALE is 'en', test 'en-US', 'en-GB', etc.
|
||||
if (SOURCE_LOCALE === 'en') {
|
||||
expect(normalizeLocale('en-US')).toBe(SOURCE_LOCALE);
|
||||
expect(normalizeLocale('en-GB')).toBe(SOURCE_LOCALE);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './isValidUuid';
|
||||
export * from './isDefined';
|
||||
export * from './isValidLocale';
|
||||
export * from './isValidUuid';
|
||||
export * from './normalizeLocale';
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from '@/translations';
|
||||
|
||||
/**
|
||||
* Maps language codes to full locale keys in APP_LOCALES
|
||||
* Example: 'fr' -> 'fr-FR', 'en' -> 'en'
|
||||
*/
|
||||
const languageToLocaleMap = Object.keys(APP_LOCALES).reduce<
|
||||
Record<string, string>
|
||||
>((map, locale) => {
|
||||
// Extract the language code (part before the hyphen or the whole code if no hyphen)
|
||||
const language = locale.split('-')[0].toLowerCase();
|
||||
|
||||
// Only add to the map if not already added or if the current locale is the source locale
|
||||
// This ensures language codes map to their full locale version (e.g., 'es' -> 'es-ES')
|
||||
// but preserves 'en' -> 'en' since it's the source locale
|
||||
if (!map[language] || locale === SOURCE_LOCALE) {
|
||||
map[language] = locale;
|
||||
}
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Normalizes a locale string to match our supported formats
|
||||
*/
|
||||
export const normalizeLocale = (
|
||||
value: string | null,
|
||||
): keyof typeof APP_LOCALES => {
|
||||
if (value === null) {
|
||||
return SOURCE_LOCALE;
|
||||
}
|
||||
|
||||
// Direct match in our supported locales
|
||||
if (value in APP_LOCALES) {
|
||||
return value as keyof typeof APP_LOCALES;
|
||||
}
|
||||
|
||||
// Try case-insensitive match (e.g., 'fr-fr' -> 'fr-FR')
|
||||
const caseInsensitiveMatch = Object.keys(APP_LOCALES).find(
|
||||
(locale) => locale.toLowerCase() === value.toLowerCase(),
|
||||
);
|
||||
if (caseInsensitiveMatch) {
|
||||
return caseInsensitiveMatch as keyof typeof APP_LOCALES;
|
||||
}
|
||||
|
||||
// Try matching just the language part (e.g., 'fr' -> 'fr-FR')
|
||||
const languageCode = value?.trim() ? value.split('-')[0].toLowerCase() : '';
|
||||
if (languageToLocaleMap[languageCode]) {
|
||||
return languageToLocaleMap[languageCode] as keyof typeof APP_LOCALES;
|
||||
}
|
||||
|
||||
return SOURCE_LOCALE;
|
||||
};
|
||||
Reference in New Issue
Block a user