@kemdict/gettext 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -0
- package/README.md +158 -0
- package/lib/gettext.js +233 -0
- package/lib/loaders.js +54 -0
- package/lib/plural-data.js +245 -0
- package/lib/plurals.js +76 -0
- package/package.json +45 -0
- package/types/gettext.d.ts +132 -0
- package/types/gettext.d.ts.map +1 -0
- package/types/loaders.d.ts +17 -0
- package/types/loaders.d.ts.map +1 -0
- package/types/plural-data.d.ts +24 -0
- package/types/plural-data.d.ts.map +1 -0
- package/types/plurals.d.ts +23 -0
- package/types/plurals.d.ts.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Copyright (c) 2011-2012 Andris Reinman
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
11
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
12
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
13
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
14
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
15
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
16
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @kemdict/gettext
|
|
2
|
+
|
|
3
|
+
A fork of [node-gettext](https://github.com/alexanderwallin/node-gettext). The hope is to add more stuff to the runtime behavior of node-gettext, as well as add extraction features.
|
|
4
|
+
|
|
5
|
+
The main use is for [Kemdict](https://github.com/kemdict/kemdict).
|
|
6
|
+
|
|
7
|
+
The design of not directly depending on gettext-parser will be kept.
|
|
8
|
+
|
|
9
|
+
## Stability
|
|
10
|
+
|
|
11
|
+
I do not commit to any backwards compatibility whatsoever at this early stage.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Supports contexts and plurals
|
|
18
|
+
- Load translations in the format returned by [gettext-parser](https://github.com/smhg/gettext-parser), so .json, .mo, and .po files can all be loaded
|
|
19
|
+
- Use plural forms from the PO file, with fallback for many languages
|
|
20
|
+
- Safe by default (there is effectively an allowlist of `plurals=` expressions), if more complicated expressions are needed and you trust the PO files then use trusted mode.
|
|
21
|
+
|
|
22
|
+
### Comparison with GNU gettext and node-gettext
|
|
23
|
+
|
|
24
|
+
(This is modified from node-gettext's explanation.)
|
|
25
|
+
|
|
26
|
+
1. **There is no global locale, nor is the current locale a state of the class.** When the Gettext instance itself holds the current locale and is used asynchronously (like on a server handling requests), [a request may set the locale to something else before another one has finished using it](https://github.com/alexanderwallin/node-gettext/issues/67). This library instead binds the “current” locale per set of translation functions.
|
|
27
|
+
|
|
28
|
+
To infer the current locale from environment variables, use the `guessEnvLocale` function, which should read the LANG etc. variables basically just like GNU gettext does.
|
|
29
|
+
|
|
30
|
+
2. **There are no categories.** GNU gettext features [categories such as `LC_MESSAGES`, `LC_NUMERIC` and `LC_MONETARY`](https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Environment-Variables), which are better handled with other libraries in the JS world. This is just like node-gettext.
|
|
31
|
+
3. **There are no domains.** Like [Python's gettext](https://docs.python.org/3/library/gettext.html), this library takes a class-based approach, so if you need multiple sets of translations simply use multiple instances of the `Gettext` class. node-gettext retains domains.
|
|
32
|
+
4. **Translations have to be loaded from the file system in a separate step.**
|
|
33
|
+
|
|
34
|
+
GNU gettext is a C library that reads files from the file system: after using `bindtextdomain(domain, localesDirPath)` and `setlocale(category, locale)`, those four parameters are then used to read the appropriate translations file.
|
|
35
|
+
|
|
36
|
+
This library (like `node-gettext`) has a goal to work both on the server and in browsers, and the file system may not always be available. Therefore it is up to the developer to read translation files from disk and provide them in the constructor.
|
|
37
|
+
|
|
38
|
+
Various loaders are also provided as wrappers over `gettext-parser`; see below. In environments supporting Rollup plugins, use rollup-plugin-gettext (also developed in this repository) to import and bundle translation data.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
TODO. This package is not yet published; the name isn't even final yet (I'm not sure I actually want to publish this as @kemdict/gettext).
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import Gettext from '@kemdict/gettext'
|
|
48
|
+
import swedishTranslations from './translations/sv-SE.json'
|
|
49
|
+
|
|
50
|
+
const gt = new Gettext({
|
|
51
|
+
translations: {
|
|
52
|
+
'sv-SE': swedishTranslations
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
const { gettext } = gt.bindLocale('sv-SE')
|
|
56
|
+
|
|
57
|
+
gettext('The world is a funny place')
|
|
58
|
+
// -> "Världen är en underlig plats"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Recipes
|
|
62
|
+
|
|
63
|
+
#### Load and add translations from .mo or .po files
|
|
64
|
+
|
|
65
|
+
`@kemdict/gettext` expects translations to be in the format specified by [`gettext-parser`](https://github.com/smhg/gettext-parser). We also provide various loaders to conveniently read translation files on disk.
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import Gettext from '@kemdict/gettext'
|
|
69
|
+
import { loadTranslations } from '@kemdict/gettext/loaders'
|
|
70
|
+
const gt = new Gettext({
|
|
71
|
+
// this reads and parses all .po files under path/to/locales
|
|
72
|
+
// their filenames are the locale names ('de.po' creates an entry for 'de')
|
|
73
|
+
translations: loadTranslations('path/to/locales')
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Reference
|
|
78
|
+
|
|
79
|
+
### "@kemdict/gettext"
|
|
80
|
+
|
|
81
|
+
#### guessEnvLocale(env) → string[]
|
|
82
|
+
|
|
83
|
+
Guess or lookup the preferred language list from environment variables.
|
|
84
|
+
|
|
85
|
+
`env` defaults to `process.env`.
|
|
86
|
+
|
|
87
|
+
#### class Gettext({ sourceLocale, translations, trusted })
|
|
88
|
+
|
|
89
|
+
The main class. This class holds the loaded translation catalogs.
|
|
90
|
+
|
|
91
|
+
`sourceLocale` specifies which locale source text is written in. I am not sure this is necessary or useful.
|
|
92
|
+
|
|
93
|
+
`translations` is in the shape of `Record<string, GetTextTranslations>` where `GetTextTranslations` is the return type of `gettext-parser`'s parsers.
|
|
94
|
+
|
|
95
|
+
`trusted` specifies whether the PO file is trusted to be safe. By default the Plural-Forms expression is matched against a lookup table; with `trusted` as true the Plural-Forms expression will be directly used to create a JS function.
|
|
96
|
+
|
|
97
|
+
##### gettext.getLocales() → string[]
|
|
98
|
+
|
|
99
|
+
Return the list of locales that are in this gettext instance's catalog.
|
|
100
|
+
|
|
101
|
+
##### gettext.bindLocale(locales) → Translators
|
|
102
|
+
|
|
103
|
+
Return an object of translator functions that will translate as if `locales` is the “current” locale.
|
|
104
|
+
|
|
105
|
+
#### Translators
|
|
106
|
+
|
|
107
|
+
This object actually doesn't have a name in code but I'm abstracting them like this here.
|
|
108
|
+
|
|
109
|
+
This is an object of functions which can all be used standalone without needing to `.bind(this)`.
|
|
110
|
+
|
|
111
|
+
##### Translators.gettext(msgid), _(msgid)
|
|
112
|
+
|
|
113
|
+
Translate a string like gettext.
|
|
114
|
+
|
|
115
|
+
##### Translators.ngettext(msgid, msgidPlural, count)
|
|
116
|
+
|
|
117
|
+
Translate a string with plural handling like ngettext.
|
|
118
|
+
|
|
119
|
+
##### Translators.pgettext(msgctxt, msgid)
|
|
120
|
+
|
|
121
|
+
Translate a string with context like pgettext.
|
|
122
|
+
|
|
123
|
+
##### Translators.npgettext(msgctxt, msgid, msgidPlural, count)
|
|
124
|
+
|
|
125
|
+
Translate a string with context and plural handling like npgettext.
|
|
126
|
+
|
|
127
|
+
### Loaders ("@kemdict/gettext/loaders.js")
|
|
128
|
+
|
|
129
|
+
#### bindtextdomain(domain, ...localesDirs) → Record<string, GetTextTranslations>
|
|
130
|
+
|
|
131
|
+
Load MO files from the directories `localesDirs`. These directories should be arranged like /usr/share/locale, i.e. `<locale>/LC_MESSAGES/<domain>.mo`.
|
|
132
|
+
|
|
133
|
+
Loading translations from [Elisa](http://apps.kde.org/elisa) if it's installed for example:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import Gettext, { guessEnvLocale } from "@kemdict/gettext";
|
|
137
|
+
import { bindtextdomain } from "@kemdict/gettext/loaders";
|
|
138
|
+
const gt = new Gettext({
|
|
139
|
+
translations: bindtextdomain("elisa", "/usr/share/locale/"),
|
|
140
|
+
});
|
|
141
|
+
const { pgettext } = gt.bindLocale(guessEnvLocale());
|
|
142
|
+
pgettext("@title:window", "Choose Folder") // 選擇資料夾 in zh_TW
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### loadTranslations(dir) → Record<string, GetTextTranslations>
|
|
146
|
+
|
|
147
|
+
Load PO files from `dir`. `dir` should contain one PO file for each locale, like `<dir>/zh_TW.po`, `<dir>/de.po`, `<dir>/sv.po`, and so on.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
|
152
|
+
|
|
153
|
+
## See also
|
|
154
|
+
|
|
155
|
+
- [node-gettext](https://github.com/alexanderwallin/node-gettext) - where this library forked from, because I have way too many major changes that I want to implement
|
|
156
|
+
- [gettext-parser](https://github.com/smhg/gettext-parser) - Parsing and compiling gettext translations between .po/.mo files and JSON
|
|
157
|
+
- [lioness](https://github.com/alexanderwallin/lioness) - Gettext library for React
|
|
158
|
+
- [react-gettext-parser](https://github.com/laget-se/react-gettext-parser) - Extracting gettext translatable strings from JS(X) code
|
package/lib/gettext.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { parsePluralForms, fallbackPluralForms } from "./plurals.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Guess or lookup the preferred language list from environment variables.
|
|
5
|
+
* @param {Record<string, string | undefined>} env
|
|
6
|
+
* A map of environment variables. Defaults to process.env.
|
|
7
|
+
* @returns string[] | undefined
|
|
8
|
+
*/
|
|
9
|
+
export function guessEnvLocale(env = process?.env) {
|
|
10
|
+
if (!env) return;
|
|
11
|
+
// If $LANG is C, C.<encoding>, or POSIX: return msgid untranslated.
|
|
12
|
+
// Prefer LANGUAGE, then LC_ALL, then LC_MESSAGES, then LANG.
|
|
13
|
+
const LANG = env["LANG"];
|
|
14
|
+
if (LANG && (LANG === "C" || LANG.startsWith("C.") || LANG === "POSIX")) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
/** @type Set<string> */
|
|
18
|
+
const locales = new Set();
|
|
19
|
+
const LANGUAGE = env["LANGUAGE"];
|
|
20
|
+
if (LANGUAGE)
|
|
21
|
+
LANGUAGE.split(":").forEach((lang) => {
|
|
22
|
+
locales.add(lang);
|
|
23
|
+
});
|
|
24
|
+
const LC_ALL = env["LC_ALL"];
|
|
25
|
+
if (LC_ALL) locales.add(LC_ALL);
|
|
26
|
+
const LC_MESSAGES = env["LC_MESSAGES"];
|
|
27
|
+
if (LC_MESSAGES) locales.add(LC_MESSAGES);
|
|
28
|
+
if (LANG) locales.add(LANG);
|
|
29
|
+
return [...locales];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @import { GetTextTranslations } from "gettext-parser";
|
|
34
|
+
* @typedef {string} Locale
|
|
35
|
+
* @typedef {{ eventName: string, callback: Function }} Listener
|
|
36
|
+
* @typedef {GetTextTranslations} Catalog
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export default class Gettext {
|
|
40
|
+
/** @type Map<Locale, Catalog> */
|
|
41
|
+
catalogs = new Map();
|
|
42
|
+
/** @type Array<Listener> */
|
|
43
|
+
listeners = [];
|
|
44
|
+
trusted = false;
|
|
45
|
+
/**
|
|
46
|
+
* Creates and returns a new Gettext instance.
|
|
47
|
+
*
|
|
48
|
+
* @typedef {Object} Options - a set of options
|
|
49
|
+
* @property {string} [sourceLocale] - The locale that the source code and its
|
|
50
|
+
* texts are written in. Translations for
|
|
51
|
+
* this locale is not necessary.
|
|
52
|
+
* @property {Record<Locale, Catalog>} [translations] - Translations to add to the catalog
|
|
53
|
+
* @property {boolean} [trusted] - Trust that the plural forms code in
|
|
54
|
+
* `translations` are safe to evaluate.
|
|
55
|
+
* @param {Options} [options]
|
|
56
|
+
*/
|
|
57
|
+
constructor(options) {
|
|
58
|
+
options = options || {};
|
|
59
|
+
|
|
60
|
+
// Set source locale
|
|
61
|
+
this.sourceLocale = "";
|
|
62
|
+
if (options.sourceLocale) {
|
|
63
|
+
this.sourceLocale = options.sourceLocale;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options.translations) {
|
|
67
|
+
for (const [locale, catalog] of Object.entries(
|
|
68
|
+
options.translations,
|
|
69
|
+
)) {
|
|
70
|
+
this.catalogs.set(locale, catalog);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (options.trusted) this.trusted = true;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Return locales currently added to the catalogs.
|
|
77
|
+
*/
|
|
78
|
+
getLocales() {
|
|
79
|
+
return this.catalogs.keys();
|
|
80
|
+
}
|
|
81
|
+
// NOTE: This function is actually relatively hot, since every component and
|
|
82
|
+
// every module would call it. But caching this could only really be faster
|
|
83
|
+
// if we generate the key from the arguments with something with maybe 2
|
|
84
|
+
// inputs, anything more complex would literally just be slower. At best
|
|
85
|
+
// it's a 2x speed increase, but we're talking about going from 0.0002ms per
|
|
86
|
+
// bindLocale call to 0.0001ms here. It's not worth it.
|
|
87
|
+
/**
|
|
88
|
+
* Return functions that translate strings into `locale`.
|
|
89
|
+
* This allows not having global state while also not having to pass the
|
|
90
|
+
* locale for every call.
|
|
91
|
+
*
|
|
92
|
+
* @param {Locale[] | Locale | undefined} locales
|
|
93
|
+
* A string to use as a locale, or an array of locales to try to match for,
|
|
94
|
+
* or undefined which means to not do any translations.
|
|
95
|
+
*/
|
|
96
|
+
bindLocale(locales) {
|
|
97
|
+
const localesArr = !locales
|
|
98
|
+
? []
|
|
99
|
+
: Array.isArray(locales)
|
|
100
|
+
? locales
|
|
101
|
+
: [locales];
|
|
102
|
+
|
|
103
|
+
// The value of `this` would no longer be our instance if we call each
|
|
104
|
+
// of the functions as standalone functions. This reference to our
|
|
105
|
+
// instance, on the other hand, will not change even when the returned
|
|
106
|
+
// functions are called as standalone functions.
|
|
107
|
+
const self = this;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The base function for all variants.
|
|
111
|
+
* This does not need to take `locale` as an input, because all functions
|
|
112
|
+
* resulting from a given `.with` call all use the same locale.
|
|
113
|
+
*
|
|
114
|
+
* @param {string | null | undefined} msgctxt - Translation context. undefined or empty string means no context.
|
|
115
|
+
* @param {string} msgid - String to be translated
|
|
116
|
+
* @param {string} [msgidPlural] - If no translation was found, return this on count!=1
|
|
117
|
+
* @param {number} [count] - Number count for the plural
|
|
118
|
+
* @return {string}
|
|
119
|
+
*
|
|
120
|
+
*/
|
|
121
|
+
const baseGettext = (msgctxt, msgid, msgidPlural, count) => {
|
|
122
|
+
const context = msgctxt || "";
|
|
123
|
+
|
|
124
|
+
let defaultTranslation = msgid;
|
|
125
|
+
if (count !== undefined && !isNaN(count) && count !== 1) {
|
|
126
|
+
defaultTranslation = msgidPlural || msgid;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const locale of localesArr) {
|
|
130
|
+
if (
|
|
131
|
+
locale === "C" ||
|
|
132
|
+
locale === "POSIX" ||
|
|
133
|
+
locale.startsWith("C.")
|
|
134
|
+
) {
|
|
135
|
+
return defaultTranslation;
|
|
136
|
+
}
|
|
137
|
+
const pluralFunc = self._getCatalogPluralForms(locale).plural;
|
|
138
|
+
const catalog = self.catalogs.get(locale);
|
|
139
|
+
const translation = catalog?.translations?.[context]?.[msgid];
|
|
140
|
+
if (!translation) continue;
|
|
141
|
+
|
|
142
|
+
/** @type {boolean | number} */
|
|
143
|
+
let index = typeof count === "number" ? pluralFunc(count) : 0;
|
|
144
|
+
if (typeof index === "boolean") {
|
|
145
|
+
index = index ? 1 : 0;
|
|
146
|
+
}
|
|
147
|
+
const msgstr = translation.msgstr[index];
|
|
148
|
+
if (msgstr) return msgstr;
|
|
149
|
+
}
|
|
150
|
+
return defaultTranslation;
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
/**
|
|
154
|
+
* Translate a string.
|
|
155
|
+
* The locale is implicit.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} msgid - String to be translated
|
|
158
|
+
* @return {string} Translation or the original string if no translation was found
|
|
159
|
+
*/
|
|
160
|
+
gettext(msgid) {
|
|
161
|
+
return baseGettext(undefined, msgid);
|
|
162
|
+
},
|
|
163
|
+
/**
|
|
164
|
+
* Translate a string.
|
|
165
|
+
* Same as `gettext`.
|
|
166
|
+
* The locale is implicit.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} msgid - String to be translated
|
|
169
|
+
* @return {string} Translation or the original string if no translation was found
|
|
170
|
+
*/
|
|
171
|
+
_(msgid) {
|
|
172
|
+
return baseGettext(undefined, msgid);
|
|
173
|
+
},
|
|
174
|
+
/**
|
|
175
|
+
* Translate a plural string.
|
|
176
|
+
* The locale is implicit.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} msgid String to be translated when count is not plural
|
|
179
|
+
* @param {string} msgidPlural String to be translated when count is plural
|
|
180
|
+
* @param {number} count Number count for the plural
|
|
181
|
+
* @return {string} Translation or the original string if no translation was found
|
|
182
|
+
*/
|
|
183
|
+
ngettext(msgid, msgidPlural, count) {
|
|
184
|
+
return baseGettext(undefined, msgid, msgidPlural, count);
|
|
185
|
+
},
|
|
186
|
+
/**
|
|
187
|
+
* Translate a string from a specific context.
|
|
188
|
+
* The locale is implicit.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} msgctxt Translation context
|
|
191
|
+
* @param {string} msgid String to be translated
|
|
192
|
+
* @return {string} Translation or the original string if no translation was found
|
|
193
|
+
*/
|
|
194
|
+
pgettext(msgctxt, msgid) {
|
|
195
|
+
return baseGettext(msgctxt, msgid);
|
|
196
|
+
},
|
|
197
|
+
/**
|
|
198
|
+
* Translate a plural string from a specific context.
|
|
199
|
+
* The locale is implicit.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} msgctxt Translation context
|
|
202
|
+
* @param {string} msgid String to be translated when count is not plural
|
|
203
|
+
* @param {string} msgidPlural String to be translated when count is plural
|
|
204
|
+
* @param {number} count Number count for the plural
|
|
205
|
+
* @return {string} Translation or the original string if no translation was found
|
|
206
|
+
*/
|
|
207
|
+
npgettext(msgctxt, msgid, msgidPlural, count) {
|
|
208
|
+
return baseGettext(msgctxt, msgid, msgidPlural, count);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Cache for computed plural forms.
|
|
214
|
+
* Right now the computation is just normalizing the value then looking it
|
|
215
|
+
* up, but this already benefits from a cache.
|
|
216
|
+
* @type {Map<string, import("./plural-data.js").PluralFormsObj>}
|
|
217
|
+
*/
|
|
218
|
+
_pluralForms = new Map();
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Return plural forms header for the current catalogs for `locale`.
|
|
222
|
+
* @param {string} locale - The locale name
|
|
223
|
+
*/
|
|
224
|
+
_getCatalogPluralForms(locale) {
|
|
225
|
+
return this._pluralForms.getOrInsertComputed(locale, () => {
|
|
226
|
+
const header = this.catalogs.get(locale)?.headers["Plural-Forms"];
|
|
227
|
+
return header
|
|
228
|
+
? parsePluralForms(header, this.trusted) ||
|
|
229
|
+
fallbackPluralForms(locale)
|
|
230
|
+
: fallbackPluralForms(locale);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
package/lib/loaders.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** @import {Catalog, Locale} from "./gettext.js" */
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { po, mo } from "gettext-parser";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Load MO translations the directories `localesDirs`.
|
|
9
|
+
*
|
|
10
|
+
* These directories should be arranged similar to /usr/share/locale, like
|
|
11
|
+
* <dir>/<locale>/LC_MESSAGES/<domain>.mo.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} domain
|
|
14
|
+
* @param {...string} localesDirs
|
|
15
|
+
*/
|
|
16
|
+
export function bindtextdomain(domain, ...localesDirs) {
|
|
17
|
+
/** @type Record<Locale, Catalog> */
|
|
18
|
+
const catalogs = {};
|
|
19
|
+
for (const localesDir of localesDirs) {
|
|
20
|
+
if (!fs.existsSync(localesDir)) continue;
|
|
21
|
+
for (const locale of fs.readdirSync(localesDir)) {
|
|
22
|
+
const localePath = path.join(localesDir, locale);
|
|
23
|
+
if (!fs.statSync(localePath).isDirectory()) continue;
|
|
24
|
+
const messagesPath = path.join(localePath, "LC_MESSAGES");
|
|
25
|
+
if (!fs.existsSync(messagesPath)) continue;
|
|
26
|
+
if (!fs.statSync(messagesPath).isDirectory()) continue;
|
|
27
|
+
const domainPath = path.join(messagesPath, `${domain}.mo`);
|
|
28
|
+
if (!fs.existsSync(domainPath)) continue;
|
|
29
|
+
|
|
30
|
+
const translations = mo.parse(fs.readFileSync(domainPath));
|
|
31
|
+
catalogs[locale] = translations;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return catalogs;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load PO translations from `dir`.
|
|
39
|
+
* `dir` should be structured like <dir>/<locale>.po.
|
|
40
|
+
* @param {string} dir
|
|
41
|
+
*/
|
|
42
|
+
export function loadTranslations(dir) {
|
|
43
|
+
/** @type Record<Locale, Catalog> */
|
|
44
|
+
const catalogs = {};
|
|
45
|
+
for (const file of fs.readdirSync(dir)) {
|
|
46
|
+
if (file.endsWith(".po")) {
|
|
47
|
+
const translations = po.parse(
|
|
48
|
+
fs.readFileSync(path.join(dir, file)),
|
|
49
|
+
);
|
|
50
|
+
catalogs[file.slice(0, -3)] = translations;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return catalogs;
|
|
54
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// The fallback plurals of this file are from GNU Gettext's plural-table.c as
|
|
2
|
+
// well as converted from Unicode CLDR.
|
|
3
|
+
|
|
4
|
+
// FIXME obviously this is not ideal!
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {((n: number) => number) | ((n: number) => boolean)} PluralFunc
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* nplurals: number;
|
|
10
|
+
* plural: PluralFunc
|
|
11
|
+
* }} PluralFormsObj */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lookup table for existing "plural=" expressions to "parsed" function values.
|
|
15
|
+
* The keys are normalized to always start and end with parens to reduce (though
|
|
16
|
+
* not eliminate) duplicate keys.
|
|
17
|
+
* @type Record<string, PluralFunc>
|
|
18
|
+
*/
|
|
19
|
+
export const pluralFuncTable = {
|
|
20
|
+
"(n==1?0:n==2?1:n==0||(n%100>=3&&n%100<=10)?2:n%100>=11&&n%100<=19?3:4)": (
|
|
21
|
+
n,
|
|
22
|
+
) =>
|
|
23
|
+
n == 1
|
|
24
|
+
? 0
|
|
25
|
+
: n == 2
|
|
26
|
+
? 1
|
|
27
|
+
: n == 0 || (n % 100 >= 3 && n % 100 <= 10)
|
|
28
|
+
? 2
|
|
29
|
+
: n % 100 >= 11 && n % 100 <= 19
|
|
30
|
+
? 3
|
|
31
|
+
: 4,
|
|
32
|
+
"(n%10==1&&n%100!=11)": (n) => n % 10 == 1 && n % 100 != 11,
|
|
33
|
+
"(n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11&&n%100<=99?4:5)": (
|
|
34
|
+
n,
|
|
35
|
+
) =>
|
|
36
|
+
n == 0
|
|
37
|
+
? 0
|
|
38
|
+
: n == 1
|
|
39
|
+
? 1
|
|
40
|
+
: n == 2
|
|
41
|
+
? 2
|
|
42
|
+
: n % 100 >= 3 && n % 100 <= 10
|
|
43
|
+
? 3
|
|
44
|
+
: n % 100 >= 11 && n % 100 <= 99
|
|
45
|
+
? 4
|
|
46
|
+
: 5,
|
|
47
|
+
"((n==1)?0:(n>=2&&n<=4)?1:2)": (n) =>
|
|
48
|
+
n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2,
|
|
49
|
+
"((n==1||n==11)?0:(n==2||n==12)?1:(n>2&&n<20)?2:3)": (n) =>
|
|
50
|
+
n == 1 || n == 11 ? 0 : n == 2 || n == 12 ? 1 : n > 2 && n < 20 ? 2 : 3,
|
|
51
|
+
"(0)": () => 0,
|
|
52
|
+
"(1)": () => 1,
|
|
53
|
+
"(n!=1)": (n) => n != 1,
|
|
54
|
+
"(n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3)": (n) =>
|
|
55
|
+
n % 100 == 1
|
|
56
|
+
? 0
|
|
57
|
+
: n % 100 == 2
|
|
58
|
+
? 1
|
|
59
|
+
: n % 100 == 3 || n % 100 == 4
|
|
60
|
+
? 2
|
|
61
|
+
: 3,
|
|
62
|
+
"(n%100==1?0:n%100==2?1:n%100>=3&&n%100<=4?2:3)": (n) =>
|
|
63
|
+
n % 100 == 1
|
|
64
|
+
? 0
|
|
65
|
+
: n % 100 == 2
|
|
66
|
+
? 1
|
|
67
|
+
: n % 100 >= 3 && n % 100 <= 4
|
|
68
|
+
? 2
|
|
69
|
+
: 3,
|
|
70
|
+
"(n%100==1?1:n%100==2?2:n%100==3||n%100==4?3:0)": (n) =>
|
|
71
|
+
n % 100 == 1
|
|
72
|
+
? 1
|
|
73
|
+
: n % 100 == 2
|
|
74
|
+
? 2
|
|
75
|
+
: n % 100 == 3 || n % 100 == 4
|
|
76
|
+
? 3
|
|
77
|
+
: 0,
|
|
78
|
+
"(n%10==1&&n%100!=11?0:n!=0?1:2)": (n) =>
|
|
79
|
+
n % 10 == 1 && n % 100 != 11 ? 0 : n != 0 ? 1 : 2,
|
|
80
|
+
"(n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?1:2)": (n) =>
|
|
81
|
+
n % 10 == 1 && n % 100 != 11
|
|
82
|
+
? 0
|
|
83
|
+
: n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20)
|
|
84
|
+
? 1
|
|
85
|
+
: 2,
|
|
86
|
+
"(n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2)": (
|
|
87
|
+
n,
|
|
88
|
+
) =>
|
|
89
|
+
n % 10 == 1 && n % 100 != 11
|
|
90
|
+
? 0
|
|
91
|
+
: n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)
|
|
92
|
+
? 1
|
|
93
|
+
: 2,
|
|
94
|
+
"(n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<12||n%100>14)?1:n%10==0||n%10>=5&&n%10<=9||n%100>=11&&n%100<=14?2:3)":
|
|
95
|
+
(n) =>
|
|
96
|
+
n % 10 == 1 && n % 100 != 11
|
|
97
|
+
? 0
|
|
98
|
+
: n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)
|
|
99
|
+
? 1
|
|
100
|
+
: n % 10 == 0 ||
|
|
101
|
+
(n % 10 >= 5 && n % 10 <= 9) ||
|
|
102
|
+
(n % 100 >= 11 && n % 100 <= 14)
|
|
103
|
+
? 2
|
|
104
|
+
: 3,
|
|
105
|
+
"(n%10==1?0:n%10==2?1:2)": (n) => (n % 10 == 1 ? 0 : n % 10 == 2 ? 1 : 2),
|
|
106
|
+
"(n==0?0:n==1?1:n==2?2:3)": (n) =>
|
|
107
|
+
n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : 3,
|
|
108
|
+
"(n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11?4:5)": (n) =>
|
|
109
|
+
n == 0
|
|
110
|
+
? 0
|
|
111
|
+
: n == 1
|
|
112
|
+
? 1
|
|
113
|
+
: n == 2
|
|
114
|
+
? 2
|
|
115
|
+
: n % 100 >= 3 && n % 100 <= 10
|
|
116
|
+
? 3
|
|
117
|
+
: n % 100 >= 11
|
|
118
|
+
? 4
|
|
119
|
+
: 5,
|
|
120
|
+
"(n==0||n==1)": (n) => n == 0 || n == 1,
|
|
121
|
+
"(n==1)?0:((n==2)?1:((n>10&&n%10==0)?2:3))": (n) =>
|
|
122
|
+
n == 1 ? 0 : n == 2 ? 1 : n > 10 && n % 10 == 0 ? 2 : 3,
|
|
123
|
+
"(n==1?0:(n==0||(n%100>0&&n%100<20))?1:2)": (n) =>
|
|
124
|
+
n == 1 ? 0 : n == 0 || (n % 100 > 0 && n % 100 < 20) ? 1 : 2,
|
|
125
|
+
"(n==1?0:n%10>=2&&(n%100<10||n%100>=20)?1:n%10==0||(n%100>10&&n%100<20)?2:3)":
|
|
126
|
+
(n) =>
|
|
127
|
+
n == 1
|
|
128
|
+
? 0
|
|
129
|
+
: n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20)
|
|
130
|
+
? 1
|
|
131
|
+
: n % 10 == 0 || (n % 100 > 10 && n % 100 < 20)
|
|
132
|
+
? 2
|
|
133
|
+
: 3,
|
|
134
|
+
"(n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2)": (n) =>
|
|
135
|
+
n == 1
|
|
136
|
+
? 0
|
|
137
|
+
: n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)
|
|
138
|
+
? 1
|
|
139
|
+
: 2,
|
|
140
|
+
"(n==1?0:n==2?1:2)": (n) => (n == 1 ? 0 : n == 2 ? 1 : 2),
|
|
141
|
+
"(n==1?0:n==2?1:n<7?2:n<11?3:4)": (n) =>
|
|
142
|
+
n == 1 ? 0 : n == 2 ? 1 : n < 7 ? 2 : n < 11 ? 3 : 4,
|
|
143
|
+
"(n==1?3:n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2)":
|
|
144
|
+
(n) =>
|
|
145
|
+
n == 1
|
|
146
|
+
? 3
|
|
147
|
+
: n % 10 == 1 && n % 100 != 11
|
|
148
|
+
? 0
|
|
149
|
+
: n % 10 >= 2 &&
|
|
150
|
+
n % 10 <= 4 &&
|
|
151
|
+
(n % 100 < 10 || n % 100 >= 20)
|
|
152
|
+
? 1
|
|
153
|
+
: 2,
|
|
154
|
+
"(n>1)": (n) => n > 1,
|
|
155
|
+
"(n>2)": (n) => n > 2,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// from Gettext's plural-table.c
|
|
159
|
+
// prettier-ignore
|
|
160
|
+
const pluralTable = {
|
|
161
|
+
"ja vi ko": "nplurals=1; plural=0;",
|
|
162
|
+
"en de nl sv da no nb nn fo es pt it bg el fi et he eo hu tr ca": "nplurals=2; plural=(n != 1);",
|
|
163
|
+
"pt_BR fr": "nplurals=2; plural=(n > 1);",
|
|
164
|
+
"lv": "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);",
|
|
165
|
+
"ga": "nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;",
|
|
166
|
+
"ro": "nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;",
|
|
167
|
+
"lt": "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);",
|
|
168
|
+
"ru uk be sr hr": "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);",
|
|
169
|
+
"cs sk": "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;",
|
|
170
|
+
"pl": "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);",
|
|
171
|
+
"sl": "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// from Unicode CLDR (plurals.xml), converted with Gettext's cldr-plurals program
|
|
175
|
+
// prettier-ignore
|
|
176
|
+
const cldrPlurals = {
|
|
177
|
+
"bm bo dz hnj id ig ii in ja jbo jv jw kde kea km ko lkt lo ms my nqo osa root sah ses sg su th to tpi vi wo yo yue zh":
|
|
178
|
+
"nplurals=1; plural=0;",
|
|
179
|
+
"am as bn doi fa gu hi kn kok kok_Latn pcm zu":
|
|
180
|
+
"nplurals=2; plural=(n==0 || n==1);",
|
|
181
|
+
"ff hy kab": "nplurals=2; plural=(n > 1);",
|
|
182
|
+
"ast de en et fi fy gl ia ie io ji lij nl sc sv sw ur yi":
|
|
183
|
+
"nplurals=2; plural=(n != 1);",
|
|
184
|
+
"si": "nplurals=2; plural=(n > 1);",
|
|
185
|
+
"ak bho csw guw ln mg nso pa ti wa": "nplurals=2; plural=(n > 1);",
|
|
186
|
+
"tzm": "nplurals=2; plural=(n<=1 || (n>=11 && n<=99));",
|
|
187
|
+
"af an asa az bal bem bez bg brx ce cgg chr ckb dv ee el eo eu fo fur gsw ha haw hu jgo jmc ka kaj kcg kk kkj kl ks ksb ku ky lb lg mas mgo ml mn mr nah nb nd ne nn nnh no nr ny nyn om or os pap ps rm rof rwk saq sd sdh seh sn so sq ss ssy st syr ta te teo tig tk tn tr ts ug uz ve vo vun wae xh xog":
|
|
188
|
+
"nplurals=2; plural=(n != 1);",
|
|
189
|
+
"da": "nplurals=2; plural=(n != 1);",
|
|
190
|
+
"is": "nplurals=2; plural=(n%10==1 && n%100!=11);",
|
|
191
|
+
"mk": "nplurals=2; plural=(n%10==1 && n%100!=11);",
|
|
192
|
+
"ceb fil tl":
|
|
193
|
+
"nplurals=2; plural=(n==1 || n==2 || n==3 || (n%10!=4 && n%10!=6 && n%10!=9));",
|
|
194
|
+
"lv prg":
|
|
195
|
+
"nplurals=3; plural=(n%10==0 || (n%100>=11 && n%100<=19) ? 0 : n%10==1 && n%100!=11 ? 1 : 2);",
|
|
196
|
+
"lag": "nplurals=3; plural=(n==0 ? 0 : (n==0 || n==1) && n!=0 ? 1 : 2);",
|
|
197
|
+
"blo cv ksh": "nplurals=3; plural=(n==0 ? 0 : n==1 ? 1 : 2);",
|
|
198
|
+
"he iw": "nplurals=3; plural=(n==1 ? 0 : n==2 ? 1 : 2);",
|
|
199
|
+
"iu naq sat se sma smi smj smn sms":
|
|
200
|
+
"nplurals=3; plural=(n==1 ? 0 : n==2 ? 1 : 2);",
|
|
201
|
+
"shi": "nplurals=3; plural=(n==0 || n==1 ? 0 : n>=2 && n<=10 ? 1 : 2);",
|
|
202
|
+
"mo ro":
|
|
203
|
+
"nplurals=3; plural=(n==1 ? 0 : n==0 || (n!=1 && n%100>=1 && n%100<=19) ? 1 : 2);",
|
|
204
|
+
"bs hr sh sr":
|
|
205
|
+
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);",
|
|
206
|
+
"fr": "nplurals=3; plural=(n==0 || n==1 ? 0 : n!=0 && n%1000000==0 ? 1 : 2);",
|
|
207
|
+
"pt": "nplurals=3; plural=(n<=1 ? 0 : n!=0 && n%1000000==0 ? 1 : 2);",
|
|
208
|
+
"ca it lld pt_PT scn vec":
|
|
209
|
+
"nplurals=3; plural=(n==1 ? 0 : n!=0 && n%1000000==0 ? 1 : 2);",
|
|
210
|
+
"es": "nplurals=3; plural=(n==1 ? 0 : n!=0 && n%1000000==0 ? 1 : 2);",
|
|
211
|
+
"gd": "nplurals=4; plural=(n==1 || n==11 ? 0 : n==2 || n==12 ? 1 : (n>=3 && n<=10) || (n>=13 && n<=19) ? 2 : 3);",
|
|
212
|
+
"sl": "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n%100<=4 ? 2 : 3);",
|
|
213
|
+
"dsb hsb":
|
|
214
|
+
"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n%100<=4 ? 2 : 3);",
|
|
215
|
+
"cs sk": "nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 ? 1 : 2);",
|
|
216
|
+
"pl": "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);",
|
|
217
|
+
"be": "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);",
|
|
218
|
+
"lt": "nplurals=3; plural=(n%10==1 && (n%100<11 || n%100>19) ? 0 : n%10>=2 && n%10<=9 && (n%100<11 || n%100>19) ? 1 : 2);",
|
|
219
|
+
"ru uk":
|
|
220
|
+
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);",
|
|
221
|
+
"sgs": "nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n==2 ? 1 : n!=2 && n%10>=2 && n%10<=9 && (n%100<11 || n%100>19) ? 2 : 3);",
|
|
222
|
+
"br": "nplurals=5; plural=(n%10==1 && n%100!=11 && n%100!=71 && n%100!=91 ? 0 : n%10==2 && n%100!=12 && n%100!=72 && n%100!=92 ? 1 : ((n%10>=3 && n%10<=4) || n%10==9) && (n%100<10 || n%100>19) && (n%100<70 || n%100>79) && (n%100<90 || n%100>99) ? 2 : n!=0 && n%1000000==0 ? 3 : 4);",
|
|
223
|
+
"mt": "nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n==0 || (n%100>=3 && n%100<=10) ? 2 : n%100>=11 && n%100<=19 ? 3 : 4);",
|
|
224
|
+
"ga": "nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4);",
|
|
225
|
+
"gv": "nplurals=4; plural=(n%10==1 ? 0 : n%10==2 ? 1 : n%100==0 || n%100==20 || n%100==40 || n%100==60 || n%100==80 ? 2 : 3);",
|
|
226
|
+
"kw": "nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n%100==2 || n%100==22 || n%100==42 || n%100==62 || n%100==82 || (n%1000==0 && ((n%100000>=1000 && n%100000<=20000) || n%100000==40000 || n%100000==60000 || n%100000==80000)) || (n!=0 && n%1000000==100000) ? 2 : n%100==3 || n%100==23 || n%100==43 || n%100==63 || n%100==83 ? 3 : n!=1 && (n%100==1 || n%100==21 || n%100==41 || n%100==61 || n%100==81) ? 4 : 5);",
|
|
227
|
+
"ar ars":
|
|
228
|
+
"nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);",
|
|
229
|
+
"cy": "nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 5);",
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Lookup table mapping locales to plural forms.
|
|
234
|
+
* @param {string} locale - The locale to look up.
|
|
235
|
+
*/
|
|
236
|
+
export function localePluralForms(locale) {
|
|
237
|
+
// Try gettext's first
|
|
238
|
+
for (const [langs, value] of Object.entries(pluralTable)) {
|
|
239
|
+
if (langs.split(" ").includes(locale)) return value;
|
|
240
|
+
}
|
|
241
|
+
// Then try CLDR
|
|
242
|
+
for (const [langs, value] of Object.entries(cldrPlurals)) {
|
|
243
|
+
if (langs.split(" ").includes(locale)) return value;
|
|
244
|
+
}
|
|
245
|
+
}
|
package/lib/plurals.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { pluralFuncTable, localePluralForms } from "./plural-data.js";
|
|
2
|
+
|
|
3
|
+
// The function can only always return numbers or always return booleans
|
|
4
|
+
// regardless of the input.
|
|
5
|
+
/**
|
|
6
|
+
* @import { PluralFormsObj } from "./plural-data.js";
|
|
7
|
+
* @type PluralFormsObj
|
|
8
|
+
*/
|
|
9
|
+
const defaultPluralForms = {
|
|
10
|
+
nplurals: 2,
|
|
11
|
+
plural: (n) => n !== 1,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* "Parse" the plural forms.
|
|
16
|
+
*
|
|
17
|
+
* Ideally this would actually parse it instead of using a lookup table, except
|
|
18
|
+
* that would be too complicated and also questionably safe. So maybe using a
|
|
19
|
+
* lookup table is fine.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} pluralForms - The plural forms string from the header
|
|
22
|
+
* @param {boolean} trusted - Trust that the code in `pluralForms` is safe to run
|
|
23
|
+
*/
|
|
24
|
+
export function parsePluralForms(pluralForms, trusted = false) {
|
|
25
|
+
const normalized = pluralForms.replaceAll(/ |(?:;(?:\\n)?$)/g, "");
|
|
26
|
+
const match = normalized.match(/^nplurals=(\d);plural=(.*)/);
|
|
27
|
+
if (!match) {
|
|
28
|
+
console.log(
|
|
29
|
+
`Warning: matching pluralForms failed! Original value: ${pluralForms}`,
|
|
30
|
+
);
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const nplurals = parseInt(match[1]);
|
|
34
|
+
let pluralStr = match[2];
|
|
35
|
+
if (!pluralStr.match(/^\(.*\)$/)) {
|
|
36
|
+
pluralStr = "(" + pluralStr + ")";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const func = trusted
|
|
40
|
+
? /** @type import("./plural-data.js").PluralFunc */ (
|
|
41
|
+
new Function("n", `return ${pluralStr}`)
|
|
42
|
+
)
|
|
43
|
+
: pluralFuncTable[pluralStr];
|
|
44
|
+
if (func) {
|
|
45
|
+
return {
|
|
46
|
+
nplurals,
|
|
47
|
+
plural: func,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
console.log(`Warning: unknown pluralForms! "${pluralForms}"`);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Return the fallback plural forms for `locale`.
|
|
56
|
+
*
|
|
57
|
+
* If the locale with a region does not match, try without its region.
|
|
58
|
+
* @param {string} locale - The locale to get the fallback value for.
|
|
59
|
+
*/
|
|
60
|
+
export function fallbackPluralForms(locale) {
|
|
61
|
+
const pluralForms =
|
|
62
|
+
localePluralForms(locale) ||
|
|
63
|
+
// if not found, try with underscore. GNU Gettext usually prefers underscore
|
|
64
|
+
localePluralForms(locale.replace("-", "_")) ||
|
|
65
|
+
// if not found, try the top level code. The top level code can only
|
|
66
|
+
// be 2 ~ 3 characters.
|
|
67
|
+
localePluralForms(locale.slice(0, 3).replace(/[-_]/, ""));
|
|
68
|
+
if (pluralForms) {
|
|
69
|
+
const parsed = parsePluralForms(pluralForms, true);
|
|
70
|
+
if (parsed) return parsed;
|
|
71
|
+
}
|
|
72
|
+
console.log(
|
|
73
|
+
`Warning: no fallback plurals found for locale "${locale}". Using default plurals (Germanic).`,
|
|
74
|
+
);
|
|
75
|
+
return defaultPluralForms;
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kemdict/gettext",
|
|
3
|
+
"description": "A JavaScript implementation of gettext, a localization framework",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"//": "Messy setup for this fork. The original author Andris Reinman is the original author but not the one responsible for this fork.",
|
|
6
|
+
"author": "Kisaragi Hiu",
|
|
7
|
+
"contributors": ["Andris Reinman", "Kisaragi Hiu"],
|
|
8
|
+
"maintainers": ["Kisaragi Hiu <mail@kisaragi-hiu.com>"],
|
|
9
|
+
"homepage": "http://github.com/kemdict/gettext",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/kemdict/gettext.git"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"files": ["lib", "types", "README.md", "LICENSE", "package.json"],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./types/gettext.d.ts",
|
|
20
|
+
"default": "./lib/gettext.js"
|
|
21
|
+
},
|
|
22
|
+
"./loaders.js": {
|
|
23
|
+
"types": "./types/loaders.d.ts",
|
|
24
|
+
"default": "./lib/loaders.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/gettext-parser": "^8.0.0",
|
|
29
|
+
"@types/node": "^26.0.1",
|
|
30
|
+
"gettext-parser": "^9.0.0",
|
|
31
|
+
"prettier": "^3.7.4",
|
|
32
|
+
"typescript": "^6.0.3"
|
|
33
|
+
},
|
|
34
|
+
"engine": {
|
|
35
|
+
"node": ">=26"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"i18n",
|
|
39
|
+
"l10n",
|
|
40
|
+
"internationalization",
|
|
41
|
+
"localization",
|
|
42
|
+
"translation",
|
|
43
|
+
"gettext"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guess or lookup the preferred language list from environment variables.
|
|
3
|
+
* @param {Record<string, string | undefined>} env
|
|
4
|
+
* A map of environment variables. Defaults to process.env.
|
|
5
|
+
* @returns string[] | undefined
|
|
6
|
+
*/
|
|
7
|
+
export function guessEnvLocale(env?: Record<string, string | undefined>): string[] | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* @import { GetTextTranslations } from "gettext-parser";
|
|
10
|
+
* @typedef {string} Locale
|
|
11
|
+
* @typedef {{ eventName: string, callback: Function }} Listener
|
|
12
|
+
* @typedef {GetTextTranslations} Catalog
|
|
13
|
+
*/
|
|
14
|
+
export default class Gettext {
|
|
15
|
+
/**
|
|
16
|
+
* Creates and returns a new Gettext instance.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {Object} Options - a set of options
|
|
19
|
+
* @property {string} [sourceLocale] - The locale that the source code and its
|
|
20
|
+
* texts are written in. Translations for
|
|
21
|
+
* this locale is not necessary.
|
|
22
|
+
* @property {Record<Locale, Catalog>} [translations] - Translations to add to the catalog
|
|
23
|
+
* @property {boolean} [trusted] - Trust that the plural forms code in
|
|
24
|
+
* `translations` are safe to evaluate.
|
|
25
|
+
* @param {Options} [options]
|
|
26
|
+
*/
|
|
27
|
+
constructor(options?: {
|
|
28
|
+
/**
|
|
29
|
+
* - The locale that the source code and its
|
|
30
|
+
* texts are written in. Translations for
|
|
31
|
+
* this locale is not necessary.
|
|
32
|
+
*/
|
|
33
|
+
sourceLocale?: string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* - Translations to add to the catalog
|
|
36
|
+
*/
|
|
37
|
+
translations?: Record<string, GetTextTranslations> | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* - Trust that the plural forms code in
|
|
40
|
+
* `translations` are safe to evaluate.
|
|
41
|
+
*/
|
|
42
|
+
trusted?: boolean | undefined;
|
|
43
|
+
});
|
|
44
|
+
/** @type Map<Locale, Catalog> */
|
|
45
|
+
catalogs: Map<Locale, Catalog>;
|
|
46
|
+
/** @type Array<Listener> */
|
|
47
|
+
listeners: Array<Listener>;
|
|
48
|
+
trusted: boolean;
|
|
49
|
+
sourceLocale: string;
|
|
50
|
+
/**
|
|
51
|
+
* Return locales currently added to the catalogs.
|
|
52
|
+
*/
|
|
53
|
+
getLocales(): MapIterator<string>;
|
|
54
|
+
/**
|
|
55
|
+
* Return functions that translate strings into `locale`.
|
|
56
|
+
* This allows not having global state while also not having to pass the
|
|
57
|
+
* locale for every call.
|
|
58
|
+
*
|
|
59
|
+
* @param {Locale[] | Locale | undefined} locales
|
|
60
|
+
* A string to use as a locale, or an array of locales to try to match for,
|
|
61
|
+
* or undefined which means to not do any translations.
|
|
62
|
+
*/
|
|
63
|
+
bindLocale(locales: Locale[] | Locale | undefined): {
|
|
64
|
+
/**
|
|
65
|
+
* Translate a string.
|
|
66
|
+
* The locale is implicit.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} msgid - String to be translated
|
|
69
|
+
* @return {string} Translation or the original string if no translation was found
|
|
70
|
+
*/
|
|
71
|
+
gettext(msgid: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* Translate a string.
|
|
74
|
+
* Same as `gettext`.
|
|
75
|
+
* The locale is implicit.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} msgid - String to be translated
|
|
78
|
+
* @return {string} Translation or the original string if no translation was found
|
|
79
|
+
*/
|
|
80
|
+
_(msgid: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* Translate a plural string.
|
|
83
|
+
* The locale is implicit.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} msgid String to be translated when count is not plural
|
|
86
|
+
* @param {string} msgidPlural String to be translated when count is plural
|
|
87
|
+
* @param {number} count Number count for the plural
|
|
88
|
+
* @return {string} Translation or the original string if no translation was found
|
|
89
|
+
*/
|
|
90
|
+
ngettext(msgid: string, msgidPlural: string, count: number): string;
|
|
91
|
+
/**
|
|
92
|
+
* Translate a string from a specific context.
|
|
93
|
+
* The locale is implicit.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} msgctxt Translation context
|
|
96
|
+
* @param {string} msgid String to be translated
|
|
97
|
+
* @return {string} Translation or the original string if no translation was found
|
|
98
|
+
*/
|
|
99
|
+
pgettext(msgctxt: string, msgid: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* Translate a plural string from a specific context.
|
|
102
|
+
* The locale is implicit.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} msgctxt Translation context
|
|
105
|
+
* @param {string} msgid String to be translated when count is not plural
|
|
106
|
+
* @param {string} msgidPlural String to be translated when count is plural
|
|
107
|
+
* @param {number} count Number count for the plural
|
|
108
|
+
* @return {string} Translation or the original string if no translation was found
|
|
109
|
+
*/
|
|
110
|
+
npgettext(msgctxt: string, msgid: string, msgidPlural: string, count: number): string;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Cache for computed plural forms.
|
|
114
|
+
* Right now the computation is just normalizing the value then looking it
|
|
115
|
+
* up, but this already benefits from a cache.
|
|
116
|
+
* @type {Map<string, import("./plural-data.js").PluralFormsObj>}
|
|
117
|
+
*/
|
|
118
|
+
_pluralForms: Map<string, import("./plural-data.js").PluralFormsObj>;
|
|
119
|
+
/**
|
|
120
|
+
* Return plural forms header for the current catalogs for `locale`.
|
|
121
|
+
* @param {string} locale - The locale name
|
|
122
|
+
*/
|
|
123
|
+
_getCatalogPluralForms(locale: string): import("./plural-data.js").PluralFormsObj;
|
|
124
|
+
}
|
|
125
|
+
export type Locale = string;
|
|
126
|
+
export type Listener = {
|
|
127
|
+
eventName: string;
|
|
128
|
+
callback: Function;
|
|
129
|
+
};
|
|
130
|
+
export type Catalog = GetTextTranslations;
|
|
131
|
+
import type { GetTextTranslations } from "gettext-parser";
|
|
132
|
+
//# sourceMappingURL=gettext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gettext.d.ts","sourceRoot":"","sources":["../lib/gettext.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,qCAJW,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,wBAyB5C;AAED;;;;;GAKG;AAEH;IAMI;;;;;;;;;;;OAWG;IACH;;;;;;;;;;;;;;;;OAiBC;IAlCD,iCAAiC;IACjC,UADU,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CACT;IACrB,4BAA4B;IAC5B,WADU,KAAK,CAAC,QAAQ,CAAC,CACV;IACf,iBAAgB;IAiBZ,qBAAsB;IAc1B;;OAEG;IACH,kCAEC;IAOD;;;;;;;;OAQG;IACH,oBAJW,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS;QA6DhC;;;;;;WAMG;uBAFS,MAAM,GACN,MAAM;QAKlB;;;;;;;WAOG;iBAFS,MAAM,GACN,MAAM;QAKlB;;;;;;;;WAQG;wBAJS,MAAM,eACN,MAAM,SACN,MAAM,GACN,MAAM;QAKlB;;;;;;;WAOG;0BAHS,MAAM,SACN,MAAM,GACN,MAAM;QAKlB;;;;;;;;;WASG;2BALS,MAAM,SACN,MAAM,eACN,MAAM,SACN,MAAM,GACN,MAAM;MAMzB;IACD;;;;;OAKG;IACH,cAFU,GAAG,CAAC,MAAM,EAAE,OAAO,kBAAkB,EAAE,cAAc,CAAC,CAEvC;IAEzB;;;OAGG;IACH,+BAFW,MAAM,6CAUhB;CACJ;qBAvMY,MAAM;uBACN;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,WAAU;CAAE;sBACzC,mBAAmB;yCAHQ,gBAAgB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load MO translations the directories `localesDirs`.
|
|
3
|
+
*
|
|
4
|
+
* These directories should be arranged similar to /usr/share/locale, like
|
|
5
|
+
* <dir>/<locale>/LC_MESSAGES/<domain>.mo.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} domain
|
|
8
|
+
* @param {...string} localesDirs
|
|
9
|
+
*/
|
|
10
|
+
export function bindtextdomain(domain: string, ...localesDirs: string[]): Record<string, import("gettext-parser").GetTextTranslations>;
|
|
11
|
+
/**
|
|
12
|
+
* Load PO translations from `dir`.
|
|
13
|
+
* `dir` should be structured like <dir>/<locale>.po.
|
|
14
|
+
* @param {string} dir
|
|
15
|
+
*/
|
|
16
|
+
export function loadTranslations(dir: string): Record<string, import("gettext-parser").GetTextTranslations>;
|
|
17
|
+
//# sourceMappingURL=loaders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loaders.d.ts","sourceRoot":"","sources":["../lib/loaders.js"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,uCAHW,MAAM,kBACH,MAAM,EAAA,gEAqBnB;AAED;;;;GAIG;AACH,sCAFW,MAAM,gEAchB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lookup table mapping locales to plural forms.
|
|
3
|
+
* @param {string} locale - The locale to look up.
|
|
4
|
+
*/
|
|
5
|
+
export function localePluralForms(locale: string): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {((n: number) => number) | ((n: number) => boolean)} PluralFunc
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* nplurals: number;
|
|
10
|
+
* plural: PluralFunc
|
|
11
|
+
* }} PluralFormsObj */
|
|
12
|
+
/**
|
|
13
|
+
* Lookup table for existing "plural=" expressions to "parsed" function values.
|
|
14
|
+
* The keys are normalized to always start and end with parens to reduce (though
|
|
15
|
+
* not eliminate) duplicate keys.
|
|
16
|
+
* @type Record<string, PluralFunc>
|
|
17
|
+
*/
|
|
18
|
+
export const pluralFuncTable: Record<string, PluralFunc>;
|
|
19
|
+
export type PluralFunc = ((n: number) => number) | ((n: number) => boolean);
|
|
20
|
+
export type PluralFormsObj = {
|
|
21
|
+
nplurals: number;
|
|
22
|
+
plural: PluralFunc;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=plural-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plural-data.d.ts","sourceRoot":"","sources":["../lib/plural-data.js"],"names":[],"mappings":"AAuOA;;;GAGG;AACH,0CAFW,MAAM,sBAWhB;AA/OD;;;;;uBAKuB;AAEvB;;;;;GAKG;AACH,8BAFS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CA2IjC;yBArJY,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;6BAClD;IACR,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,CAAA;CACpB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Parse" the plural forms.
|
|
3
|
+
*
|
|
4
|
+
* Ideally this would actually parse it instead of using a lookup table, except
|
|
5
|
+
* that would be too complicated and also questionably safe. So maybe using a
|
|
6
|
+
* lookup table is fine.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} pluralForms - The plural forms string from the header
|
|
9
|
+
* @param {boolean} trusted - Trust that the code in `pluralForms` is safe to run
|
|
10
|
+
*/
|
|
11
|
+
export function parsePluralForms(pluralForms: string, trusted?: boolean): {
|
|
12
|
+
nplurals: number;
|
|
13
|
+
plural: import("./plural-data.js").PluralFunc;
|
|
14
|
+
} | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Return the fallback plural forms for `locale`.
|
|
17
|
+
*
|
|
18
|
+
* If the locale with a region does not match, try without its region.
|
|
19
|
+
* @param {string} locale - The locale to get the fallback value for.
|
|
20
|
+
*/
|
|
21
|
+
export function fallbackPluralForms(locale: string): PluralFormsObj;
|
|
22
|
+
import type { PluralFormsObj } from "./plural-data.js";
|
|
23
|
+
//# sourceMappingURL=plurals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plurals.d.ts","sourceRoot":"","sources":["../lib/plurals.js"],"names":[],"mappings":"AAaA;;;;;;;;;GASG;AACH,8CAHW,MAAM,YACN,OAAO;;;cA8BjB;AAED;;;;;GAKG;AACH,4CAFW,MAAM,kBAkBhB;oCAtEkC,kBAAkB"}
|