Skip to content

Commit a958c04

Browse files
authored
Merge pull request #851 from nextcloud-libraries/refactor/drop-node-gettext
refactor(gettext): Drop `node-gettext` dependency and use our translation logic
2 parents 1cc2694 + 5e82a6e commit a958c04

File tree

9 files changed

+173
-104
lines changed

9 files changed

+173
-104
lines changed

.eslintrc.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
{
22
"extends": [
3-
"@nextcloud"
3+
"@nextcloud/eslint-config/typescript"
44
],
55
"ignorePatterns": ["dist"],
66
"overrides": [
7+
{
8+
"files": ["**.ts"],
9+
"rules": {
10+
"no-useless-constructor": "off",
11+
"@typescript-eslint/no-useless-constructor": "error"
12+
}
13+
},
714
{
815
"files": ["tests/*.js"],
916
"rules": {

lib/gettext.ts

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,36 @@ const gt = getGettextBuilder()
1818
gt.gettext('some string to translate')
1919
```
2020
*/
21-
import GetText from 'node-gettext'
21+
import type { AppTranslations } from './registry.ts'
22+
import { getLanguage, getPlural, translate, translatePlural } from './index.ts'
2223

23-
import { getLanguage } from '.'
24+
export interface GettextTranslation {
25+
msgid: string
26+
msgid_plural?: string
27+
msgstr: string[]
28+
}
29+
30+
export interface GettextTranslationContext {
31+
[msgid: string]: GettextTranslation
32+
}
33+
34+
export interface GettextTranslationBundle {
35+
headers: {
36+
[headerName: string]: string
37+
},
38+
translations: {
39+
[context: string]: GettextTranslationContext
40+
}
41+
}
2442

2543
class GettextBuilder {
2644

27-
private locale?: string
28-
private translations = {} as Record<string, unknown>
2945
private debug = false
46+
private language = 'en'
47+
private translations = {} as Record<string, GettextTranslationBundle>
3048

31-
setLanguage(language: string): GettextBuilder {
32-
this.locale = language
49+
setLanguage(language: string): this {
50+
this.language = language
3351
return this
3452
}
3553

@@ -39,60 +57,56 @@ class GettextBuilder {
3957
*
4058
* @deprecated use `detectLanguage` instead.
4159
*/
42-
detectLocale(): GettextBuilder {
60+
detectLocale(): this {
4361
return this.detectLanguage()
4462
}
4563

4664
/**
4765
* Try to detect locale from context with `en` as fallback value.
4866
* This only works within a Nextcloud page context.
4967
*/
50-
detectLanguage(): GettextBuilder {
68+
detectLanguage(): this {
5169
return this.setLanguage(getLanguage().replace('-', '_'))
5270
}
5371

54-
addTranslation(language: string, data: unknown): GettextBuilder {
72+
addTranslation(language: string, data: GettextTranslationBundle): this {
5573
this.translations[language] = data
5674
return this
5775
}
5876

59-
enableDebugMode(): GettextBuilder {
77+
enableDebugMode(): this {
6078
this.debug = true
6179
return this
6280
}
6381

6482
build(): GettextWrapper {
65-
return new GettextWrapper(this.locale || 'en', this.translations, this.debug)
83+
if (this.debug) {
84+
console.debug(`Creating gettext instance for language ${this.language}`)
85+
}
86+
87+
const translations = Object.values(this.translations[this.language]?.translations[''] ?? {})
88+
.map(({ msgid, msgid_plural: msgidPlural, msgstr }) => {
89+
if (msgidPlural !== undefined) {
90+
return [`_${msgid}_::_${msgidPlural}_`, msgstr]
91+
}
92+
return [msgid, msgstr[0]]
93+
})
94+
95+
const bundle: AppTranslations = {
96+
pluralFunction: (n: number) => getPlural(n, this.language),
97+
translations: Object.fromEntries(translations),
98+
}
99+
100+
return new GettextWrapper(bundle)
66101
}
67102

68103
}
69104

70105
class GettextWrapper {
71106

72-
private gt: GetText
73-
74-
constructor(locale: string, data: Record<string|symbol|number, unknown>, debug: boolean) {
75-
this.gt = new GetText({
76-
debug,
77-
sourceLocale: 'en',
78-
})
79-
80-
for (const key in data) {
81-
this.gt.addTranslations(key, 'messages', data[key] as object)
82-
}
83-
84-
this.gt.setLocale(locale)
85-
}
86-
87-
private subtitudePlaceholders(translated: string, vars: Record<string, string | number>): string {
88-
return translated.replace(/{([^{}]*)}/g, (a, b) => {
89-
const r = vars[b]
90-
if (typeof r === 'string' || typeof r === 'number') {
91-
return r.toString()
92-
} else {
93-
return a
94-
}
95-
})
107+
constructor(
108+
private bundle: AppTranslations,
109+
) {
96110
}
97111

98112
/**
@@ -102,10 +116,7 @@ class GettextWrapper {
102116
* @param placeholders map of placeholder key to value
103117
*/
104118
gettext(original: string, placeholders: Record<string, string | number> = {}): string {
105-
return this.subtitudePlaceholders(
106-
this.gt.gettext(original),
107-
placeholders,
108-
)
119+
return translate('', original, placeholders, undefined, { bundle: this.bundle })
109120
}
110121

111122
/**
@@ -117,10 +128,7 @@ class GettextWrapper {
117128
* @param placeholders optional map of placeholder key to value
118129
*/
119130
ngettext(singular: string, plural: string, count: number, placeholders: Record<string, string | number> = {}): string {
120-
return this.subtitudePlaceholders(
121-
this.gt.ngettext(singular, plural, count).replace(/%n/g, count.toString()),
122-
placeholders,
123-
)
131+
return translatePlural('', singular, plural, count, placeholders, { bundle: this.bundle })
124132
}
125133

126134
}

lib/registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export interface NextcloudWindowWithRegistry extends Nextcloud.v27.WindowWithGlo
4646

4747
declare const window: NextcloudWindowWithRegistry
4848

49-
interface AppTranslations {
49+
export interface AppTranslations {
5050
translations: Translations
5151
pluralFunction: PluralFunction
5252
}

lib/translation.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*/
5-
import type { Translations } from './registry'
6-
import { getLanguage, getLocale } from './locale'
5+
import type { AppTranslations, Translations } from './registry.ts'
6+
import { generateFilePath } from '@nextcloud/router'
7+
import { getLanguage, getLocale } from './locale.ts'
78
import {
89
getAppTranslations,
910
hasAppTranslations,
1011
registerAppTranslations,
1112
unregisterAppTranslations,
12-
} from './registry'
13-
import { generateFilePath } from '@nextcloud/router'
13+
} from './registry.ts'
1414

1515
import DOMPurify from 'dompurify'
1616
import escapeHTML from 'escape-html'
@@ -20,6 +20,12 @@ interface TranslationOptions {
2020
escape?: boolean
2121
/** enable/disable sanitization (by default enabled) */
2222
sanitize?: boolean
23+
24+
/**
25+
* This is only intended for internal usage.
26+
* @private
27+
*/
28+
bundle?: AppTranslations
2329
}
2430

2531
interface TranslationVariableReplacementObject<T> {
@@ -116,7 +122,7 @@ export function translate<T extends string>(
116122
})
117123
}
118124

119-
const bundle = getAppTranslations(app)
125+
const bundle = options?.bundle ?? getAppTranslations(app)
120126
let translation = bundle.translations[text] || text
121127
translation = Array.isArray(translation) ? translation[0] : translation
122128

@@ -150,7 +156,7 @@ export function translatePlural<T extends string, K extends string, >(
150156
options?: TranslationOptions,
151157
): string {
152158
const identifier = '_' + textSingular + '_::_' + textPlural + '_'
153-
const bundle = getAppTranslations(app)
159+
const bundle = options?.bundle ?? getAppTranslations(app)
154160
const value = bundle.translations[identifier]
155161

156162
if (typeof value !== 'undefined') {
@@ -245,10 +251,10 @@ export function unregister(appName: string) {
245251
*
246252
*
247253
* @param {number} number the number of elements
254+
* @param {string|undefined} language the language to use (or autodetect if not set)
248255
* @return {number} 0 for the singular form(, 1 for the first plural form, ...)
249256
*/
250-
export function getPlural(number: number) {
251-
let language = getLanguage()
257+
export function getPlural(number: number, language = getLanguage()) {
252258
if (language === 'pt-BR') {
253259
// temporary set a locale for brazilian
254260
language = 'xbr'

package-lock.json

Lines changed: 44 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)