Skip to content

Commit a8b9cdd

Browse files
Benjamin Hodgsonprivatenumber
Benjamin Hodgson
andauthored
feat: support multiple localize functions (#24)
Co-authored-by: Hiroki Osame <[email protected]>
1 parent ce9b23a commit a8b9cdd

File tree

5 files changed

+190
-78
lines changed

5 files changed

+190
-78
lines changed

README.md

+11-4
Original file line numberDiff line numberDiff line change
@@ -130,23 +130,30 @@ Enable to see warnings when unused string keys are found.
130130
### localizeCompiler
131131
Type:
132132
```ts
133-
(
133+
Record<
134+
string, // localizer function name (eg. __)
135+
(
134136
this: LocalizeCompilerContext,
135137
localizerArguments: string[],
136138
localeName: string,
137-
) => string
139+
) => string
140+
>
138141
```
139142

140143
Default:
141144
```ts
142-
function (localizerArguments) {
145+
{
146+
__(localizerArguments) {
143147
const [key] = localizerArguments;
144148
const keyResolved = this.resolveKey();
145149
return keyResolved ? JSON.stringify(keyResolved) : key;
150+
}
146151
}
147152
```
148153

149-
A function to generate a JS string to replace the `__()` call with. It gets called for each localize function call (eg. `__(...)`) for each locale.
154+
An object of functions to generate a JS string to replace the `__()` call with. The object key is the localize function name, and its function gets called for each localize function call (eg. `__(...)`) for each locale. This allows you to have multiple localization functions, with separate compilation logic for each of them.
155+
156+
Note, you cannot use both `functionName` and `localizeCompiler`. Simply set the function name as a key in the `localizeCompiler` object instead.
150157

151158
#### localizerArguments
152159
An array of strings containing JavaScript expressions. The expressions are stringified arguments of the original call. So `localizerArguments[0]` will be a JavaScript expression containing the translation key.

src/index.ts

+81-55
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
LocaleFilePath,
1313
LocalizeCompiler,
1414
WP5,
15+
LocalizeCompilerContext,
1516
} from './types';
1617
import { loadLocales } from './utils/load-locales';
1718
import { interpolateLocaleToFileName } from './utils/localize-filename';
@@ -29,10 +30,13 @@ import {
2930
fileNameTemplatePlaceholder,
3031
} from './multi-locale';
3132
import { callLocalizeCompiler } from './utils/call-localize-compiler';
33+
import { stringifyAst } from './utils/stringify-ast';
3234

3335
// eslint-disable-next-line @typescript-eslint/no-var-requires
3436
const { name } = require('../package.json');
3537

