Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/app/supported-languages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
},
"scripts": {
"build": "rimraf dist && tsc --project tsconfig.build.json",
"dev": "tsc --watch",
"clean": "rimraf dist .eslintcache",
"lint": "biome check . && tsc",
"lint:fix": "biome check --write",
"test": "vitest run",
Expand All @@ -36,9 +34,7 @@
"devDependencies": {
"@biomejs/biome": "^2.3.7",
"@lokalise/biome-config": "^3.1.0",
"@lokalise/node-core": "^14.0.0",
"@lokalise/tsconfig": "^3.1.0",
"@rollup/plugin-typescript": "^12.1.0",
"@vitest/coverage-v8": "^4.0.15",
"rimraf": "^6.0.1",
"typescript": "5.9.3",
Expand Down
11 changes: 11 additions & 0 deletions packages/app/supported-languages/src/constants/getters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { languages } from './languages.ts'
import { lokaliseSupportedLanguagesAndLocales } from './lokalise-languages.ts'
import { regions } from './regions.ts'
import { scripts } from './scripts.ts'
import { standardLocales } from './standard-locales.ts'

export const getAllLanguages = () => languages
export const getAllRegions = () => regions
export const getAllScripts = () => scripts
export const getStandardLocales = () => standardLocales
export const getLokaliseSupportedLanguagesAndLocales = () => lokaliseSupportedLanguagesAndLocales
5 changes: 5 additions & 0 deletions packages/app/supported-languages/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './getters.ts'
export type { Language } from './languages.ts'
export type { Region } from './regions.ts'
export type { Script } from './scripts.ts'
export type { StandardLocale } from './standard-locales.ts'
63 changes: 63 additions & 0 deletions packages/app/supported-languages/src/display-names.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest'
import { getLanguageNameInEnglish, getLocalisedLanguageName } from './display-names.ts'

describe('getLanguageNameInEnglish', () => {
it('returns name for a language code', () => {
expect(getLanguageNameInEnglish('es')).toBe('Spanish')
expect(getLanguageNameInEnglish('en')).toBe('English')
expect(getLanguageNameInEnglish('fr')).toBe('French')
})

it('returns name for a language-region locale', () => {
expect(getLanguageNameInEnglish('en-US')).toBe('English (United States)')
expect(getLanguageNameInEnglish('pt-BR')).toBe('Portuguese (Brazil)')
})

it('returns name for a language-script-region locale', () => {
expect(getLanguageNameInEnglish('bs-Latn-BA')).toBe('Bosnian (Latin, Bosnia & Herzegovina)')
expect(getLanguageNameInEnglish('zh-Hans-CN')).toBe('Chinese (Simplified, China)')
})

it('returns null for an empty string', () => {
expect(getLanguageNameInEnglish('')).toBeNull()
})

it('returns null for an unsupported tag', () => {
expect(getLanguageNameInEnglish('3141516')).toBeNull()
})
})

describe('getLocalisedLanguageName', () => {
it('returns name in the destination language', () => {
expect(getLocalisedLanguageName('es', 'fr')).toBe('espagnol')
expect(getLocalisedLanguageName('en', 'de')).toBe('Englisch')
expect(getLocalisedLanguageName('fr', 'es')).toBe('francés')
})

it('returns name for a language-region locale', () => {
expect(getLocalisedLanguageName('en-US', 'fr')).toBe('anglais (États-Unis)')
expect(getLocalisedLanguageName('pt-BR', 'es')).toBe('portugués (Brasil)')
})

it('returns name for a language-script-region locale', () => {
expect(getLocalisedLanguageName('bs-Latn-BA', 'fr')).toBe(
'bosniaque (latin, Bosnie-Herzégovine)',
)
})

it('respects the languageDisplay option', () => {
expect(getLocalisedLanguageName('en-US', 'fr', { languageDisplay: 'dialect' })).toBe(
'anglais américain',
)
})

it('returns null for an invalid source tag', () => {
expect(getLocalisedLanguageName('', 'en')).toBeNull()
expect(getLocalisedLanguageName('3141516', 'en')).toBeNull()
})

it('returns null for an invalid destination tag', () => {
expect(getLocalisedLanguageName('fr', 'wow')).toBeNull()
expect(getLocalisedLanguageName('fr', '')).toBeNull()
})
})
33 changes: 33 additions & 0 deletions packages/app/supported-languages/src/display-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Locale } from './locale.ts'
import { isSupportedLocale } from './locale.ts'

export const getLocalisedLanguageName = (
tag: Locale,
destinationTag: Locale,
options?: Omit<Partial<Intl.DisplayNamesOptions>, 'type'>,
): string | null => {
if (!isSupportedLocale(tag) || !isSupportedLocale(destinationTag)) {
return null
}

const displayNames = new Intl.DisplayNames([destinationTag], {
type: 'language',
languageDisplay: 'standard',
...options,
})

try {
const displayName = displayNames.of(tag)

if (displayName === 'root') {
return null
}

return displayName ?? null
} catch (_) {
return null
}
}

export const getLanguageNameInEnglish = (tag: Locale): string | null =>
getLocalisedLanguageName(tag, 'en')
8 changes: 0 additions & 8 deletions packages/app/supported-languages/src/either.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,3 @@ type Right<U> = {
* @see {@link https://antman-does-software.com/stop-catching-errors-in-typescript-use-the-either-type-to-make-your-code-predictable Further reading on motivation for Either type}
*/
export type Either<T, U> = NonNullable<Left<T> | Right<U>>

/***
* Variation of Either, which may or may not have Error set, but always has Result
*/
export type DefiniteEither<T, U> = {
error?: T
result: U
}
205 changes: 4 additions & 201 deletions packages/app/supported-languages/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,201 +1,4 @@
import type { Either } from '@lokalise/node-core'
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the changes in this PR are about splitting this big index file

import type { Language } from './languages.ts'
import { languages, languagesSet } from './languages.ts'
import { lokaliseSupportedLanguagesAndLocales } from './lokaliseLanguages.ts'
import type { Region } from './regions.ts'
import { regions, regionsSet } from './regions.ts'
import type { Script } from './scripts.ts'
import { scripts, scriptsSet } from './scripts.ts'
import type { StandardLocale } from './standard-locales.ts'
import { standardLocales, standardLocalesSet } from './standard-locales.ts'

/**
* String representation of a locale.
*/
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export type LocaleString = Language | `${Language}-${Region}` | `${Language}-${Script}-${Region}`

/**
* Object representation of a locale.
*/
export type LocaleObject = {
language: string
script?: string
region?: string
}

/**
* Get list of all our standard locale codes.
*/
export const getStandardLocales = () => {
return standardLocales
}

/**
* Get list of all available languages we support.
*/
export const getAllLanguages = () => {
return languages
}

/**
* Get list of all available scripts we support.
*/
export const getAllScripts = () => {
return scripts
}

/**
* Get list of all available regions we support.
*/
export const getAllRegions = () => {
return regions
}

export const getLokaliseSupportedLanguagesAndLocales = () => {
return lokaliseSupportedLanguagesAndLocales
}

/**
* Determine if `tag` is part of our standard locales.
*/
export const isStandardLocale = (tag: string): tag is StandardLocale => {
return standardLocalesSet.has(tag as StandardLocale)
}

/**
* Parse locale string into object.
*
* @throws {RangeError} If locale is structurally invalid or values are not in our supported values
*/
export const parseLocale = (tag: LocaleString): Either<string, LocaleObject> => {
if (!isSupportedLocale(tag)) {
return { error: `Locale tag ${tag} is not supported` }
}

const { language, script, region } = new Intl.Locale(tag)

return { result: { language, script, region } }
}

/**
* Turn LocaleObject into LocaleString.
*/
export const stringifyLocale = (obj: LocaleObject): LocaleString => {
return [obj.language, obj.script, obj.region].filter(Boolean).join('-')
}

/**
* Verify that `tag` is a valid locale code and all parts of it is in our lists of supported values.
*/
export const isSupportedLocale = (tag: string) => {
try {
const { language, script, region } = new Intl.Locale(tag)

if (region && !regionsSet.has(region)) {
return false
}

if (script && !scriptsSet.has(script)) {
return false
}

return languagesSet.has(language)
} catch (_) {
return false
}
}

/**
* Get common regions for a language, based on our standard locales.
*/
export const getCommonRegionsForLanguage = (() => {
const cache: Record<Language, Array<Region>> = {}

// Create a mapping of languages to common regions up front
for (const locale of standardLocales) {
const { language, region } = new Intl.Locale(locale)

if (!region) {
continue
}

const current = cache[language] ?? []
current.push(region)
cache[language] = current
}

return (language: string): Array<Region> => {
return cache[language] ?? []
}
})()

/**
* Get common languages for a region, based on our standard locales.
*/
export const getCommonLanguagesForRegion = (() => {
const cache: Record<Region, Array<Language>> = {}

// Create a mapping of languages to common regions up front
for (const locale of standardLocales) {
const { language, region } = new Intl.Locale(locale)

if (!region) {
continue
}

const current = cache[region] ?? []
current.push(language)
cache[region] = current
}

return (region: string): Array<Language> => {
return cache[region] ?? []
}
})()

export const normalizeLocale = (tag: string) => {
/**
* "und" is used in some systems to mean an "Unknown Language".
* Throughout our system however, we prefer to use "null" to mean unknown language.*
* */
if (tag === 'und') return null

try {
return stringifyLocale(new Intl.Locale(tag))
} catch (_) {
return null
}
}

export const getLanguageNameInEnglish = (tag: LocaleString): string | null => {
return getLocalisedLanguageName(tag, 'en')
}

export const getLocalisedLanguageName = (
tag: LocaleString,
destinationTag: LocaleString,
options?: Omit<Partial<Intl.DisplayNamesOptions>, 'type'>,
): string | null => {
if (!isSupportedLocale(tag) || !isSupportedLocale(destinationTag)) {
return null
}

const displayNames = new Intl.DisplayNames([destinationTag], {
type: 'language',
languageDisplay: 'standard',
...options,
})

try {
const displayName = displayNames.of(tag)

if (displayName === 'root') {
return null
}

return displayName ?? null
} catch (_) {
return null
}
}
export * from './constants/index.ts'
export * from './display-names.ts'
export * from './locale.ts'
export * from './locale-lookup.ts'
5 changes: 5 additions & 0 deletions packages/app/supported-languages/src/intl.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare namespace Intl {
interface Locale {
getTextInfo(): { direction: 'ltr' | 'rtl' }
}
}
34 changes: 34 additions & 0 deletions packages/app/supported-languages/src/locale-lookup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { getCommonLanguagesForRegion, getCommonRegionsForLanguage } from './locale-lookup.ts'

describe('getCommonRegionsForLanguage', () => {
it('returns regions for a language present in standard locales', () => {
expect(getCommonRegionsForLanguage('zh')).toMatchObject(['CN', 'HK', 'MO', 'SG', 'TW'])
expect(getCommonRegionsForLanguage('en')).toContain('US')
expect(getCommonRegionsForLanguage('en')).toContain('GB')
})

it('returns empty array for a language not present in standard locales', () => {
expect(getCommonRegionsForLanguage('ace')).toEqual([])
})

it('returns empty array for an unknown language', () => {
expect(getCommonRegionsForLanguage('abc')).toEqual([])
})
})

describe('getCommonLanguagesForRegion', () => {
it('returns languages for a region present in standard locales', () => {
expect(getCommonLanguagesForRegion('CA')).toMatchObject(['en', 'fr', 'iu'])
expect(getCommonLanguagesForRegion('CH')).toContain('de')
expect(getCommonLanguagesForRegion('CH')).toContain('fr')
})

it('returns empty array for a region not present in standard locales', () => {
expect(getCommonLanguagesForRegion('AC')).toEqual([])
})

it('returns empty array for an unknown region', () => {
expect(getCommonLanguagesForRegion('AB')).toEqual([])
})
})
Loading
Loading