@discord/intl-loader-core 0.9.0-rc.1 → 0.9.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/package.json +3 -2
- package/src/database.js +9 -0
- package/src/processing.js +195 -0
- package/src/transformer.js +156 -0
- package/src/util.js +55 -0
- package/src/watcher.js +101 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discord/intl-loader-core",
|
|
3
|
-
"version": "0.9.0
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Core utilities for writing loaders and transformers using @discord/intl",
|
|
6
6
|
"author": "Jon Egeland",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"index.js",
|
|
11
11
|
"types.d.ts",
|
|
12
|
+
"src",
|
|
12
13
|
"types"
|
|
13
14
|
],
|
|
14
15
|
"exports": {
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
"chokidar": "^3.6.0",
|
|
26
27
|
"debug": "^4.3.6",
|
|
27
28
|
"fast-glob": "^3.3.2",
|
|
28
|
-
"@discord/intl-message-database": "0.9.0
|
|
29
|
+
"@discord/intl-message-database": "0.9.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/debug": "^4.1.12",
|
package/src/database.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const { IntlMessagesDatabase } = require('@discord/intl-message-database');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A shared message database instance that's used and shared across all parts
|
|
5
|
+
* of the loader and plugin together.
|
|
6
|
+
*/
|
|
7
|
+
const database = new IntlMessagesDatabase();
|
|
8
|
+
|
|
9
|
+
module.exports = { database };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
const debug = require('debug')('intl:loader-core');
|
|
4
|
+
const { IntlCompiledMessageFormat } = require('@discord/intl-message-database');
|
|
5
|
+
|
|
6
|
+
const { database } = require('./database');
|
|
7
|
+
const { findAllTranslationFiles, getLocaleFromTranslationsFileName } = require('./util');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* format?: IntlCompiledMessageFormat,
|
|
12
|
+
* bundleSecrets?: boolean,
|
|
13
|
+
* }} IntlPrecompileOptions
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} sourcePath
|
|
18
|
+
* @param {import('@discord/intl-message-database').IntlSourceFile} sourceFile
|
|
19
|
+
*/
|
|
20
|
+
function debugSourceFile(sourcePath, sourceFile) {
|
|
21
|
+
debug(
|
|
22
|
+
`[${sourcePath}] Parsed messages file: type=${sourceFile.type}, locale=${sourceFile.locale}, messageCount=${sourceFile.messageKeys.length}, meta=${JSON.stringify(sourceFile.meta)}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
*
|
|
28
|
+
* @param {string} sourcePath Path of the source file being processed, for debug logging
|
|
29
|
+
* @param {import('@discord/intl-message-database').IntlSourceFile} sourceFile SourceFile object from the database used to find translations.
|
|
30
|
+
* @param {string} translationsPath Fully-resolved path to the directory for translations.
|
|
31
|
+
* @returns {Record<string, string>}
|
|
32
|
+
*/
|
|
33
|
+
function buildTranslationsLocaleMap(sourcePath, sourceFile, translationsPath) {
|
|
34
|
+
if (sourceFile.meta.translate === false) {
|
|
35
|
+
debug(`[${sourcePath}] translate is set to false, no locale map is needed`);
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
const map = findAllTranslationFiles(translationsPath);
|
|
39
|
+
if (map instanceof Error) {
|
|
40
|
+
debug(`[${sourcePath}] Failed to build locale map: [${map.name}] ${map.message}`);
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} sourcePath
|
|
48
|
+
* @param {string=} sourceContent
|
|
49
|
+
* @param {{
|
|
50
|
+
* processTranslations?: boolean,
|
|
51
|
+
* locale?: string
|
|
52
|
+
* }=} options
|
|
53
|
+
* @returns {import('../types').ProcessDefinitionsResult}
|
|
54
|
+
*/
|
|
55
|
+
function processDefinitionsFile(sourcePath, sourceContent, options = {}) {
|
|
56
|
+
const {
|
|
57
|
+
processTranslations = false,
|
|
58
|
+
// TODO: Make this more configurable/automatically determined.
|
|
59
|
+
locale = 'en-US',
|
|
60
|
+
} = options;
|
|
61
|
+
debug(`[${sourcePath}] Processing definitions with locale "${locale}"`);
|
|
62
|
+
|
|
63
|
+
if (sourceContent != null) {
|
|
64
|
+
database.processDefinitionsFileContent(sourcePath, sourceContent);
|
|
65
|
+
} else {
|
|
66
|
+
database.processDefinitionsFile(sourcePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sourceFile = database.getSourceFile(sourcePath);
|
|
70
|
+
debugSourceFile(sourcePath, sourceFile);
|
|
71
|
+
if (sourceFile.type !== 'definition') {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Expected ${sourcePath} to be a message definitions file, but it resulted in ${sourceFile.type} instead.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const messageKeys = database.getSourceFileKeyMap(sourcePath);
|
|
78
|
+
const translationsPath = path.resolve(path.dirname(sourcePath), sourceFile.meta.translationsPath);
|
|
79
|
+
const translationsLocaleMap = buildTranslationsLocaleMap(
|
|
80
|
+
sourcePath,
|
|
81
|
+
sourceFile,
|
|
82
|
+
translationsPath,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (processTranslations) {
|
|
86
|
+
database.processAllTranslationFiles(translationsLocaleMap);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
sourceFile,
|
|
91
|
+
locale,
|
|
92
|
+
messageKeys,
|
|
93
|
+
translationsPath,
|
|
94
|
+
translationsLocaleMap,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
*
|
|
100
|
+
* @param {string} sourcePath
|
|
101
|
+
* @param {string=} sourceContent
|
|
102
|
+
* @param {{
|
|
103
|
+
* locale?: string,
|
|
104
|
+
* }=} options
|
|
105
|
+
* @returns {import('../types').ProcessTranslationsResult}
|
|
106
|
+
*/
|
|
107
|
+
function processTranslationsFile(sourcePath, sourceContent, options = {}) {
|
|
108
|
+
const { locale = getLocaleFromTranslationsFileName(sourcePath) } = options;
|
|
109
|
+
if (sourceContent) {
|
|
110
|
+
database.processTranslationFileContent(sourcePath, locale, sourceContent);
|
|
111
|
+
} else {
|
|
112
|
+
database.processTranslationFile(sourcePath, locale);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const sourceFile = database.getSourceFile(sourcePath);
|
|
116
|
+
debugSourceFile(sourcePath, sourceFile);
|
|
117
|
+
if (sourceFile.type !== 'translation') {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Expected ${sourcePath} to be a message translations file, but it resulted in ${sourceFile.type} instead.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
sourceFile,
|
|
125
|
+
locale,
|
|
126
|
+
messageKeys: database.getSourceFileKeyMap(sourcePath),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Precompile the messages defined in the given `sourcePath` using the value of the translation for
|
|
132
|
+
* that message in the given `locale`. `format` specifies which serialization format the result
|
|
133
|
+
* will be written in.
|
|
134
|
+
*
|
|
135
|
+
* By default, the compiled content will be returned as a Buffer containing the serialized string,
|
|
136
|
+
* but if `outputFile` is given then the content will be written directly to the file and the
|
|
137
|
+
* function becomes `void`.
|
|
138
|
+
*
|
|
139
|
+
* Compiling automatically handles filtering out messages based on the meta information like
|
|
140
|
+
* `translate`, `secret`, and `bundleSecrets`, to ensure that all consumers apply these values
|
|
141
|
+
* accurately and consistently.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} sourcePath
|
|
144
|
+
* @param {string} locale
|
|
145
|
+
* @param {string=} outputFile
|
|
146
|
+
* @param {IntlPrecompileOptions} [options]
|
|
147
|
+
*
|
|
148
|
+
* @returns {Buffer | void}
|
|
149
|
+
*/
|
|
150
|
+
function precompileFileForLocale(sourcePath, locale, outputFile, options = {}) {
|
|
151
|
+
return outputFile != null
|
|
152
|
+
? database.precompile(sourcePath, locale, outputFile, options)
|
|
153
|
+
: database.precompileToBuffer(sourcePath, locale, options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate a `.d.ts` file containing TypeScript type definitions for all of the messages defined in
|
|
158
|
+
* `sourcePath`. This method does not process `sourcePath` at all, meaning it expects the database
|
|
159
|
+
* to already know about the source, as well as all of the related translations to create an
|
|
160
|
+
* accurate typescript definition for each message.
|
|
161
|
+
*
|
|
162
|
+
* If not given, `outputFile` will default to the same path as `sourcePath`, with the last extension
|
|
163
|
+
* replaced by `.d.ts`. For example, a file like `SomeMessages.Other.messages.js` would become
|
|
164
|
+
* `SomeMessages.Other.messages.d.ts`.
|
|
165
|
+
*
|
|
166
|
+
* If `allowNullability` is set, the generated types for variables within messages will allow
|
|
167
|
+
* `null` and `undefined` for most value types, as well as looser restrictions on typing, such as
|
|
168
|
+
* allowing `string | number` for number variables.
|
|
169
|
+
*
|
|
170
|
+
* Returns `true` if the types were successfully generated, or `false` otherwise, such as if the
|
|
171
|
+
* source file is not already in the database.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} sourcePath
|
|
174
|
+
* @param {string=} outputFile
|
|
175
|
+
* @param {boolean=} allowNullability
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
function generateTypeDefinitions(sourcePath, outputFile, allowNullability = false) {
|
|
179
|
+
const paths = database.getAllSourceFilePaths();
|
|
180
|
+
if (!paths.includes(sourcePath)) return false;
|
|
181
|
+
|
|
182
|
+
database.generateTypes(
|
|
183
|
+
sourcePath,
|
|
184
|
+
outputFile ?? sourcePath.replace(/\.[^.]+$/, '.d.ts'),
|
|
185
|
+
allowNullability,
|
|
186
|
+
);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
generateTypeDefinitions,
|
|
192
|
+
precompileFileForLocale,
|
|
193
|
+
processDefinitionsFile,
|
|
194
|
+
processTranslationsFile,
|
|
195
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common class for parsing and transforming the content of a messages
|
|
3
|
+
* definition file (e.g., "SomeFeature.messages.js") into a production-ready
|
|
4
|
+
* version, with message keys obfuscated, loading harnesses configured, and
|
|
5
|
+
* more.
|
|
6
|
+
*
|
|
7
|
+
* This transformation is intended to be used alongside the _consumer_
|
|
8
|
+
* transforms implemented as SWC and Babel plugins, which transform the
|
|
9
|
+
* callsites for messages into matching formats. Consider this input example:
|
|
10
|
+
*
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // SomeModule.messages.js
|
|
13
|
+
* import {defineMessages} from '@discord/intl';
|
|
14
|
+
*
|
|
15
|
+
* export default defineMessages({
|
|
16
|
+
* THIS_IS_A_MESSAGE: 'it has some content with {values}',
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // SomeConsumer.tsx
|
|
20
|
+
* import someModuleMessages from 'SomeModule.messages.js';
|
|
21
|
+
* i18n.format(someModuleMessages.THIS_IS_A_MESSAGE, {values: "I'm a value!"});
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* This transformer will only handle `SomeModule.messages.js`, and will output
|
|
25
|
+
* something like:
|
|
26
|
+
*
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const {i18n} = require('@discord/intl');
|
|
29
|
+
* const _keys = ["a9fn23"];
|
|
30
|
+
* const _locales = {"en-US": () => require('./messages/en-US.messages.json')};
|
|
31
|
+
* export const messagesLoader = createLoader(_keys, _locales);
|
|
32
|
+
* export default messagesLoader.getBinds();
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Notice how the message keys have been hashed into short keys, and the
|
|
36
|
+
* dynamic imports for each locale's data have been inserted automatically. In
|
|
37
|
+
* this example, the only locale is the source locale.
|
|
38
|
+
*
|
|
39
|
+
* The SWC and Babel transformers then take care of the second file,
|
|
40
|
+
* transforming the usage to use the hashed keys:
|
|
41
|
+
*
|
|
42
|
+
* ```typescript
|
|
43
|
+
* import someModuleMessages from 'SomeModule.messages.js';
|
|
44
|
+
* i18n.format(someModuleMessages["a9fn23"], {values: "i'm a value!"});
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* The transformed file also contains a named export for `messagesLoader`,
|
|
48
|
+
* which consumers can use to query and update the loading state for the
|
|
49
|
+
* messages managed by that loader, including waiting for a locale to be
|
|
50
|
+
* loaded, kicking off new loads, and more:
|
|
51
|
+
*
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import {messagesLoader} from 'SomeModule.messages.js';
|
|
54
|
+
* // Wait for the loader to be initialized with default messages
|
|
55
|
+
* await messagesLoader.waitForDefaultLocaleLoaded();
|
|
56
|
+
* // Wait for a specific locale to load, starting the load if it
|
|
57
|
+
* // is not yet in progress.
|
|
58
|
+
* await messagesLoader.waitForLocaleLoaded('fr');
|
|
59
|
+
* // In hot-reloading environments, use the second `requireCurrent`
|
|
60
|
+
* // parameter to wait for the latest data, even if a value already
|
|
61
|
+
* // exists.
|
|
62
|
+
* const loaded = messagesLoader.isLocaleLoaded('fr', true);
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
class MessageDefinitionsTransformer {
|
|
66
|
+
/**
|
|
67
|
+
* @param {import('../types.d.ts').MessageDefinitionsTransformerOptions} options
|
|
68
|
+
*/
|
|
69
|
+
constructor(options) {
|
|
70
|
+
this.options = options;
|
|
71
|
+
this.loaderName = 'messagesLoader';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns a compiled string for an object that maps locale names to a
|
|
76
|
+
* dynamic require function for that locale, based on the supported locales
|
|
77
|
+
* that were determined for this file. The shape ends up as:
|
|
78
|
+
*
|
|
79
|
+
* ```typescript
|
|
80
|
+
* {
|
|
81
|
+
* "en-US": () => import("path/to/en-US.json"),
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
getLocaleRequireMap() {
|
|
88
|
+
const localeProperties = [];
|
|
89
|
+
for (const [locale, importPath] of Object.entries(this.options.localeMap)) {
|
|
90
|
+
// This assumes that the author has specified `importPath`
|
|
91
|
+
// as a properly-resolvable path for the bundler, which we can't easily
|
|
92
|
+
// enforce, unfortunately.
|
|
93
|
+
localeProperties.push(`"${locale}": () => ${this.options.getTranslationImport(importPath)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return `{${localeProperties.join(',')}}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Return a map of key hashes to their original values, as well as a plain-text map of locales
|
|
101
|
+
* to the file names that they import from.
|
|
102
|
+
*
|
|
103
|
+
* @returns {string[]}
|
|
104
|
+
*/
|
|
105
|
+
debugModeSetup() {
|
|
106
|
+
if (!this.options.debug) return [];
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
`${this.loaderName}.withDebugValues(${JSON.stringify(this.options.messageKeys)}, ${JSON.stringify(this.options.localeMap)})`,
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Return the lines to export fields from this module, as determined by the `exportMode` on this
|
|
115
|
+
* transformer.
|
|
116
|
+
*/
|
|
117
|
+
exportFields() {
|
|
118
|
+
switch (this.options.exportMode ?? 'esm') {
|
|
119
|
+
case 'esm':
|
|
120
|
+
return [`export {${this.loaderName}};`, `export default binds;`];
|
|
121
|
+
case 'commonjs':
|
|
122
|
+
return [`module.exports = { messagesLoader: ${this.loaderName}, default: binds };`];
|
|
123
|
+
case 'transpiledEsModule':
|
|
124
|
+
return [
|
|
125
|
+
`Object.defineProperty(exports, "__esModule", { value: true });`,
|
|
126
|
+
`exports["messageLoader"] = ${this.loaderName};`,
|
|
127
|
+
`exports["default"] = binds;`,
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns the reduced, transformed output for this file. Currently not
|
|
134
|
+
* configurable, but could be told to include default messages or preserve
|
|
135
|
+
* information as necessary.
|
|
136
|
+
*
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
getOutput() {
|
|
140
|
+
return [
|
|
141
|
+
this.options.getPrelude?.() ?? '// No additional prelude was configured.',
|
|
142
|
+
`const {createLoader} = require('@discord/intl');`,
|
|
143
|
+
`const _keys = ${JSON.stringify(this.options.messageKeys)};`,
|
|
144
|
+
`const _locales = ${this.getLocaleRequireMap()};`,
|
|
145
|
+
`const _defaultLocale = ${JSON.stringify(this.options.defaultLocale)};`,
|
|
146
|
+
`const ${this.loaderName} = createLoader(_keys, _locales, _defaultLocale);`,
|
|
147
|
+
...this.debugModeSetup(),
|
|
148
|
+
`const binds = ${this.loaderName}.getBinds();`,
|
|
149
|
+
...this.exportFields(),
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
MessageDefinitionsTransformer,
|
|
156
|
+
};
|
package/src/util.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const { isMessageTranslationsFile } = require('@discord/intl-message-database');
|
|
4
|
+
|
|
5
|
+
const IGNORED_MESSAGE_FILE_PATTERNS = [/.*\.compiled.messages\..*/];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Return the presumed locale for a translations file from it's name. The convention follows the
|
|
9
|
+
* format: `some/path/to/<locale>.messages.jsona`, so the locale is determined by taking the content
|
|
10
|
+
* of the basename up until the first `.`.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} fileName
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function getLocaleFromTranslationsFileName(fileName) {
|
|
16
|
+
return path.basename(fileName).split('.')[0];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Scan the given `translationsPath` to discover all translation files that exist, returning them
|
|
21
|
+
* as a map from locale name to the path for importing.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} translationsPath
|
|
24
|
+
* @returns {Record<string, string> | Error}
|
|
25
|
+
*/
|
|
26
|
+
function findAllTranslationFiles(translationsPath) {
|
|
27
|
+
/** @type {Record<string, string>} */
|
|
28
|
+
const localeMap = {};
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const translationFiles = fs.readdirSync(translationsPath, { encoding: 'utf-8' });
|
|
32
|
+
for (const foundFile of translationFiles) {
|
|
33
|
+
const filePath = path.join(translationsPath, foundFile);
|
|
34
|
+
// Only include translation files, not definitions files.
|
|
35
|
+
if (!isMessageTranslationsFile(filePath)) continue;
|
|
36
|
+
// Some files are excluded, like pre-compiled artifacts.
|
|
37
|
+
if (IGNORED_MESSAGE_FILE_PATTERNS.some((pattern) => pattern.test(filePath))) continue;
|
|
38
|
+
|
|
39
|
+
const locale = getLocaleFromTranslationsFileName(filePath);
|
|
40
|
+
localeMap[locale] = filePath;
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return new Error(
|
|
44
|
+
`The translations directory ${translationsPath} was not found. No translations will be loaded for these messages`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return localeMap;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
IGNORED_MESSAGE_FILE_PATTERNS,
|
|
53
|
+
findAllTranslationFiles,
|
|
54
|
+
getLocaleFromTranslationsFileName,
|
|
55
|
+
};
|
package/src/watcher.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
const chokidar = require('chokidar');
|
|
4
|
+
const fg = require('fast-glob');
|
|
5
|
+
const debug = require('debug')('intl:loader-core:watcher');
|
|
6
|
+
const {
|
|
7
|
+
isMessageDefinitionsFile,
|
|
8
|
+
IntlCompiledMessageFormat,
|
|
9
|
+
} = require('@discord/intl-message-database');
|
|
10
|
+
|
|
11
|
+
const { database } = require('./database');
|
|
12
|
+
const { processDefinitionsFile, precompileFileForLocale } = require('./processing');
|
|
13
|
+
|
|
14
|
+
const ALWAYS_IGNORE_PATTERNS = [
|
|
15
|
+
// Ignore our own compiled message files, even though they shouldn't have a matching extension.
|
|
16
|
+
'*.compiled.messages.*',
|
|
17
|
+
];
|
|
18
|
+
// TODO: This should come from the database extension? Or Utilities? Unsure, but the extensions
|
|
19
|
+
// should have some centralized location in general
|
|
20
|
+
const MESSAGE_DEFINITION_FILE_PATTERNS = ['**/*.messages.js'];
|
|
21
|
+
const DEFAULT_LOCALE = 'en-US';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} filePath
|
|
25
|
+
* @param {string} assetExtension
|
|
26
|
+
* @param {import('./processing').IntlPrecompileOptions} [options]
|
|
27
|
+
*/
|
|
28
|
+
function processFile(filePath, assetExtension, options = {}) {
|
|
29
|
+
const { format = IntlCompiledMessageFormat.KeylessJson, bundleSecrets = false } = options;
|
|
30
|
+
debug(`Processing file: ${filePath}`);
|
|
31
|
+
if (!isMessageDefinitionsFile(filePath)) {
|
|
32
|
+
debug(`${filePath} is not a definitions file. Skipping processing`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Convert the file name from `.messages.js` to `.compiled.messages.jsona` for output.
|
|
38
|
+
const outputPath = filePath.replace(/\.messages\.js$/, `.compiled.messages.${assetExtension}`);
|
|
39
|
+
const result = processDefinitionsFile(filePath);
|
|
40
|
+
precompileFileForLocale(filePath, result.locale, undefined, {
|
|
41
|
+
format,
|
|
42
|
+
bundleSecrets,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
database.processDefinitionsFile(filePath);
|
|
46
|
+
database.precompile(filePath, DEFAULT_LOCALE, outputPath, {
|
|
47
|
+
format: IntlCompiledMessageFormat.KeylessJson,
|
|
48
|
+
bundleSecrets,
|
|
49
|
+
});
|
|
50
|
+
debug(`Wrote definitions to: ${outputPath}`);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
debug('[INTL Error] Failed to compile messages');
|
|
53
|
+
console.error(e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string[]} watchedFolders
|
|
59
|
+
* @param {{
|
|
60
|
+
* watch?: boolean,
|
|
61
|
+
* ignore?: string[],
|
|
62
|
+
* assetExtension?: string
|
|
63
|
+
* precompileOptions?: import('./processing').IntlPrecompileOptions,
|
|
64
|
+
* }} options
|
|
65
|
+
*/
|
|
66
|
+
async function compileIntlMessageFiles(
|
|
67
|
+
watchedFolders,
|
|
68
|
+
{ watch = true, ignore = [], assetExtension = 'json', precompileOptions = {} } = {},
|
|
69
|
+
) {
|
|
70
|
+
const ignoredPatterns = ignore.concat(ALWAYS_IGNORE_PATTERNS);
|
|
71
|
+
const globs = watchedFolders.flatMap((folder) =>
|
|
72
|
+
MESSAGE_DEFINITION_FILE_PATTERNS.map((pattern) => path.join(folder, pattern)),
|
|
73
|
+
);
|
|
74
|
+
debug(`Configured message file patterns:\n- ${globs.join('\n- ')}`);
|
|
75
|
+
|
|
76
|
+
// Perform one initial scan and compilation to ensure all files exist before Metro might try to
|
|
77
|
+
// resolve them.
|
|
78
|
+
debug('Scanning for initial messages files');
|
|
79
|
+
for await (const filePath of fg.stream(globs, {
|
|
80
|
+
ignore: ignoredPatterns,
|
|
81
|
+
absolute: true,
|
|
82
|
+
onlyFiles: true,
|
|
83
|
+
})) {
|
|
84
|
+
processFile(filePath.toString(), assetExtension, precompileOptions);
|
|
85
|
+
}
|
|
86
|
+
debug('Initial message scan completed.');
|
|
87
|
+
|
|
88
|
+
if (watch) {
|
|
89
|
+
debug(`Setting up file watching for configured paths`);
|
|
90
|
+
chokidar
|
|
91
|
+
.watch(globs, { ignored: ignoredPatterns, ignoreInitial: true })
|
|
92
|
+
.on('all', (event, filePath) => {
|
|
93
|
+
debug(`Got event ${event} for ${filePath}`);
|
|
94
|
+
processFile(filePath, assetExtension, precompileOptions);
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
debug('Not watching files because `watch` option was false');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { compileIntlMessageFiles, ALWAYS_IGNORE_PATTERNS };
|