This repository was archived by the owner on May 19, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathindex.js
More file actions
181 lines (157 loc) · 5.5 KB
/
index.js
File metadata and controls
181 lines (157 loc) · 5.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/* eslint-env commonjs, node, es6 */
const Path = require("path");
const LoaderUtils = require("loader-utils");
/**
* @typedef {Object} WebpackConfig
* @property {string} context
* The root path of the project, essentially.
* Relative paths in the webpack config object are considered to be relative to this.
*/
/**
* @typedef {Object} WebpackLoaderContext
* @property {WebpackConfig} options - The webpack configuration object.
* @property {function} cacheable - Marks the loader as cacheable.
* @property {function} exec - Executes a piece of JavaScript as a module.
* @property {string} resourcePath - The path of the module being required.
*/
/**
* @typedef {Object} LoaderConfig
* @property {boolean} shorten
* If true, shorten prefixes generated instead of using a full relative path.
* This should be avoided because it is non-deterministic depending on the
* order in which the files are compiled.
*/
/**
* @type {number}
* When shortening prefixes, the next prefix number that will be used.
*/
let sequence = 1;
/**
* @type {Object.<string, number>}
* When shortening prefixes, a list of prefixes that have already been
* mapped to a short prefix.
*/
const prefixMap = {};
/**
* Takes a single i18n file and returns a JavaScript module that generates
* globally-unique keys for nested strings, and exports those keys in the
* same structure as the input file, also exporting a $messages export with
* the actual values of these messages in particular languages.
*
* The i18n file is assumed to be a JavaScript module, to load JSON this
* loader should be chained with `json-loader`.
*
* @this {LoaderConfig}
* @param {string} source - The JSON source of the messages file being required.
*/
module.exports = function(source) {
this.cacheable && this.cacheable();
const config = getConfig.call(this);
return compile.call(this, source, config);
};
/**
* Returns a unique prefix specific to the intl module being loaded. This allows
* messages to be unique even if different modules export the same key.
*
* @this {WebpackLoaderContext}
* @param {LoaderConfig} config - Our config.
*/
function getPrefix(config) {
const prefix =
Path.relative(this.options.context, this.resourcePath)
.replace(/\\/g, "/"); // Crude attempt at normalisation
if (!config.shorten)
return prefix;
return prefixMap[prefix] = prefixMap[prefix] || sequence++;
}
/**
* Parses the source into a JSON object and verifies its structure.
*
* The source should be a JSON object with nested string fields.
* Other types of field are not allowed.
*
* @this {WebpackLoaderContext}
* @param {string} source - The source string passed in by Webpack.
* @return {Object} - The parsed JSON.
*/
function parseSource(source) {
const json = this.exec(source, this.resourcePath);
if (typeof json !== "object")
throw new Error("Locale data must be an object");
/**
* Recursively checks the validity of a parsed i18n file.
* @param {Object} obj - the current part of the file being verified.
* @param {string} path
* The nested path to the current part of the file, to
* help produce more useful error messages.
*/
function check(obj, path) {
for (let key in obj) {
// TODO Maybe validate that top-level keys are valid
// locales, or at least look like them
switch(typeof obj[key]) {
case "string": break;
case "object": check(obj[key], `${path}.${key}`); break;
default:
throw new TypeError(`${path}.${key}: values in a locale file must be strings or objects`);
}
}
}
check(json, "root");
return json;
}
/**
* Gets the loader config.
*
* @this {WebpackConfig}
* @returns {LoaderConfig}
*/
function getConfig() {
return LoaderUtils.getLoaderConfig(this, "reactIntlModules");
}
/**
* Converts a single file to an ECMAScript module.
*
* Replaces message names with unique message ids in the same nested structure,
* which is exported as the default export.
*
* Each language is exported as an object mapping message IDs to actual strings
* in that language.
*
* @this {WebpackLoaderContext}
* @param {string} source - The source file's contents.
* @param {LoaderConfig} config - The loader config.
* @returns {string} The compiled JavaScript module.
*/
function compile(source, config) {
const json = parseSource.call(this, source);
const prefix = getPrefix.call(this, config);
let ids = {};
let langs = {};
function convert(obj, path) {
let ids = {};
let values = {};
for (let key in obj) {
const value = obj[key];
if (typeof value === "object") {
const nested = convert(obj[key], `${path}${key}.`);
ids[key] = nested.ids;
Object.assign(values, nested.values);
} else {
ids[key] = path + key;
values[path + key] = value;
}
}
return { ids, values };
}
for (let lang in json) {
let converted = convert(json[lang], `${prefix}:`);
langs[lang] = converted.values;
Object.assign(ids, converted.ids);
}
return [
...Object.keys(ids).map(key =>
`export const ${key}=${JSON.stringify(ids[key])}`),
`export const $messages = ${JSON.stringify(langs)}`
].join(";");
}