From 5ab93eef5f55a0982c34d2d06a58990ccf66fee4 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 30 Apr 2023 00:06:02 +0100 Subject: [PATCH] [fix] pick a better default language (#1201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the language selection. Previously, we would miss the user's languages that included a locale. For example, if a user's languages were `['en-US', 'fr'], then they would get 'fr' because 'en-US' wasn't in our table—though 'en' was! We were already doing the splitting elsewhere but now we do it here, too. ### Release Note - Improves default language --------- Co-authored-by: Lu[ke] Wilson --- .prettierrc | 2 +- packages/tlschema/src/records/TLUser.ts | 11 ++-- packages/tlschema/src/translations.test.ts | 43 ++++++++++++ packages/tlschema/src/translations.ts | 65 +++++++++++++++++++ .../hooks/useTranslation/useTranslation.tsx | 9 ++- 5 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 packages/tlschema/src/translations.test.ts create mode 100644 packages/tlschema/src/translations.ts diff --git a/.prettierrc b/.prettierrc index 7fd1df8c6..a808b9c2b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,4 +6,4 @@ "tabWidth": 2, "useTabs": true, "plugins": ["prettier-plugin-organize-imports"] -} +} \ No newline at end of file diff --git a/packages/tlschema/src/records/TLUser.ts b/packages/tlschema/src/records/TLUser.ts index 816303b47..b76df633c 100644 --- a/packages/tlschema/src/records/TLUser.ts +++ b/packages/tlschema/src/records/TLUser.ts @@ -1,6 +1,6 @@ import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlstore' import { T } from '@tldraw/tlvalidate' -import { LANGUAGES } from '../languages' +import { getDefaultTranslationLocale } from '../translations' import { userIdValidator } from '../validation' /** @@ -50,15 +50,12 @@ export const TLUser = createRecordType('user', { validator: userTypeValidator, scope: 'instance', }).withDefaultProperties((): Omit => { - let lang + let locale = 'en' if (typeof window !== 'undefined' && window.navigator) { - const availLocales = LANGUAGES.map(({ locale }) => locale) as string[] - lang = window.navigator.languages.find((lang) => { - return availLocales.indexOf(lang) > -1 - }) + locale = getDefaultTranslationLocale(window.navigator.languages) } return { name: 'New User', - locale: lang ?? 'en', + locale, } }) diff --git a/packages/tlschema/src/translations.test.ts b/packages/tlschema/src/translations.test.ts new file mode 100644 index 000000000..285f3f276 --- /dev/null +++ b/packages/tlschema/src/translations.test.ts @@ -0,0 +1,43 @@ +import { getDefaultTranslationLocale } from './translations' + +type DefaultLanguageTest = { + name: string + input: string[] + output: string +} + +describe('Choosing a sensible default translation locale', () => { + const tests: DefaultLanguageTest[] = [ + { + name: 'finds a matching language locale', + input: ['fr'], + output: 'fr', + }, + { + name: 'finds a matching region locale', + input: ['pt-PT'], + output: 'pt-pt', + }, + { + name: 'picks a region locale if no language locale available', + input: ['pt'], + output: 'pt-br', + }, + { + name: 'picks a language locale if no region locale available', + input: ['fr-CA'], + output: 'fr', + }, + { + name: 'picks the first language that loosely matches', + input: ['fr-CA', 'pt-PT'], + output: 'fr', + }, + ] + + for (const test of tests) { + it(test.name, () => { + expect(getDefaultTranslationLocale(test.input)).toEqual(test.output) + }) + } +}) diff --git a/packages/tlschema/src/translations.ts b/packages/tlschema/src/translations.ts new file mode 100644 index 000000000..e74ac729c --- /dev/null +++ b/packages/tlschema/src/translations.ts @@ -0,0 +1,65 @@ +import { LANGUAGES } from './languages' + +type TLListedTranslation = { + readonly locale: string + readonly label: string +} + +type TLListedTranslations = TLListedTranslation[] +type TLTranslationLocale = TLListedTranslations[number]['locale'] + +/** @public */ +export function getDefaultTranslationLocale(locales: readonly string[]): TLTranslationLocale { + for (const locale of locales) { + const supportedLocale = getSupportedLocale(locale) + if (supportedLocale) { + return supportedLocale + } + } + return 'en' +} + +/** @public */ +const DEFAULT_LOCALE_REGIONS: { [locale: string]: TLTranslationLocale } = { + zh: 'zh-cn', + pt: 'pt-br', + ko: 'ko-kr', + hi: 'hi-in', +} + +/** @public */ +function getSupportedLocale(locale: string): TLTranslationLocale | null { + // If we have an exact match, return it! + // (e.g. if the user has 'fr' and we have 'fr') + // (or if the user has 'pt-BR' and we have 'pt-br') + const exactMatch = LANGUAGES.find((t) => t.locale === locale.toLowerCase()) + if (exactMatch) { + return exactMatch.locale + } + + // Otherwise, we need to be more flexible... + const [language, region] = locale.split(/[-_]/).map((s) => s.toLowerCase()) + + // If the user's language has a region... + // let's try to find non-region-specific locale for them + // (e.g. if they have 'fr-CA' but we only have 'fr') + if (region) { + const languageMatch = LANGUAGES.find((t) => t.locale === language) + if (languageMatch) { + return languageMatch.locale + } + } + + // If the user's language doesn't have a region... + // let's try to find a region-specific locale for them + // (e.g. if they have 'pt' but we only have 'pt-pt' or 'pt-br') + // + // In this case, we choose the hard-coded default region for that language + if (language in DEFAULT_LOCALE_REGIONS) { + return DEFAULT_LOCALE_REGIONS[language] + } + + // Oh no! We don't have a translation for this language! + // Let's give up... + return null +} diff --git a/packages/ui/src/lib/hooks/useTranslation/useTranslation.tsx b/packages/ui/src/lib/hooks/useTranslation/useTranslation.tsx index 217cd49f6..fde3334f0 100644 --- a/packages/ui/src/lib/hooks/useTranslation/useTranslation.tsx +++ b/packages/ui/src/lib/hooks/useTranslation/useTranslation.tsx @@ -15,7 +15,7 @@ export interface TranslationProviderProps { * @example * * ```ts - * ; + * * ``` */ overrides?: Record> @@ -58,14 +58,13 @@ export const TranslationProvider = track(function TranslationProvider({ let isCancelled = false async function loadTranslation() { - const localeString = locale ?? navigator.language.split(/[-_]/)[0] - const translation = await getTranslation(localeString, getAssetUrl) + const translation = await getTranslation(locale, getAssetUrl) if (translation && !isCancelled) { - if (overrides && overrides[localeString]) { + if (overrides && overrides[locale]) { setCurrentTranslation({ ...translation, - messages: { ...translation.messages, ...overrides[localeString] }, + messages: { ...translation.messages, ...overrides[locale] }, }) } else { setCurrentTranslation(translation)