-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Lang direction #895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: Lang direction #895
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
77029ee
Removing unused type
CarlosGamero d1b0d64
Improving constants + export
CarlosGamero c5a1ee4
index fix
CarlosGamero c5a75cc
Split index
CarlosGamero 7fe3b83
Adding getters
CarlosGamero 4c8c814
Adding getLocaleDirection
CarlosGamero 7ed9cf6
Lint
CarlosGamero 44ebd60
dep cleanup
CarlosGamero 7505583
Improve types
CarlosGamero a44f1d1
Coverage fix
CarlosGamero 56f1e0a
Type fixes
CarlosGamero 5e2040e
Adjusting types to avoid breaking changes
CarlosGamero 6b841f6
Merge branch 'main' into feat/lang_direction
CarlosGamero fb4c4da
Minor rename
CarlosGamero 6a36699
Fix test
CarlosGamero 52371e0
Merge branch 'feat/lang_direction' of https://github.com/lokalise/sha…
CarlosGamero 243e12e
Type improvement
CarlosGamero 7bd6c4d
typo
CarlosGamero 0fad5df
Adding rtl lang constant
CarlosGamero 0537e3e
Using rtl set for getLocaleDirection
CarlosGamero bd687ed
Lint
CarlosGamero 3b83669
Removing unused type
CarlosGamero File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
63 changes: 63 additions & 0 deletions
63
packages/app/supported-languages/src/display-names.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,201 +1,4 @@ | ||
| import type { Either } from '@lokalise/node-core' | ||
| 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' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
34
packages/app/supported-languages/src/locale-lookup.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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([]) | ||
| }) | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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