38+
const defaultLocalizerName = '__';
39+
3640
class LocalizeAssetsPlugin<LocalizedData = string> {
3741
private readonly options: Options<LocalizedData>;
3842

@@ -42,7 +46,7 @@ class LocalizeAssetsPlugin<LocalizedData = string> {
4246

4347
private readonly localizeCompiler: LocalizeCompiler<LocalizedData>;
4448

45-
private readonly functionName: string;
49+
private readonly functionNames: string[];
4650

4751
private locales: LocalesMap<LocalizedData> = {};
4852

@@ -55,9 +59,6 @@ class LocalizeAssetsPlugin<LocalizedData = string> {
5559

5660
this.options = options;
5761

58-
const functionName = options.functionName ?? '__';
59-
this.functionName = functionName;
60-
6162
this.localeNames = Object.keys(options.locales);
6263
if (this.localeNames.length === 1) {
6364
[this.singleLocale] = this.localeNames;
@@ -66,18 +67,12 @@ class LocalizeAssetsPlugin<LocalizedData = string> {
6667
this.localizeCompiler = (
6768
this.options.localizeCompiler
6869
? this.options.localizeCompiler
69-
: function (localizerArguments) {
70-
const [key] = localizerArguments;
71-
72-
if (localizerArguments.length > 1) {
73-
this.emitWarning(`[${name}] Ignoring confusing usage of localization function: ${functionName}(${localizerArguments.join(', ')})`);
74-
return key;
75-
}
76-
77-
const keyResolved = this.resolveKey();
78-
return keyResolved ? JSON.stringify(keyResolved) : key;
70+
: {
71+
[this.options.functionName ?? defaultLocalizerName]: defaultLocalizeCompilerFunction,
7972
}
8073
);
74+
75+
this.functionNames = Object.keys(this.localizeCompiler);
8176
}
8277

8378
apply(compiler: Compiler) {
@@ -154,54 +149,62 @@ class LocalizeAssetsPlugin<LocalizedData = string> {
154149
private interceptTranslationFunctionCalls(
155150
normalModuleFactory: NormalModuleFactory,
156151
) {
157-
const { locales, singleLocale, functionName } = this;
152+
const { locales, singleLocale, functionNames } = this;
158153
const validator = localizedStringKeyValidator(locales, this.options.throwOnMissing);
159154

160-
onFunctionCall(
161-
normalModuleFactory,
162-
functionName,
163-
(parser, callExpressionNode) => {
164-
const { module } = parser.state;
165-
const firstArgumentNode = callExpressionNode.arguments[0];
166-
167-
// Enforce minimum requirement that first argument is a string
168-
if (
169-
!(
170-
callExpressionNode.arguments.length > 0
171-
&& firstArgumentNode.type === 'Literal'
172-
&& typeof firstArgumentNode.value === 'string'
173-
)
174-
) {
175-
const location = callExpressionNode.loc!.start;
176-
reportModuleWarning(
177-
module,
178-
new WebpackError(`[${name}] Ignoring confusing usage of localization function "${functionName}" in ${module.resource}:${location.line}:${location.column}`),
179-
);
180-
return;
181-
}
182-
183-
const stringKey: LocalizedStringKey = firstArgumentNode.value;
184-
185-
validator.assertValidLocaleString(
186-
stringKey,
155+
const handler = (
156+
parser: WP5.javascript.JavascriptParser,
157+
callExpressionNode: SimpleCallExpression,
158+
functionName: string,
159+
) => {
160+
const { module } = parser.state;
161+
const firstArgumentNode = callExpressionNode.arguments[0];
162+
163+
// Enforce minimum requirement that first argument is a string
164+
if (
165+
!(
166+
callExpressionNode.arguments.length > 0
167+
&& firstArgumentNode.type === 'Literal'
168+
&& typeof firstArgumentNode.value === 'string'
169+
)
170+
) {
171+
const location = callExpressionNode.loc!.start;
172+
reportModuleWarning(
187173
module,
188-
callExpressionNode,
174+
new WebpackError(`[${name}] Ignoring confusing usage of localization function "${functionName}" in ${module.resource}:${location.line}:${location.column}`),
189175
);
176+
return;
177+
}
190178

191-
for (const fileDependency of this.fileDependencies) {
192-
module.buildInfo.fileDependencies.add(fileDependency);
193-
}
179+
const stringKey: LocalizedStringKey = firstArgumentNode.value;
194180

195-
const replacement = (
196-
singleLocale
197-
? this.getLocalizedString(callExpressionNode, stringKey, module, singleLocale)
198-
: this.getMarkedFunctionPlaceholder(callExpressionNode, stringKey, module)
199-
);
200-
toConstantDependency(parser, replacement)(callExpressionNode);
181+
validator.assertValidLocaleString(
182+
stringKey,
183+
module,
184+
callExpressionNode,
185+
);
201186

202-
return true;
203-
},
204-
);
187+
for (const fileDependency of this.fileDependencies) {
188+
module.buildInfo.fileDependencies.add(fileDependency);
189+
}
190+
191+
const replacement = (
192+
singleLocale
193+
? this.getLocalizedString(callExpressionNode, stringKey, module, singleLocale)
194+
: this.getMarkedFunctionPlaceholder(callExpressionNode, stringKey, module)
195+
);
196+
toConstantDependency(parser, replacement)(callExpressionNode);
197+
198+
return true;
199+
};
200+
201+
for (const functionName of functionNames) {
202+
onFunctionCall(
203+
normalModuleFactory,
204+
functionName,
205+
(parser, node) => handler(parser, node, functionName),
206+
);
207+
}
205208
}
206209

207210
/**
@@ -256,6 +259,29 @@ class LocalizeAssetsPlugin<LocalizedData = string> {
256259

257260
return markLocalizeFunction(callNode);
258261
}
262+
263+
static defaultLocalizeCompiler: LocalizeCompiler = {
264+
[defaultLocalizerName]: defaultLocalizeCompilerFunction,
265+
};
266+
}
267+
268+
function defaultLocalizeCompilerFunction(
269+
this: LocalizeCompilerContext,
270+
localizerArguments: string[],
271+
) {
272+
const [key] = localizerArguments;
273+
274+
if (localizerArguments.length > 1) {
275+
let code = stringifyAst(this.callNode);
276+
if (code.length > 80) {
277+
code = `${code.slice(0, 80)}…`;
278+
}
279+
this.emitWarning(`[${name}] Ignoring confusing usage of localization function: ${code})`);
280+
return key;
281+
}
282+
283+
const keyResolved = this.resolveKey();
284+
return keyResolved ? JSON.stringify(keyResolved) : key;
259285
}
260286

261287
export = LocalizeAssetsPlugin;

src/types.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ export interface LocalizeCompilerContext<LocalizedData = string> {
3333
emitError(message: string): void;
3434
}
3535

36-
export type LocalizeCompiler<LocalizedData = string> = (
37-
this: LocalizeCompilerContext<LocalizedData>,
38-
functionArgments: string[],
39-
localeName: string,
40-
) => string;
36+
export interface LocalizeCompiler<LocalizedData = string> {
37+
[functionName: string]: (
38+
this: LocalizeCompilerContext<LocalizedData>,
39+
functionArgments: string[],
40+
localeName: string,
41+
) => string;
42+
}
4143

4244
export function validateOptions<LocalizedData>(options: Options<LocalizedData>): void {
4345
if (!options) {
@@ -53,6 +55,14 @@ export function validateOptions<LocalizedData>(options: Options<LocalizedData>):
5355
&& options.sourceMapForLocales.some(locale => !hasOwnProp(options.locales, locale))) {
5456
throw new Error('sourceMapForLocales must contain valid locales');
5557
}
58+
if (options.localizeCompiler) {
59+
if (Object.keys(options.localizeCompiler).length === 0) {
60+
throw new Error('localizeCompiler can\'t be an empty object');
61+
}
62+
if (options.functionName) {
63+
throw new Error('Can\'t use localizeCompiler and also specify functionName');
64+
}
65+
}
5666
}
5767

5868
export { WP4, WP5 };

src/utils/call-localize-compiler.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Identifier } from 'estree';
12
import { LocalizeCompiler, LocalizeCompilerContext } from '../types';
23
import { stringifyAst } from './stringify-ast';
34

@@ -7,5 +8,6 @@ export function callLocalizeCompiler<LocalizedData>(
78
localeName: string,
89
) {
910
const callNodeArguments = context.callNode.arguments.map(stringifyAst);
10-
return localizeCompiler.call(context, callNodeArguments, localeName);
11+
const functionName = (context.callNode.callee as Identifier).name;
12+
return localizeCompiler[functionName].call(context, callNodeArguments, localeName);
1113
}

0 commit comments

Comments
 (0)