@discord/intl-loader-core 0.8.0 → 0.9.0-rc.1

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 CHANGED
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "name": "@discord/intl-loader-core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0-rc.1",
4
4
  "license": "MIT",
5
5
  "description": "Core utilities for writing loaders and transformers using @discord/intl",
6
6
  "author": "Jon Egeland",
7
7
  "main": "index.js",
8
- "types": "index.d.ts",
8
+ "types": "types/index.d.ts",
9
9
  "files": [
10
10
  "index.js",
11
- "index.d.ts",
12
- "src"
11
+ "types.d.ts",
12
+ "types"
13
13
  ],
14
14
  "exports": {
15
- ".": "./index.js"
15
+ ".": {
16
+ "default": "./index.js",
17
+ "types": "./types/index.d.ts"
18
+ }
16
19
  },
17
20
  "repository": {
18
21
  "type": "git",
@@ -22,9 +25,14 @@
22
25
  "chokidar": "^3.6.0",
23
26
  "debug": "^4.3.6",
24
27
  "fast-glob": "^3.3.2",
25
- "@discord/intl-message-database": "0.8.0"
28
+ "@discord/intl-message-database": "0.9.0-rc.1"
26
29
  },
27
30
  "devDependencies": {
28
- "@types/debug": "^4.1.12"
31
+ "@types/debug": "^4.1.12",
32
+ "typescript": "*"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc && cp types.d.ts types/",
36
+ "build:release": "tsc && cp types.d.ts types/"
29
37
  }
30
38
  }
