kopia lustrzana https://github.com/Tldraw/Tldraw
[fix] pick a better default language (#1201)
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 <l2wilson94@gmail.com>pull/1213/head
rodzic
00d4648ef5
commit
5ab93eef5f
|
@ -6,4 +6,4 @@
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"plugins": ["prettier-plugin-organize-imports"]
|
"plugins": ["prettier-plugin-organize-imports"]
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlstore'
|
import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlstore'
|
||||||
import { T } from '@tldraw/tlvalidate'
|
import { T } from '@tldraw/tlvalidate'
|
||||||
import { LANGUAGES } from '../languages'
|
import { getDefaultTranslationLocale } from '../translations'
|
||||||
import { userIdValidator } from '../validation'
|
import { userIdValidator } from '../validation'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,15 +50,12 @@ export const TLUser = createRecordType<TLUser>('user', {
|
||||||
validator: userTypeValidator,
|
validator: userTypeValidator,
|
||||||
scope: 'instance',
|
scope: 'instance',
|
||||||
}).withDefaultProperties((): Omit<TLUser, 'id' | 'typeName'> => {
|
}).withDefaultProperties((): Omit<TLUser, 'id' | 'typeName'> => {
|
||||||
let lang
|
let locale = 'en'
|
||||||
if (typeof window !== 'undefined' && window.navigator) {
|
if (typeof window !== 'undefined' && window.navigator) {
|
||||||
const availLocales = LANGUAGES.map(({ locale }) => locale) as string[]
|
locale = getDefaultTranslationLocale(window.navigator.languages)
|
||||||
lang = window.navigator.languages.find((lang) => {
|
|
||||||
return availLocales.indexOf(lang) > -1
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: 'New User',
|
name: 'New User',
|
||||||
locale: lang ?? 'en',
|
locale,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
|
@ -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
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ export interface TranslationProviderProps {
|
||||||
* @example
|
* @example
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* ;<TranslationProvider overrides={{ en: { 'style-panel.styles': 'Properties' } }} />
|
* <TranslationProvider overrides={{ en: { 'style-panel.styles': 'Properties' } }} />
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
overrides?: Record<string, Record<string, string>>
|
overrides?: Record<string, Record<string, string>>
|
||||||
|
@ -58,14 +58,13 @@ export const TranslationProvider = track(function TranslationProvider({
|
||||||
let isCancelled = false
|
let isCancelled = false
|
||||||
|
|
||||||
async function loadTranslation() {
|
async function loadTranslation() {
|
||||||
const localeString = locale ?? navigator.language.split(/[-_]/)[0]
|
const translation = await getTranslation(locale, getAssetUrl)
|
||||||
const translation = await getTranslation(localeString, getAssetUrl)
|
|
||||||
|
|
||||||
if (translation && !isCancelled) {
|
if (translation && !isCancelled) {
|
||||||
if (overrides && overrides[localeString]) {
|
if (overrides && overrides[locale]) {
|
||||||
setCurrentTranslation({
|
setCurrentTranslation({
|
||||||
...translation,
|
...translation,
|
||||||
messages: { ...translation.messages, ...overrides[localeString] },
|
messages: { ...translation.messages, ...overrides[locale] },
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setCurrentTranslation(translation)
|
setCurrentTranslation(translation)
|
||||||
|
|
Ładowanie…
Reference in New Issue