From 3da004253025155233194284a162bb0b98d3fe3f Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Sat, 24 Jun 2023 22:41:31 +0200 Subject: [PATCH] feat: Add new `isRTL` function to check whether a given, or the current, language is right-to-left read Signed-off-by: Ferdinand Thiessen --- lib/index.ts | 3 +- lib/locale.ts | 67 ++++++++++++++++++++++++++++ lib/translation.ts | 25 +---------- tests/locale.test.ts | 92 +++++++++++++++++++++++++++++++++++++++ tests/translation.test.ts | 47 -------------------- 5 files changed, 163 insertions(+), 71 deletions(-) create mode 100644 lib/locale.ts create mode 100644 tests/locale.test.ts diff --git a/lib/index.ts b/lib/index.ts index 8fbc490a..a0a43c5f 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -14,5 +14,6 @@ console.warn(n('my-app', 'Got an error', 'Got multiple errors', 2)); export type { Translations } from './registry' -export * from './translation' export * from './date' +export * from './locale' +export * from './translation' diff --git a/lib/locale.ts b/lib/locale.ts new file mode 100644 index 00000000..e20d310e --- /dev/null +++ b/lib/locale.ts @@ -0,0 +1,67 @@ +/** + * Returns the user's locale + */ +export function getLocale(): string { + return document.documentElement.dataset.locale || 'en' +} + +/** + * Returns user's locale in canonical form + * E.g. `en-US` instead of `en_US` + */ +export function getCanonicalLocale(): string { + return getLocale().replace(/_/g, '-') +} + +/** + * Returns the user's language + */ +export function getLanguage(): string { + return document.documentElement.lang || 'en' +} + +/** + * Check whether the current, or a given, language is read right-to-left + * + * @param language Language code to check, defaults to current language + */ +export function isRTL(language?: string): boolean { + const languageCode = language || getLanguage() + + // Source: https://meta.wikimedia.org/wiki/Template:List_of_language_names_ordered_by_code + const rtlLanguages = [ + /* eslint-disable no-multi-spaces */ + 'ae', // Avestan + 'ar', // 'العربية', Arabic + 'arc', // Aramaic + 'arz', // 'مصرى', Egyptian + 'bcc', // 'بلوچی مکرانی', Southern Balochi + 'bqi', // 'بختياري', Bakthiari + 'ckb', // 'Soranî / کوردی', Sorani + 'dv', // Dhivehi + 'fa', // 'فارسی', Persian + 'glk', // 'گیلکی', Gilaki + 'ha', // 'هَوُسَ', Hausa + 'he', // 'עברית', Hebrew + 'khw', // 'کھوار', Khowar + 'ks', // 'कॉशुर / کٲشُر', Kashmiri + 'ku', // 'Kurdî / كوردی', Kurdish + 'mzn', // 'مازِرونی', Mazanderani + 'nqo', // 'ߒߞߏ', N’Ko + 'pnb', // 'پنجابی', Western Punjabi + 'ps', // 'پښتو', Pashto, + 'sd', // 'سنڌي', Sindhi + 'ug', // 'Uyghurche / ئۇيغۇرچە', Uyghur + 'ur', // 'اردو', Urdu + 'uzs', // 'اوزبیکی', Uzbek Afghan + 'yi', // 'ייִדיש', Yiddish + /* eslint-enable no-multi-spaces */ + ] + + // special case for Uzbek Afghan + if ((language || getCanonicalLocale()).startsWith('uz-AF')) { + return true + } + + return rtlLanguages.includes(languageCode) +} diff --git a/lib/translation.ts b/lib/translation.ts index 101ef82c..a31b05f3 100644 --- a/lib/translation.ts +++ b/lib/translation.ts @@ -1,10 +1,11 @@ +import type { Translations } from './registry' +import { getLanguage, getLocale } from './locale' import { getAppTranslations, hasAppTranslations, registerAppTranslations, unregisterAppTranslations, } from './registry' -import type { Translations } from './registry' import { generateFilePath } from '@nextcloud/router' import DOMPurify from 'dompurify' @@ -18,28 +19,6 @@ interface TranslationOptions { sanitize?: boolean } -/** - * Returns the user's locale - */ -export function getLocale(): string { - return document.documentElement.dataset.locale || 'en' -} - -/** - * Returns user's locale in canonical form - * E.g. `en-US` instead of `en_US` - */ -export function getCanonicalLocale(): string { - return getLocale().replace(/_/g, '-') -} - -/** - * Returns the user's language - */ -export function getLanguage(): string { - return document.documentElement.lang || 'en' -} - /** * Translate a string * diff --git a/tests/locale.test.ts b/tests/locale.test.ts new file mode 100644 index 00000000..f21f8181 --- /dev/null +++ b/tests/locale.test.ts @@ -0,0 +1,92 @@ +import { + getCanonicalLocale, + getLanguage, + getLocale, + isRTL +} from '../lib/locale' + +const setLocale = (locale: string) => document.documentElement.setAttribute('data-locale', locale) +const setLanguage = (lang: string) => document.documentElement.setAttribute('lang', lang) + +describe('getCanonicalLocale', () => { + afterEach(() => { + setLocale('') + }) + + it('Returns primary locales as is', () => { + setLocale('de') + expect(getCanonicalLocale()).toEqual('de') + setLocale('zu') + expect(getCanonicalLocale()).toEqual('zu') + }) + + it('Returns extended locales with hyphens', () => { + setLocale('az_Cyrl_AZ') + expect(getCanonicalLocale()).toEqual('az-Cyrl-AZ') + setLocale('de_DE') + expect(getCanonicalLocale()).toEqual('de-DE') + }) +}) + +test('getLanguage', () => { + document.documentElement.removeAttribute('lang') + // Expect fallback + expect(getLanguage()).toBe('en') + setLanguage('') + expect(getLanguage()).toBe('en') + + // Expect value + setLanguage('zu') + expect(getLanguage()).toBe('zu') +}) + +test('getLocale', () => { + document.documentElement.removeAttribute('data-locale') + // Expect fallback + expect(getLocale()).toBe('en') + setLocale('') + expect(getLocale()).toBe('en') + + // Expect value + setLocale('de_DE') + expect(getLocale()).toBe('de_DE') +}) + +describe('isRTL', () => { + beforeEach(() => document.documentElement.removeAttribute('data-locale')) + + it('fallsback to English which is LTR', () => { + // Expect fallback which is English = LTR + expect(isRTL()).toBe(false) + }) + + it('uses the given argument over the current language', () => { + // If a value is given it should use that language over the fallback + expect(isRTL('ar')).toBe(true) + setLanguage('ar') + expect(isRTL('de')).toBe(false) + }) + + it('without an argument the current language is used', () => { + // It uses the configured language + setLanguage('he') + expect(isRTL()).toBe(true) + }) + + it('without an argument the current language is used', () => { + // It uses the configured language + setLanguage('he') + expect(isRTL()).toBe(true) + }) + + it('handles Uzbek Afghan correctly', () => { + // Given as argument + expect(isRTL('uz')).toBe(false) + expect(isRTL('uz-AF')).toBe(true) + + // configured as current language + setLanguage('uz') + setLocale('uz_AF') + expect(isRTL()).toBe(true) + }) +}) diff --git a/tests/translation.test.ts b/tests/translation.test.ts index 5c877f51..6eb5a426 100644 --- a/tests/translation.test.ts +++ b/tests/translation.test.ts @@ -1,8 +1,5 @@ import type { NextcloudWindowWithRegistry } from '../lib/registry' import { - getCanonicalLocale, - getLanguage, - getLocale, getPlural, register, translate, @@ -15,50 +12,6 @@ declare const window: NextcloudWindowWithRegistry const setLocale = (locale: string) => document.documentElement.setAttribute('data-locale', locale) const setLanguage = (lang: string) => document.documentElement.setAttribute('lang', lang) -describe('getCanonicalLocale', () => { - afterEach(() => { - setLocale('') - }) - - it('Returns primary locales as is', () => { - setLocale('de') - expect(getCanonicalLocale()).toEqual('de') - setLocale('zu') - expect(getCanonicalLocale()).toEqual('zu') - }) - - it('Returns extended locales with hyphens', () => { - setLocale('az_Cyrl_AZ') - expect(getCanonicalLocale()).toEqual('az-Cyrl-AZ') - setLocale('de_DE') - expect(getCanonicalLocale()).toEqual('de-DE') - }) -}) - -test('getLanguage', () => { - document.documentElement.removeAttribute('lang') - // Expect fallback - expect(getLanguage()).toBe('en') - setLanguage('') - expect(getLanguage()).toBe('en') - - // Expect value - setLanguage('zu') - expect(getLanguage()).toBe('zu') -}) - -test('getLocale', () => { - document.documentElement.removeAttribute('data-locale') - // Expect fallback - expect(getLocale()).toBe('en') - setLocale('') - expect(getLocale()).toBe('en') - - // Expect value - setLocale('de_DE') - expect(getLocale()).toBe('de_DE') -}) - describe('translate', () => { const mockWindowDE = () => { window._oc_l10n_registry_translations = {