Skip to content

Commit

Permalink
Provide translate and translatePlural functions
Browse files Browse the repository at this point in the history
* Provide both functions, code is mostly taken from server
  some minor cleanup
* Added test cases
* Added *not* exported `registry` module for loading
  translations (so this is not exposed to users)

Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
  • Loading branch information
susnux committed Jan 3, 2023
1 parent b12de9e commit f4182fa
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 18 deletions.
79 changes: 64 additions & 15 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/// <reference types="@nextcloud/typings" />

declare var OC: Nextcloud.v16.OC | Nextcloud.v17.OC | Nextcloud.v18.OC | Nextcloud.v19.OC |
Nextcloud.v20.OC | Nextcloud.v21.OC | Nextcloud.v22.OC | Nextcloud.v23.OC |
Nextcloud.v24.OC;
declare var window: Nextcloud.v16.WindowWithGlobals | Nextcloud.v17.WindowWithGlobals | Nextcloud.v18.WindowWithGlobals | Nextcloud.v19.WindowWithGlobals;
declare var window: Nextcloud.v16.WindowWithGlobals
| Nextcloud.v17.WindowWithGlobals
| Nextcloud.v18.WindowWithGlobals
| Nextcloud.v19.WindowWithGlobals;

import DOMPurify from 'dompurify'
import escapeHTML from 'escape-html'
import { getAppTranslations } from './registry'

/**
* Returns the user's locale
Expand All @@ -24,7 +28,10 @@ export function getLanguage(): string {
}

interface TranslationOptions {
escape?: boolean
/** enable/disable auto escape of placeholders (by default enabled) */
escape?: boolean,
/** enable/disable sanitization (by default enabled) */
sanitize?: boolean,
}

/**
Expand All @@ -37,13 +44,44 @@ interface TranslationOptions {
* @param {object} [options] options object
* @return {string}
*/
export function translate(app: string, text: string, vars?: object, count?: number, options?: TranslationOptions): string {
if (typeof OC === 'undefined') {
console.warn('No OC found')
export function translate(app: string, text: string, vars?: Record<string, any>, number?: number, options?: TranslationOptions): string {
const defaultOptions = {
escape: true,
sanitize: true,
}
const allOptions = Object.assign({}, defaultOptions, options || {})

const identity = (value) => value
const optSanitize = allOptions.sanitize ? DOMPurify.sanitize : identity
const optEscape = allOptions.escape ? escapeHTML : identity

// TODO: cache this function to avoid inline recreation
// of the same function over and over again in case
// translate() is used in a loop
const _build = (text: string, vars?: Record<string, any>, number?: number) => {
return text
.replace(/%n/g, '' + number)
.replace(/{([^{}]*)}/g, (match, key) => {
if (vars === undefined || !(key in vars)) return optSanitize(match)

const r = vars[key]
if (typeof r === 'string' || typeof r === 'number') {
return optSanitize(optEscape(r))
} else {
return optSanitize(match)
}
}
)
}

return OC.L10N.translate(app, text, vars, count, options)
const bundle = getAppTranslations(app)
const translation = bundle.translations[text] || text

if (typeof vars === 'object' || number !== undefined) {
return optSanitize(_build(translation, vars, number))
} else {
return optSanitize(translation)
}
}

/**
Expand All @@ -52,19 +90,30 @@ export function translate(app: string, text: string, vars?: object, count?: numb
* @param {string} app the id of the app for which to translate the string
* @param {string} textSingular the string to translate for exactly one object
* @param {string} textPlural the string to translate for n objects
* @param {number} count number to determine whether to use singular or plural
* @param {number} number number to determine whether to use singular or plural
* @param {Object} vars of placeholder key to value
* @param {object} options options object
* @return {string}
*/

export function translatePlural(app: string, textSingular: string, textPlural: string, count: number, vars?: object, options?: TranslationOptions): string {
if (typeof OC === 'undefined') {
console.warn('No OC found')
return textSingular
export function translatePlural(app: string, textSingular: string, textPlural: string, number: number, vars?: object, options?: TranslationOptions): string {
const identifier = '_' + textSingular + '_::_' + textPlural + '_'
const bundle = getAppTranslations(app)
const value = bundle.translations[identifier]

if (typeof (value) !== 'undefined') {
const translation = value
if (Array.isArray(translation)) {
const plural = bundle.pluralFunction(number)
return translate(app, translation[plural], vars, number, options)
}
}

return OC.L10N.translatePlural(app, textSingular, textPlural, count, vars, options)
if (number === 1) {
return translate(app, textSingular, vars, number, options)
} else {
return translate(app, textPlural, vars, number, options)
}
}

/**
Expand Down
29 changes: 29 additions & 0 deletions lib/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
declare var window: {
_oc_l10n_registry_translations: Record<string, Record<string, string | undefined>>
_oc_l10n_registry_plural_functions: Record<string, (number: number) => number>
}

interface AppTranslations {
translations: Record<string, string | undefined>;
pluralFunction: (number: number) => number;
}

/**
* @param {string} appId the app id
* @return {object}
*/
export function getAppTranslations(appId: string): AppTranslations {
if (typeof window._oc_l10n_registry_translations === 'undefined' ||
typeof window._oc_l10n_registry_plural_functions === 'undefined') {
console.warn('No OC L10N registry found')
return {
translations: {},
pluralFunction: (number: number) => number
}
}

return {
translations: window._oc_l10n_registry_translations[appId] || {},
pluralFunction: window._oc_l10n_registry_plural_functions[appId],
}
}
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
},
"dependencies": {
"core-js": "^3.6.4",
"dompurify": "^2.4.1",
"escape-html": "^1.0.3",
"node-gettext": "^3.0.0"
},
"devDependencies": {
Expand Down
61 changes: 58 additions & 3 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,67 @@ import {
getDayNamesShort,
getDayNamesMin,
getMonthNames,
getMonthNamesShort
getMonthNamesShort,
translate,
translatePlural
} from '../lib/index'

describe('getCanonicalLocale', () => {
const setLocale = (locale) => document.documentElement.setAttribute('data-locale', locale)
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) => 1 === t ? 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('')
})
Expand Down

0 comments on commit f4182fa

Please sign in to comment.