diff --git a/lib/translation.ts b/lib/translation.ts
index 07b3ccd1..33ca8529 100644
--- a/lib/translation.ts
+++ b/lib/translation.ts
@@ -86,11 +86,12 @@ export function translate(
}
const bundle = getAppTranslations(app)
- const translation = bundle.translations[text] || text
+ let translation = bundle.translations[text] || text
+ translation = Array.isArray(translation) ? translation[0] : translation
if (typeof vars === 'object' || number !== undefined) {
return optSanitize(_build(
- typeof translation === 'string' ? translation : translation[0],
+ translation,
vars,
number
))
diff --git a/tests/index.test.js b/tests/index.test.js
deleted file mode 100644
index 110e5749..00000000
--- a/tests/index.test.js
+++ /dev/null
@@ -1,142 +0,0 @@
-import {
- getCanonicalLocale,
- translate,
- translatePlural,
- register,
- _unregister,
-} from '../lib/index'
-
-const setLocale = (locale) => document.documentElement.setAttribute('data-locale', locale)
-
-describe('translate', () => {
- const mockWindowDE = () => {
- window._oc_l10n_registry_translations = {
- core: {
- 'Hello world!': 'Hallo Welt!',
- 'Hello {name}': 'Hallo {name}',
- '_download %n file_::_download %n files_': [
- 'Lade %n Datei herunter',
- 'Lade %n Dateien herunter',
- ],
- },
- }
- window._oc_l10n_registry_plural_functions = {
- core: (t) => t === 1 ? 0 : 1,
- }
- setLocale('de')
- }
-
- beforeAll(mockWindowDE)
-
- it('singular', () => {
- const text = 'Hello world!'
- const translation = translate('core', text)
- expect(translation).toBe('Hallo Welt!')
- })
-
- it('with variable', () => {
- const text = 'Hello {name}'
- const translation = translate('core', text, { name: 'J. Doe' })
- expect(translation).toBe('Hallo J. Doe')
- })
-
- it('plural', () => {
- const text = ['download %n file', 'download %n files']
-
- expect(translatePlural('core', ...text, 1)).toBe('Lade 1 Datei herunter')
-
- expect(translatePlural('core', ...text, 2)).toBe('Lade 2 Dateien herunter')
- })
-
- it('missing text', () => {
- const text = 'Good bye!'
- const translation = translate('core', text)
- expect(translation).toBe('Good bye!')
- })
-
- it('missing application', () => {
- const text = 'Good bye!'
- const translation = translate('unavailable', text)
- expect(translation).toBe('Good bye!')
- })
-})
-
-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')
- })
-})
-
-describe('register', () => {
- beforeEach(() => {
- setLocale('de_DE')
- window._oc_l10n_registry_translations = undefined
- window._oc_l10n_registry_plural_functions = undefined
- })
-
- it('initial', () => {
- register('app', {
- Application: 'Anwendung',
- '_%n guest_::_%n guests_': ['%n Gast', '%n Gäste'],
- })
- expect(translate('app', 'Application')).toBe('Anwendung')
- expect(translatePlural('app', '%n guest', '%n guests', 1)).toBe('1 Gast')
- expect(translatePlural('app', '%n guest', '%n guests', 2)).toBe('2 Gäste')
- })
-
- it('extend', () => {
- window._oc_l10n_registry_translations = {
- app: {
- Application: 'Anwendung',
- },
- }
- window._oc_l10n_registry_plural_functions = {
- app: (t) => t === 1 ? 0 : 1,
- }
- register('app', {
- Translation: 'Übersetzung',
- })
- expect(translate('app', 'Application')).toBe('Anwendung')
- expect(translate('app', 'Translation')).toBe('Übersetzung')
- })
-
- it('with another app', () => {
- window._oc_l10n_registry_translations = {
- core: {
- 'Hello world!': 'Hallo Welt!',
- },
- }
- window._oc_l10n_registry_plural_functions = {
- core: (t) => t === 1 ? 0 : 1,
- }
- register('app', {
- Application: 'Anwendung',
- })
- expect(translate('core', 'Hello world!')).toBe('Hallo Welt!')
- expect(translate('app', 'Application')).toBe('Anwendung')
- })
-
- it('unregister', () => {
- window._oc_l10n_registry_translations = {}
- window._oc_l10n_registry_plural_functions = {}
- register('app', {
- Application: 'Anwendung',
- })
- _unregister('app')
- expect(translate('app', 'Application')).toBe('Application')
- })
-})
diff --git a/tests/translation.test.ts b/tests/translation.test.ts
new file mode 100644
index 00000000..cfd29b88
--- /dev/null
+++ b/tests/translation.test.ts
@@ -0,0 +1,244 @@
+import type { NextcloudWindowWithRegistry } from '../lib/registry'
+import {
+ getCanonicalLocale,
+ getLanguage,
+ getLocale,
+ getPlural,
+ loadTranslations,
+ register,
+ translate,
+ translatePlural,
+ _unregister,
+} from '../lib/translation'
+
+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 = {
+ core: {
+ 'Hello world!': 'Hallo Welt!',
+ 'Hello {name}': 'Hallo {name}',
+ '_download %n file_::_download %n files_': [
+ 'Lade %n Datei herunter',
+ 'Lade %n Dateien herunter',
+ ],
+ '_download %n file from {url}_::_download %n files from {url}_': [
+ 'Lade %n Datei von {url} herunter',
+ 'Lade %n Dateien von {url} herunter',
+ ],
+ },
+ }
+ window._oc_l10n_registry_plural_functions = {
+ core: (t) => t === 1 ? 0 : 1,
+ }
+ setLocale('de')
+ }
+
+ beforeAll(mockWindowDE)
+
+ it('without placeholder HTML escaping', () => {
+ const text = 'Hello {name}'
+ const translation = translate('core', text, { name: 'Name' }, undefined, { escape: false })
+ expect(translation).toBe('Hallo Name')
+ })
+
+ it('with placeholder HTML escaping', () => {
+ const text = 'Hello {name}'
+ const translation = translate('core', text, { name: 'Name' })
+ expect(translation).toBe('Hallo <del>Name</del>')
+ })
+
+ it('without placeholder XSS sanitizing', () => {
+ const text = 'Hello {name}'
+ const translation = translate('core', text, { name: '' }, undefined, { sanitize: false, escape: false })
+ expect(translation).toBe('Hallo ')
+ })
+
+ it('with placeholder XSS sanitizing', () => {
+ const text = 'Hello {name}'
+ const translation = translate('core', text, { name: '' }, undefined, { escape: false })
+ expect(translation).toBe('Hallo ')
+ })
+
+ it('singular', () => {
+ const text = 'Hello world!'
+ const translation = translate('core', text)
+ expect(translation).toBe('Hallo Welt!')
+ })
+
+ it('singular with variable', () => {
+ const text = 'Hello {name}'
+ const translation = translate('core', text, { name: 'J. Doe' })
+ expect(translation).toBe('Hallo J. Doe')
+ })
+
+ it('singular with missing variable', () => {
+ const text = 'Hello {name}'
+ const translation = translate('core', text, {})
+ expect(translation).toBe('Hallo {name}')
+ })
+
+ it('singular with invalid variable', () => {
+ const text = 'Hello {name}'
+ // We need to disable ts here as users might use it from JS were no type checking is available
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ const translation = translate('core', text, { name: true }) // invalid as only `string` or `number` are allowed
+ expect(translation).toBe('Hallo {name}')
+ })
+
+ it('singular with missing translation', () => {
+ const text = 'Good bye!'
+ const translation = translate('core', text)
+ expect(translation).toBe('Good bye!')
+ })
+
+ it('singular with missing application', () => {
+ const text = 'Good bye!'
+ const translation = translate('unavailable', text)
+ expect(translation).toBe('Good bye!')
+ })
+
+ it('singular with plural translation', () => {
+ const text = '_download %n file_::_download %n files_'
+ const translation = translate('core', text)
+ expect(translation).toBe('Lade %n Datei herunter')
+ })
+
+ it('plural', () => {
+ const text = ['download %n file', 'download %n files'] as const
+
+ expect(translatePlural('core', ...text, 1)).toBe('Lade 1 Datei herunter')
+
+ expect(translatePlural('core', ...text, 2)).toBe('Lade 2 Dateien herunter')
+ })
+
+ it('plural with missing translation', () => {
+ const text = ['%n translation does not exist', '%n translations do not exist'] as const
+
+ expect(translatePlural('core', ...text, 1)).toBe('1 translation does not exist')
+ expect(translatePlural('core', ...text, 2)).toBe('2 translations do not exist')
+ })
+
+ it('plural with variable', () => {
+ const text = ['download %n file from {url}', 'download %n files from {url}'] as const
+
+ expect(translatePlural('core', ...text, 1, { url: 'nextcloud.com' })).toBe('Lade 1 Datei von nextcloud.com herunter')
+ expect(translatePlural('core', ...text, 2, { url: 'nextcloud.com' })).toBe('Lade 2 Dateien von nextcloud.com herunter')
+ })
+
+ it('plural with missing variable', () => {
+ const text = ['download %n file from {url}', 'download %n files from {url}'] as const
+
+ expect(translatePlural('core', ...text, 1)).toBe('Lade 1 Datei von {url} herunter')
+ expect(translatePlural('core', ...text, 2)).toBe('Lade 2 Dateien von {url} herunter')
+ })
+})
+
+describe('register', () => {
+ beforeEach(() => {
+ setLocale('de_DE')
+ window._oc_l10n_registry_translations = undefined
+ window._oc_l10n_registry_plural_functions = undefined
+ })
+
+ it('with blank registry', () => {
+ register('app', {
+ Application: 'Anwendung',
+ '_%n guest_::_%n guests_': ['%n Gast', '%n Gäste'],
+ })
+ expect(translate('app', 'Application')).toBe('Anwendung')
+ expect(translatePlural('app', '%n guest', '%n guests', 1)).toBe('1 Gast')
+ expect(translatePlural('app', '%n guest', '%n guests', 2)).toBe('2 Gäste')
+ })
+
+ it('extend registered translations', () => {
+ window._oc_l10n_registry_translations = {
+ app: {
+ Application: 'Anwendung',
+ },
+ }
+ window._oc_l10n_registry_plural_functions = {
+ app: (t) => t === 1 ? 0 : 1,
+ }
+ register('app', {
+ Translation: 'Übersetzung',
+ })
+ expect(translate('app', 'Application')).toBe('Anwendung')
+ expect(translate('app', 'Translation')).toBe('Übersetzung')
+ })
+
+ it('extend with another new app', () => {
+ window._oc_l10n_registry_translations = {
+ core: {
+ 'Hello world!': 'Hallo Welt!',
+ },
+ }
+ window._oc_l10n_registry_plural_functions = {
+ core: (t) => t === 1 ? 0 : 1,
+ }
+ register('app', {
+ Application: 'Anwendung',
+ })
+ expect(translate('core', 'Hello world!')).toBe('Hallo Welt!')
+ expect(translate('app', 'Application')).toBe('Anwendung')
+ })
+
+ it('unregister', () => {
+ window._oc_l10n_registry_translations = {}
+ window._oc_l10n_registry_plural_functions = {}
+ register('app', {
+ Application: 'Anwendung',
+ })
+ _unregister('app')
+ expect(translate('app', 'Application')).toBe('Application')
+ })
+})