Skip to content

Commit

Permalink
feat: Add new isRTL function to check whether a given, or the curre…
Browse files Browse the repository at this point in the history
…nt, language is right-to-left read

Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Jun 24, 2023
1 parent 408c537 commit 3da0042
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 71 deletions.
3 changes: 2 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
67 changes: 67 additions & 0 deletions lib/locale.ts
Original file line number Diff line number Diff line change
@@ -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)
}
25 changes: 2 additions & 23 deletions lib/translation.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
*
Expand Down
92 changes: 92 additions & 0 deletions tests/locale.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
47 changes: 0 additions & 47 deletions tests/translation.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type { NextcloudWindowWithRegistry } from '../lib/registry'
import {
getCanonicalLocale,
getLanguage,
getLocale,
getPlural,
register,
translate,
Expand All @@ -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 = {
Expand Down

0 comments on commit 3da0042

Please sign in to comment.