Skip to content

Commit

Permalink
Merge pull request #851 from nextcloud-libraries/refactor/drop-node-g…
Browse files Browse the repository at this point in the history
…ettext

refactor(gettext): Drop `node-gettext` dependency and use our translation logic
  • Loading branch information
susnux authored Feb 6, 2025
2 parents 1cc2694 + 5e82a6e commit a958c04
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 104 deletions.
9 changes: 8 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
{
"extends": [
"@nextcloud"
"@nextcloud/eslint-config/typescript"
],
"ignorePatterns": ["dist"],
"overrides": [
{
"files": ["**.ts"],
"rules": {
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": "error"
}
},
{
"files": ["tests/*.js"],
"rules": {
Expand Down
94 changes: 51 additions & 43 deletions lib/gettext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,36 @@ const gt = getGettextBuilder()
gt.gettext('some string to translate')
```
*/
import GetText from 'node-gettext'
import type { AppTranslations } from './registry.ts'
import { getLanguage, getPlural, translate, translatePlural } from './index.ts'

import { getLanguage } from '.'
export interface GettextTranslation {
msgid: string
msgid_plural?: string
msgstr: string[]
}

export interface GettextTranslationContext {
[msgid: string]: GettextTranslation
}

export interface GettextTranslationBundle {
headers: {
[headerName: string]: string
},
translations: {
[context: string]: GettextTranslationContext
}
}

class GettextBuilder {

private locale?: string
private translations = {} as Record<string, unknown>
private debug = false
private language = 'en'
private translations = {} as Record<string, GettextTranslationBundle>

setLanguage(language: string): GettextBuilder {
this.locale = language
setLanguage(language: string): this {
this.language = language
return this
}

Expand All @@ -39,60 +57,56 @@ class GettextBuilder {
*
* @deprecated use `detectLanguage` instead.
*/
detectLocale(): GettextBuilder {
detectLocale(): this {
return this.detectLanguage()
}

/**
* Try to detect locale from context with `en` as fallback value.
* This only works within a Nextcloud page context.
*/
detectLanguage(): GettextBuilder {
detectLanguage(): this {
return this.setLanguage(getLanguage().replace('-', '_'))
}

addTranslation(language: string, data: unknown): GettextBuilder {
addTranslation(language: string, data: GettextTranslationBundle): this {
this.translations[language] = data
return this
}

enableDebugMode(): GettextBuilder {
enableDebugMode(): this {
this.debug = true
return this
}

build(): GettextWrapper {
return new GettextWrapper(this.locale || 'en', this.translations, this.debug)
if (this.debug) {
console.debug(`Creating gettext instance for language ${this.language}`)
}

const translations = Object.values(this.translations[this.language]?.translations[''] ?? {})
.map(({ msgid, msgid_plural: msgidPlural, msgstr }) => {
if (msgidPlural !== undefined) {
return [`_${msgid}_::_${msgidPlural}_`, msgstr]
}
return [msgid, msgstr[0]]
})

const bundle: AppTranslations = {
pluralFunction: (n: number) => getPlural(n, this.language),
translations: Object.fromEntries(translations),
}

return new GettextWrapper(bundle)
}

}

class GettextWrapper {

private gt: GetText

constructor(locale: string, data: Record<string|symbol|number, unknown>, debug: boolean) {
this.gt = new GetText({
debug,
sourceLocale: 'en',
})

for (const key in data) {
this.gt.addTranslations(key, 'messages', data[key] as object)
}

this.gt.setLocale(locale)
}

private subtitudePlaceholders(translated: string, vars: Record<string, string | number>): string {
return translated.replace(/{([^{}]*)}/g, (a, b) => {
const r = vars[b]
if (typeof r === 'string' || typeof r === 'number') {
return r.toString()
} else {
return a
}
})
constructor(
private bundle: AppTranslations,
) {
}

/**
Expand All @@ -102,10 +116,7 @@ class GettextWrapper {
* @param placeholders map of placeholder key to value
*/
gettext(original: string, placeholders: Record<string, string | number> = {}): string {
return this.subtitudePlaceholders(
this.gt.gettext(original),
placeholders,
)
return translate('', original, placeholders, undefined, { bundle: this.bundle })
}

/**
Expand All @@ -117,10 +128,7 @@ class GettextWrapper {
* @param placeholders optional map of placeholder key to value
*/
ngettext(singular: string, plural: string, count: number, placeholders: Record<string, string | number> = {}): string {
return this.subtitudePlaceholders(
this.gt.ngettext(singular, plural, count).replace(/%n/g, count.toString()),
placeholders,
)
return translatePlural('', singular, plural, count, placeholders, { bundle: this.bundle })
}

}
Expand Down
2 changes: 1 addition & 1 deletion lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface NextcloudWindowWithRegistry extends Nextcloud.v27.WindowWithGlo

declare const window: NextcloudWindowWithRegistry

interface AppTranslations {
export interface AppTranslations {
translations: Translations
pluralFunction: PluralFunction
}
Expand Down
22 changes: 14 additions & 8 deletions lib/translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { Translations } from './registry'
import { getLanguage, getLocale } from './locale'
import type { AppTranslations, Translations } from './registry.ts'
import { generateFilePath } from '@nextcloud/router'
import { getLanguage, getLocale } from './locale.ts'
import {
getAppTranslations,
hasAppTranslations,
registerAppTranslations,
unregisterAppTranslations,
} from './registry'
import { generateFilePath } from '@nextcloud/router'
} from './registry.ts'

import DOMPurify from 'dompurify'
import escapeHTML from 'escape-html'
Expand All @@ -20,6 +20,12 @@ interface TranslationOptions {
escape?: boolean
/** enable/disable sanitization (by default enabled) */
sanitize?: boolean

/**
* This is only intended for internal usage.
* @private
*/
bundle?: AppTranslations
}

interface TranslationVariableReplacementObject<T> {
Expand Down Expand Up @@ -116,7 +122,7 @@ export function translate<T extends string>(
})
}

const bundle = getAppTranslations(app)
const bundle = options?.bundle ?? getAppTranslations(app)
let translation = bundle.translations[text] || text
translation = Array.isArray(translation) ? translation[0] : translation

Expand Down Expand Up @@ -150,7 +156,7 @@ export function translatePlural<T extends string, K extends string, >(
options?: TranslationOptions,
): string {
const identifier = '_' + textSingular + '_::_' + textPlural + '_'
const bundle = getAppTranslations(app)
const bundle = options?.bundle ?? getAppTranslations(app)
const value = bundle.translations[identifier]

if (typeof value !== 'undefined') {
Expand Down Expand Up @@ -245,10 +251,10 @@ export function unregister(appName: string) {
*
*
* @param {number} number the number of elements
* @param {string|undefined} language the language to use (or autodetect if not set)
* @return {number} 0 for the singular form(, 1 for the first plural form, ...)
*/
export function getPlural(number: number) {
let language = getLanguage()
export function getPlural(number: number, language = getLanguage()) {
if (language === 'pt-BR') {
// temporary set a locale for brazilian
language = 'xbr'
Expand Down
69 changes: 44 additions & 25 deletions package-lock.json

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

Loading

0 comments on commit a958c04

Please sign in to comment.