@@ -0,0 +1,14 @@
1
+ import { IntlCompiledMessageFormat } from "@discord/intl-message-database";
2
+ import { MessageDefinitionsTransformer } from "./src/transformer";
3
+ import { database } from "./src/database";
4
+ import { findAllTranslationFiles } from "./src/util";
5
+ import { getLocaleFromTranslationsFileName } from "./src/util";
6
+ import { generateTypeDefinitions } from "./src/processing";
7
+ import { hashMessageKey } from "@discord/intl-message-database";
8
+ import { isMessageDefinitionsFile } from "@discord/intl-message-database";
9
+ import { isMessageTranslationsFile } from "@discord/intl-message-database";
10
+ import { processDefinitionsFile } from "./src/processing";
11
+ import { processTranslationsFile } from "./src/processing";
12
+ import { precompileFileForLocale } from "./src/processing";
13
+ import watcher = require("./src/watcher");
14
+ export { IntlCompiledMessageFormat, MessageDefinitionsTransformer, database, findAllTranslationFiles, getLocaleFromTranslationsFileName, generateTypeDefinitions, hashMessageKey, isMessageDefinitionsFile, isMessageTranslationsFile, processDefinitionsFile, processTranslationsFile, precompileFileForLocale, watcher };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A shared message database instance that's used and shared across all parts
3
+ * of the loader and plugin together.
4
+ */
5
+ export const database: IntlMessagesDatabase;
6
+ import { IntlMessagesDatabase } from "@discord/intl-message-database";
@@ -0,0 +1,74 @@
1
+ export type IntlPrecompileOptions = {
2
+ format?: IntlCompiledMessageFormat;
3
+ bundleSecrets?: boolean;
4
+ };
5
+ /**
6
+ * Generate a `.d.ts` file containing TypeScript type definitions for all of the messages defined in
7
+ * `sourcePath`. This method does not process `sourcePath` at all, meaning it expects the database
8
+ * to already know about the source, as well as all of the related translations to create an
9
+ * accurate typescript definition for each message.
10
+ *
11
+ * If not given, `outputFile` will default to the same path as `sourcePath`, with the last extension
12
+ * replaced by `.d.ts`. For example, a file like `SomeMessages.Other.messages.js` would become
13
+ * `SomeMessages.Other.messages.d.ts`.
14
+ *
15
+ * If `allowNullability` is set, the generated types for variables within messages will allow
16
+ * `null` and `undefined` for most value types, as well as looser restrictions on typing, such as
17
+ * allowing `string | number` for number variables.
18
+ *
19
+ * Returns `true` if the types were successfully generated, or `false` otherwise, such as if the
20
+ * source file is not already in the database.
21
+ *
22
+ * @param {string} sourcePath
23
+ * @param {string=} outputFile
24
+ * @param {boolean=} allowNullability
25
+ * @returns {boolean}
26
+ */
27
+ export function generateTypeDefinitions(sourcePath: string, outputFile?: string | undefined, allowNullability?: boolean | undefined): boolean;
28
+ /**
29
+ * Precompile the messages defined in the given `sourcePath` using the value of the translation for
30
+ * that message in the given `locale`. `format` specifies which serialization format the result
31
+ * will be written in.
32
+ *
33
+ * By default, the compiled content will be returned as a Buffer containing the serialized string,
34
+ * but if `outputFile` is given then the content will be written directly to the file and the
35
+ * function becomes `void`.
36
+ *
37
+ * Compiling automatically handles filtering out messages based on the meta information like
38
+ * `translate`, `secret`, and `bundleSecrets`, to ensure that all consumers apply these values
39
+ * accurately and consistently.
40
+ *
41
+ * @param {string} sourcePath
42
+ * @param {string} locale
43
+ * @param {string=} outputFile
44
+ * @param {IntlPrecompileOptions} [options]
45
+ *
46
+ * @returns {Buffer | void}
47
+ */
48
+ export function precompileFileForLocale(sourcePath: string, locale: string, outputFile?: string | undefined, options?: IntlPrecompileOptions | undefined): Buffer | void;
49
+ /**
50
+ * @param {string} sourcePath
51
+ * @param {string=} sourceContent
52
+ * @param {{
53
+ * processTranslations?: boolean,
54
+ * locale?: string
55
+ * }=} options
56
+ * @returns {import('../types').ProcessDefinitionsResult}
57
+ */
58
+ export function processDefinitionsFile(sourcePath: string, sourceContent?: string | undefined, options?: {
59
+ processTranslations?: boolean;
60
+ locale?: string;
61
+ } | undefined): import("../types").ProcessDefinitionsResult;
62
+ /**
63
+ *
64
+ * @param {string} sourcePath
65
+ * @param {string=} sourceContent
66
+ * @param {{
67
+ * locale?: string,
68
+ * }=} options
69
+ * @returns {import('../types').ProcessTranslationsResult}
70
+ */
71
+ export function processTranslationsFile(sourcePath: string, sourceContent?: string | undefined, options?: {
72
+ locale?: string;
73
+ } | undefined): import("../types").ProcessTranslationsResult;
74
+ import { IntlCompiledMessageFormat } from "@discord/intl-message-database";
@@ -0,0 +1,106 @@
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
+ export class MessageDefinitionsTransformer {
66
+ /**
67
+ * @param {import('../types.d.ts').MessageDefinitionsTransformerOptions} options
68
+ */
69
+ constructor(options: import("../types.d.ts").MessageDefinitionsTransformerOptions);
70
+ options: import("../types.d.ts").MessageDefinitionsTransformerOptions;
71
+ loaderName: string;
72
+ /**
73
+ * Returns a compiled string for an object that maps locale names to a
74
+ * dynamic require function for that locale, based on the supported locales
75
+ * that were determined for this file. The shape ends up as:
76
+ *
77
+ * ```typescript
78
+ * {
79
+ * "en-US": () => import("path/to/en-US.json"),
80
+ * }
81
+ * ```
82
+ *
83
+ * @returns {string}
84
+ */
85
+ getLocaleRequireMap(): string;
86
+ /**
87
+ * Return a map of key hashes to their original values, as well as a plain-text map of locales
88
+ * to the file names that they import from.
89
+ *
90
+ * @returns {string[]}
91
+ */
92
+ debugModeSetup(): string[];
93
+ /**
94
+ * Return the lines to export fields from this module, as determined by the `exportMode` on this
95
+ * transformer.
96
+ */
97
+ exportFields(): string[];
98
+ /**
99
+ * Returns the reduced, transformed output for this file. Currently not
100
+ * configurable, but could be told to include default messages or preserve
101
+ * information as necessary.
102
+ *
103
+ * @returns {string}
104
+ */
105
+ getOutput(): string;
106
+ }
@@ -0,0 +1,18 @@
1
+ export const IGNORED_MESSAGE_FILE_PATTERNS: RegExp[];
2
+ /**
3
+ * Scan the given `translationsPath` to discover all translation files that exist, returning them
4
+ * as a map from locale name to the path for importing.
5
+ *
6
+ * @param {string} translationsPath
7
+ * @returns {Record<string, string> | Error}
8
+ */
9
+ export function findAllTranslationFiles(translationsPath: string): Record<string, string> | Error;
10
+ /**
11
+ * Return the presumed locale for a translations file from it's name. The convention follows the
12
+ * format: `some/path/to/<locale>.messages.jsona`, so the locale is determined by taking the content
13
+ * of the basename up until the first `.`.
14
+ *
15
+ * @param {string} fileName
16
+ * @returns {string}
17
+ */
18
+ export function getLocaleFromTranslationsFileName(fileName: string): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @param {string[]} watchedFolders
3
+ * @param {{
4
+ * watch?: boolean,
5
+ * ignore?: string[],
6
+ * assetExtension?: string
7
+ * precompileOptions?: import('./processing').IntlPrecompileOptions,
8
+ * }} options
9
+ */
10
+ export function compileIntlMessageFiles(watchedFolders: string[], { watch, ignore, assetExtension, precompileOptions }?: {
11
+ watch?: boolean;
12
+ ignore?: string[];
13
+ assetExtension?: string;
14
+ precompileOptions?: import("./processing").IntlPrecompileOptions;
15
+ }): Promise<void>;
16
+ export const ALWAYS_IGNORE_PATTERNS: string[];
@@ -0,0 +1,111 @@
1
+ import { IntlSourceFile } from '@discord/intl-message-database';
2
+
3
+ export interface MessageDefinitionsTransformerOptions {
4
+ /**
5
+ * The map of message keys that this file manages to their original values. By default, only the
6
+ * keys of this map are used (the hashed names), but in debug mode the values will also be
7
+ * included in the transformed file to provide context in errors and warnings.
8
+ */
9
+ messageKeys: Record<string, string>;
10
+ /**
11
+ * Map of locale names to import paths used for loading translations.
12
+ */
13
+ localeMap: Record<string, string>;
14
+ /**
15
+ * Default locale to use for the runtime loader. This is almost always the locale of the source
16
+ * file being transformed, but can be set explicitly to something else for special cases.
17
+ */
18
+ defaultLocale: string;
19
+ /**
20
+ * Function to create a prelude that gets injected at the start of the transformed file to set up
21
+ * anything needed for other injections later on.
22
+ */
23
+ getPrelude?: () => string;
24
+ /**
25
+ * Function to generate an import/require statement for the compiled asset file. All imports
26
+ * should be asynchronous (e.g., typically use `import` rather than `require`), but some platforms
27
+ * implement loading differently and may need different syntax. For example, React Native Assets
28
+ * are bundled using `require` statements to return an Asset ID, which can then be loaded
29
+ * asynchronously by some other code to get the actual content of the asset.
30
+ *
31
+ * The code created by this function must create a `Promise<{default: Record<string, any>}>`. In
32
+ * other words, a Promise for an object with a `default` key pointing to an object map of message
33
+ * keys to their values. For `import` statements, this is already the default. For `requires`, you
34
+ * may need to wrap the result with the `default` key, like `.then((data) => ({default: data}))`.
35
+ */
36
+ getTranslationImport(importPath: string): string;
37
+
38
+ /**
39
+ * Whether to include additional information about keys and source files in the transformed loader
40
+ * code to provide context for debugging in errors and warning messages.
41
+ */
42
+ debug?: boolean;
43
+ /**
44
+ * Control how value exports are written in the transformed file to match what any downstream
45
+ * transformer or interpreter may expect. `esm` will leave exports as `export default` and other
46
+ * module features supported in ES6 and onward. `commonjs` will explicitly use `module.exports`
47
+ * with a single object containing `default` as the key for a default export. `transpiledEsModule`
48
+ * does the same, but includes the compatibility field `__esModule` to indicate that the module
49
+ * "was transpiled" to this syntax.
50
+ *
51
+ * @default esm
52
+ */
53
+ exportMode?: 'esm' | 'commonjs' | 'transpiledEsModule';
54
+ }
55
+
56
+ /**
57
+ * The result of calling `processDefinitionsFile`, including the created source file, locale map,
58
+ * and more.
59
+ */
60
+ export interface ProcessDefinitionsResult {
61
+ /**
62
+ * Direct source file from the database that was created or updated by this process.
63
+ */
64
+ sourceFile: IntlSourceFile;
65
+ /**
66
+ * The locale that was either determined from the sourceFile name or overridden by the options
67
+ * provided to this call.
68
+ */
69
+ locale: string;
70
+ /**
71
+ * The full map of message keys contained by the processed source file to their original values.
72
+ * While `sourceFile` contains a list of key _symbols_, this list contains all of the
73
+ * resolved strings for the hashed message keys.
74
+ */
75
+ messageKeys: Record<string, string>;
76
+ /**
77
+ * Fully-resolved path to the translations directory that was scanned for entries for the
78
+ * source file.
79
+ */
80
+ translationsPath: string;
81
+ /**
82
+ * Map of locale names to file paths for all translations files that were discovered when scanning
83
+ * the configured `translationsPath`. Note that this _does not_ include the source locale, since
84
+ * it's target is often different between loaders (e.g., could be a virtual file, an asset that
85
+ * gets compiled separately, or use query parameters to control loader behavior when reusing the
86
+ * same file).
87
+ */
88
+ translationsLocaleMap: Record<string, string>;
89
+ }
90
+
91
+ /**
92
+ * The result of calling `processTranslationsFile`, including the created source file, locale map,
93
+ * and more.
94
+ */
95
+ export interface ProcessTranslationsResult {
96
+ /**
97
+ * Direct source file from the database that was created or updated by this process.
98
+ */
99
+ sourceFile: IntlSourceFile;
100
+ /**
101
+ * The full map of message keys contained by the processed source file to their original values.
102
+ * While `sourceFile` contains a list of key _symbols_, this list contains all of the
103
+ * resolved strings for the hashed message keys.
104
+ */
105
+ messageKeys: Record<string, string>;
106
+ /**
107
+ * The locale that was either determined from the sourceFile name or overridden by the options
108
+ * provided to this call.
109
+ */
110
+ locale: string;
111
+ }
package/index.d.ts DELETED
@@ -1,17 +0,0 @@
1
- export {
2
- hashMessageKey,
3
- isMessageDefinitionsFile,
4
- isMessageTranslationsFile,
5
- IntlCompiledMessageFormat,
6
- } from '@discord/intl-message-database';
7
-
8
- export { database } from './src/database';
9
- export {
10
- generateTypeDefinitions,
11
- processDefinitionsFile,
12
- processTranslationsFile,
13
- precompileFileForLocale,
14
- } from './src/processing';
15
- export { MessageDefinitionsTransformer } from './src/transformer';
16
- export { findAllTranslationFiles, getLocaleFromTranslationsFileName } from './src/util';
17
- export * as watcher from './src/watcher';
package/src/database.js DELETED
@@ -1,9 +0,0 @@
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};
package/src/processing.js DELETED
@@ -1,187 +0,0 @@
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
- * @param {string} sourcePath
11
- * @param {import('@discord/intl-message-database').IntlSourceFile} sourceFile
12
- */
13
- function debugSourceFile(sourcePath, sourceFile) {
14
- debug(
15
- `[${sourcePath}] Parsed messages file: type=${sourceFile.type}, locale=${sourceFile.locale}, messageCount=${sourceFile.messageKeys.length}, meta=${JSON.stringify(sourceFile.meta)}`,
16
- );
17
- }
18
-
19
- /**
20
- *
21
- * @param {string} sourcePath Path of the source file being processed, for debug logging
22
- * @param {import('@discord/intl-message-database').IntlSourceFile} sourceFile SourceFile object from the database used to find translations.
23
- * @param {string} translationsPath Fully-resolved path to the directory for translations.
24
- * @returns {Record<string, string>}
25
- */
26
- function buildTranslationsLocaleMap(sourcePath, sourceFile, translationsPath) {
27
- if (sourceFile.meta.translate === false) {
28
- debug(`[${sourcePath}] translate is set to false, no locale map is needed`);
29
- return {};
30
- }
31
- const map = findAllTranslationFiles(translationsPath);
32
- if (map instanceof Error) {
33
- debug(`[${sourcePath}] Failed to build locale map: [${map.name}] ${map.message}`);
34
- return {};
35
- }
36
- return map;
37
- }
38
-
39
- /**
40
- * @param {string} sourcePath
41
- * @param {string=} sourceContent
42
- * @param {{
43
- * processTranslations?: boolean,
44
- * locale?: string
45
- * }=} options
46
- * @returns {import('./types').ProcessDefinitionsResult}
47
- */
48
- function processDefinitionsFile(sourcePath, sourceContent, options = {}) {
49
- const {
50
- processTranslations = false,
51
- // TODO: Make this more configurable/automatically determined.
52
- locale = 'en-US',
53
- } = options;
54
- debug(`[${sourcePath}] Processing definitions with locale "${locale}"`);
55
-
56
- if (sourceContent != null) {
57
- database.processDefinitionsFileContent(sourcePath, sourceContent);
58
- } else {
59
- database.processDefinitionsFile(sourcePath);
60
- }
61
-
62
- const sourceFile = database.getSourceFile(sourcePath);
63
- debugSourceFile(sourcePath, sourceFile);
64
- if (sourceFile.type !== 'definition') {
65
- throw new Error(
66
- `Expected ${sourcePath} to be a message definitions file, but it resulted in ${sourceFile.type} instead.`,
67
- );
68
- }
69
-
70
- const messageKeys = database.getSourceFileKeyMap(sourcePath);
71
- const translationsPath = path.resolve(path.dirname(sourcePath), sourceFile.meta.translationsPath);
72
- const translationsLocaleMap = buildTranslationsLocaleMap(
73
- sourcePath,
74
- sourceFile,
75
- translationsPath,
76
- );
77
-
78
- if (processTranslations) {
79
- database.processAllTranslationFiles(translationsLocaleMap);
80
- }
81
-
82
- return {
83
- sourceFile,
84
- locale,
85
- messageKeys,
86
- translationsPath,
87
- translationsLocaleMap,
88
- };
89
- }
90
-
91
- /**
92
- *
93
- * @param {string} sourcePath
94
- * @param {string=} sourceContent
95
- * @param {{
96
- * locale?: string,
97
- * }=} options
98
- * @returns {import('./types').ProcessTranslationsResult}
99
- */
100
- function processTranslationsFile(sourcePath, sourceContent, options = {}) {
101
- const { locale = getLocaleFromTranslationsFileName(sourcePath) } = options;
102
- if (sourceContent) {
103
- database.processTranslationFileContent(sourcePath, locale, sourceContent);
104
- } else {
105
- database.processTranslationFile(sourcePath, locale);
106
- }
107
-
108
- const sourceFile = database.getSourceFile(sourcePath);
109
- debugSourceFile(sourcePath, sourceFile);
110
- if (sourceFile.type !== 'translation') {
111
- throw new Error(
112
- `Expected ${sourcePath} to be a message translations file, but it resulted in ${sourceFile.type} instead.`,
113
- );
114
- }
115
-
116
- return {
117
- sourceFile,
118
- locale,
119
- messageKeys: database.getSourceFileKeyMap(sourcePath),
120
- };
121
- }
122
-
123
- /**
124
- * Precompile the messages defined in the given `sourcePath` using the value of the translation for
125
- * that message in the given `locale`. `format` specifies which serialization format the result
126
- * will be written in.
127
- *
128
- * By default, the compiled content will be returned as a Buffer containing the serialized string,
129
- * but if `outputFile` is given then the content will be written directly to the file and the
130
- * function becomes `void`.
131
- *
132
- * @param {string} sourcePath
133
- * @param {string} locale
134
- * @param {{
135
- * format?: IntlCompiledMessageFormat,
136
- * outputFile?: string
137
- * }=} options
138
- *
139
- * @returns {Buffer | void}
140
- */
141
- function precompileFileForLocale(sourcePath, locale, options = {}) {
142
- const { format = IntlCompiledMessageFormat.KeylessJson, outputFile } = options;
143
- return outputFile != null
144
- ? database.precompile(sourcePath, locale, outputFile, format)
145
- : database.precompileToBuffer(sourcePath, locale, format);
146
- }
147
-
148
- /**
149
- * Generate a `.d.ts` file containing TypeScript type definitions for all of the messages defined in
150
- * `sourcePath`. This method does not process `sourcePath` at all, meaning it expects the database
151
- * to already know about the source, as well as all of the related translations to create an
152
- * accurate typescript definition for each message.
153
- *
154
- * If not given, `outputFile` will default to the same path as `sourcePath`, with the last extension
155
- * replaced by `.d.ts`. For example, a file like `SomeMessages.Other.messages.js` would become
156
- * `SomeMessages.Other.messages.d.ts`.
157
- *
158
- * If `allowNullability` is set, the generated types for variables within messages will allow
159
- * `null` and `undefined` for most value types, as well as looser restrictions on typing, such as
160
- * allowing `string | number` for number variables.
161
- *
162
- * Returns `true` if the types were successfully generated, or `false` otherwise, such as if the
163
- * source file is not already in the database.
164
- *
165
- * @param {string} sourcePath
166
- * @param {string=} outputFile
167
- * @param {boolean=} allowNullability
168
- * @returns {boolean}
169
- */
170
- function generateTypeDefinitions(sourcePath, outputFile, allowNullability = false) {
171
- const paths = database.getAllSourceFilePaths();
172
- if (!paths.includes(sourcePath)) return false;
173
-
174
- database.generateTypes(
175
- sourcePath,
176
- outputFile ?? sourcePath.replace(/\.[^.]+$/, '.d.ts'),
177
- allowNullability,
178
- );
179
- return true;
180
- }
181
-
182
- module.exports = {
183
- generateTypeDefinitions,
184
- precompileFileForLocale,
185
- processDefinitionsFile,
186
- processTranslationsFile,
187
- };
@@ -1,156 +0,0 @@
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 DELETED
@@ -1,55 +0,0 @@
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 DELETED
@@ -1,99 +0,0 @@
1
- const path = require('node:path');
2
-
3
- const chokidar = require('chokidar');
4
- const fg = require('fast-glob');
5
- const debug = require('debug')('intl:metro-intl-transformer: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
- */
27
- function processFile(filePath, assetExtension) {
28
- debug(`Processing file: ${filePath}`);
29
- if (!isMessageDefinitionsFile(filePath)) {
30
- debug(`${filePath} is not a definitions file. Skipping processing`);
31
- return;
32
- }
33
-
34
- try {
35
- // Convert the file name from `.messages.js` to `.compiled.messages.jsona` for output.
36
- const outputPath = filePath.replace(/\.messages\.js$/, `.compiled.messages.${assetExtension}`);
37
- const result = processDefinitionsFile(filePath);
38
- precompileFileForLocale(filePath, result.locale, {
39
- format: IntlCompiledMessageFormat.KeylessJson,
40
- });
41
-
42
- database.processDefinitionsFile(filePath);
43
- database.precompile(
44
- filePath,
45
- DEFAULT_LOCALE,
46
- outputPath,
47
- IntlCompiledMessageFormat.KeylessJson,
48
- );
49
- debug(`Wrote definitions to: ${outputPath}`);
50
- } catch (e) {
51
- debug('[INTL Error] Failed to compile messages');
52
- console.error(e);
53
- }
54
- }
55
-
56
- /**
57
- * @param {string[]} watchedFolders
58
- * @param {{
59
- * watch?: boolean,
60
- * ignore?: string[],
61
- * assetExtension?: string
62
- * }} options
63
- */
64
- async function compileIntlMessageFiles(
65
- watchedFolders,
66
- { watch = true, ignore = [], assetExtension = 'json' } = {},
67
- ) {
68
- const ignoredPatterns = ignore.concat(ALWAYS_IGNORE_PATTERNS);
69
- const globs = watchedFolders.flatMap((folder) =>
70
- MESSAGE_DEFINITION_FILE_PATTERNS.map((pattern) => path.join(folder, pattern)),
71
- );
72
- debug(`Configured message file patterns:\n- ${globs.join('\n- ')}`);
73
-
74
- // Perform one initial scan and compilation to ensure all files exist before Metro might try to
75
- // resolve them.
76
- debug('Scanning for initial messages files');
77
- for await (const filePath of fg.stream(globs, {
78
- ignore: ignoredPatterns,
79
- absolute: true,
80
- onlyFiles: true,
81
- })) {
82
- processFile(filePath.toString(), assetExtension);
83
- }
84
- debug('Initial message scan completed.');
85
-
86
- if (watch) {
87
- debug(`Setting up file watching for configured paths`);
88
- chokidar
89
- .watch(globs, { ignored: ignoredPatterns, ignoreInitial: true })
90
- .on('all', (event, filePath) => {
91
- debug(`Got event ${event} for ${filePath}`);
92
- processFile(filePath, assetExtension);
93
- });
94
- } else {
95
- debug('Not watching files because `watch` option was false');
96
- }
97
- }
98
-
99
- module.exports = { compileIntlMessageFiles, ALWAYS_IGNORE_PATTERNS };
File without changes