8000 Add support for a secondary localized data resolution function. · rbuckton/rushstack@f9995a4 · GitHub
[go: up one dir, main page]

Skip to content

Commit f9995a4

Browse files
committed
Add support for a secondary localized data resolution function.
1 parent 95c6d48 commit f9995a4

File tree

8 files changed

+160
-53
lines changed

8 files changed

+160
-53
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"./src/strings3.loc.json": {
3+
"string1": "la tercera cadena",
4+
"string2": "cuerda cuatro con un ' apóstrofe",
5+
"string3": "UNUSED STRING!"
6+
}
7+
}

build-tests/localization-plugin-test-03/webpack.config.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,34 @@
22

33
const path = require('path');
44
const webpack = require('webpack');
5+
const {
6+
JsonFile,
7+
FileSystem
8+
} = require('@rushstack/node-core-library');
59

610
const { LocalizationPlugin } = require('@rushstack/localization-plugin');
711
const { SetPublicPathPlugin } = require('@rushstack/set-webpack-public-path-plugin');
812
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
913
const HtmlWebpackPlugin = require('html-webpack-plugin');
1014

15+
function resolveMissingString(localeNames, localizedResourcePath) {
16+
let contextRelativePath = path.relative(__dirname, localizedResourcePath);
17+
contextRelativePath = contextRelativePath.replace(/\\/g, '/'); // Convert Windows paths to Unix paths
18+
if (!contextRelativePath.startsWith('.')) {
19+
contextRelativePath = `./${contextRelativePath}`;
20+
}
21+
22+
const result = {};
23+
for (const localeName of localeNames) {
24+
const expectedCombinedStringsPath = path.resolve(__dirname, 'localization', localeName, 'combinedStringsData.json');
25+
if (FileSystem.exists(expectedCombinedStringsPath)) {
26+
const loadedCombinedStringsPath = JsonFile.load(expectedCombinedStringsPath);
27+
result[localeName] = loadedCombinedStringsPath[contextRelativePath];
28+
}
29+
}
30+
return result;
31+
}
32+
1133
module.exports = function(env) {
1234
const configuration = {
1335
mode: 'production',
@@ -55,11 +77,6 @@ module.exports = function(env) {
5577
"string1": "la primera cadena"
5678
},
5779
"./src/chunks/strings2.loc.json": "./localization/es-es/chunks/strings2.loc.json",
58-
"./src/strings3.loc.json": {
59-
"string1": "la tercera cadena",
60-
"string2": "cuerda cuatro con un ' apóstrofe",
61-
"string3": "UNUSED STRING!"
62-
},
6380
"./src/strings4.loc.json": {
6481
"string1": "\"Cadena con comillas\""
6582
},
@@ -72,6 +89,7 @@ module.exports = function(env) {
7289
}
7390
}
7491
},
92+
resolveMissingTranslatedStrings: resolveMissingString,
7593
passthroughLocale: {
7694
usePassthroughLocale: true
7795
},

common/reviews/api/localization-plugin.api.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export interface ILocaleFileData {
3232
[stringName: string]: string;
3333
}
3434

35+
// @internal (undocumented)
36+
export interface _ILocalizationFile {
37+
// (undocumented)
38+
[stringName: string]: _ILocalizedString;
39+
}
40+
3541
// @public
3642
export interface ILocalizationPluginOptions {
3743
filesToIgnore?: string[];
@@ -76,6 +82,7 @@ export interface ILocalizedData {
7682
defaultLocale: IDefaultLocaleOptions;
7783
passthroughLocale?: IPassthroughLocaleOptions;
7884
pseudolocales?: IPseudolocalesOptions;
85+
resolveMissingTranslatedStrings?: (locales: string[], filePath: string) => IResolvedMissingTranslations;
7986
translatedStrings: ILocalizedStrings;
8087
}
8188

@@ -101,12 +108,6 @@ export interface ILocalizedWebpackChunk extends Webpack.compilation.Chunk {
101108
};
102109
}
103110

104-
// @internal (undocumented)
105-
export interface _ILocFile {
106-
// (undocumented)
107-
[stringName: string]: _ILocalizedString;
108-
}
109-
110111
// @internal (undocumented)
111112
export interface _IParseLocFileOptions {
112113
// (undocumented)
@@ -147,6 +148,12 @@ export interface IPseudolocalesOptions {
147148
[pseudoLocaleName: string]: IPseudolocaleOptions;
148149
}
149150

151+
// @public (undocumented)
152+
export interface IResolvedMissingTranslations {
153+
// (undocumented)
154+
[localeName: string]: string | ILocaleFileData;
155+
}
156+
150157
// @internal (undocumented)
151158
export interface _IStringPlaceholder {
152159
// (undocumented)
@@ -179,8 +186,10 @@ export interface ITypingsGeneratorOptions {
179186
// @public
180187
export class LocalizationPlugin implements Webpack.Plugin {
181188
constructor(options: ILocalizationPluginOptions);
189+
// Warning: (ae-forgotten-export) The symbol "IAddDefaultLocFileResult" needs to be exported by the entry point index.d.ts
190+
//
182191
// @internal (undocumented)
183-
addDefaultLocFile(terminal: Terminal, locFilePath: string, locFile: _ILocFile): string[];
192+
addDefaultLocFile(terminal: Terminal, localizedResourcePath: string, localizedResourceData: _ILocalizationFile): IAddDefaultLocFileResult;
184193
// (undocumented)
185194
apply(compiler: Webpack.Compiler): void;
186195
// Warning: (ae-forgotten-export) The symbol "IStringSerialNumberData" needs to be exported by the entry point index.d.ts
@@ -194,7 +203,7 @@ export class LocalizationPlugin implements Webpack.Plugin {
194203
// @internal (undocumented)
195204
export class _LocFileParser {
196205
// (undocumented)
197-
static parseLocFile(options: _IParseLocFileOptions): _ILocFile;
206+
static parseLocFile(options: _IParseLocFileOptions): _ILocalizationFile;
198207
}
199208

200209
// @public

webpack/localization-plugin/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,24 @@ translatedStrings: {
136136
}
137137
```
138138

139+
#### `localizedData.resolveMissingTranslatedStrings = (locales: string[], filePath: string) => { ... }`
140+
141+
This optional option can be used to resolve translated data that is missing from data that is provided
142+
in the `localizedData.translatedStrings` option. Set this option with a function expecting two parameters:
143+
the first, an array of locale names, and second, a fully-qualified path to the localized file in source. The
144+
function should return an object with locale names as keys and localized data as values. The localized data
145+
value should either be:
146+
147+
- a string: The absolute path to the translated data in `.resx` or `.loc.json` format
148+
- an object: An object containing the translated data
149+
150+
Note that these values are the same as the values that can be specified for translations for a localized
151+
resource in `localizedData.translatedStrings`.
152+
153+
If the function returns data that is missing locales or individual strings, the plugin will fall back to the
154+
default locale if `localizedData.defaultLocale.fillMissingTranslationStrings` is set to `true`. If
155+
`localizedData.defaultLocale.fillMissingTranslationStrings` is set to `false`, an error will result.
156+
139157
#### `localizedData.passthroughLocale = { }`
140158

141159
This option is used to specify how and if a passthrough locale should be generated. A passthrough locale

webpack/localization-plugin/src/LocalizationPlugin.ts

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
ILocalizationFile,
2424
IPseudolocaleOptions,
2525
ILocaleElementMap,
26-
ILocalizedStrings
26+
ILocalizedStrings,
27+
IResolvedMissingTranslations
2728
} from './interfaces';
2829
import {
2930
ILocalizedWebpackChunk
@@ -42,6 +43,18 @@ export interface IStringPlaceholder {
4243
suffix: string;
4344
}
4445

46+
/**
47+
* @internal
48+
*/
49+
export interface IAddDefaultLocFileResult {
50+
/**
51+
* A list of paths to translation files that were loaded
52+
*/
53+
additionalLoadedFilePaths: string[];
54+
55+
errors: Error[];
56+
}
57+
4558
interface IExtendedMainTemplate {
4659
hooks: {
4760
assetPath: Tapable.SyncHook<string, IAssetPathOptions>;
@@ -400,39 +413,66 @@ export class LocalizationPlugin implements Webpack.Plugin {
400413
/**
401414
* @internal
402415
*
403-
* @returns A list of paths to translation files that were loaded
416+
* @returns
404417
*/
405418
public addDefaultLocFile(
406419
terminal: Terminal,
407-
locFilePath: string,
408-
locFile: ILocalizationFile
409-
): string[] {
410-
const additionalLoadedFiles: string[] = [];
420+
localizedResourcePath: string,
421+
localizedResourceData: ILocalizationFile
422+
): IAddDefaultLocFileResult {
423+
const additionalLoadedFilePaths: string[] = [];
424+
const errors: Error[] = [];
425+
426+
const locFileData: ILocaleFileData = this._convertLocalizationFileToLocData(localizedResourceData);
427+
this._addLocFile(this._defaultLocale, localizedResourcePath, locFileData);
411428

412-
const locFileData: ILocaleFileData = this._convertLocalizationFileToLocData(locFile);
413-
this._addLocFile(this._defaultLocale, locFilePath, locFileData);
429+
const resolveLocalizedData: (localizedData: ILocaleFileData | string) => ILocaleFileData = (localizedData) => {
430+
if (typeof localizedData === 'string') {
431+
additionalLoadedFilePaths.push(localizedData);
432+
const localizationFile: ILocalizationFile = LocFileParser.parseLocFile({
433+
filePath: localizedData,
434+
content: FileSystem.readFile(localizedData),
435+
terminal: terminal
436+
});
437+
438+
return this._convertLocalizationFileToLocData(localizationFile);
439+
} else {
440+
return localizedData;
441+
}
442+
};
414443

444+
const missingLocales: string[] = [];
415445
for (const translatedLocaleName in this._resolvedTranslatedStringsFromOptions) {
416446
if (this._resolvedTranslatedStringsFromOptions.hasOwnProperty(translatedLocaleName)) {
417447
const translatedLocFileFromOptions: ILocaleFileData | string | undefined = (
418-
this._resolvedTranslatedStringsFromOptions[translatedLocaleName][locFilePath]
448+
this._resolvedTranslatedStringsFromOptions[translatedLocaleName][localizedResourcePath]
419449
);
420-
if (translatedLocFileFromOptions) {
421-
let translatedLocFileData: ILocaleFileData;
422-
if (typeof translatedLocFileFromOptions === 'string') {
423-
additionalLoadedFiles.push(translatedLocFileFromOptions);
424-
const localizationFile: ILocalizationFile = LocFileParser.parseLocFile({
425-
filePath: translatedLocFileFromOptions,
426-
content: FileSystem.readFile(translatedLocFileFromOptions),
427-
terminal: terminal
428-
});
429-
430-
translatedLocFileData = this._convertLocalizationFileToLocData(localizationFile);
431-
} else {
432-
translatedLocFileData = translatedLocFileFromOptions;
433-
}
450+
if (!translatedLocFileFromOptions) {
451+
missingLocales.push(translatedLocaleName);
452+
} else {
453+
const translatedLocFileData: ILocaleFileData = resolveLocalizedData(translatedLocFileFromOptions);
454+
this._addLocFile(translatedLocaleName, localizedResourcePath, translatedLocFileData);
455+
}
456+
}
457+
}
434458

435-
this._addLocFile(translatedLocaleName, locFilePath, translatedLocFileData);
459+
if (missingLocales.length > 0 && this._options.localizedData.resolveMissingTranslatedStrings) {
460+
let resolvedTranslatedData: IResolvedMissingTranslations | undefined = undefined;
461+
try {
462+
resolvedTranslatedData = this._options.localizedData.resolveMissingTranslatedStrings(
463+
missingLocales,
464+
localizedResourcePath
465+
);
466+
} catch (e) {
467+
errors.push(e);
468+
}
469+
470+
if (resolvedTranslatedData) {
471+
for (const resolvedLocaleName in resolvedTranslatedData) {
472+
if (resolvedTranslatedData.hasOwnProperty(resolvedLocaleName)) {
473+
const translatedLocFileData: ILocaleFileData = resolveLocalizedData(resolvedTranslatedData[resolvedLocaleName]);
474+
this._addLocFile(resolvedLocaleName, localizedResourcePath, translatedLocFileData);
475+
}
436476
}
437477
}
438478
}
@@ -446,10 +486,10 @@ export class LocalizationPlugin implements Webpack.Plugin {
446486
}
447487
}
448488

449-
this._addLocFile(pseudolocaleName, locFilePath, pseudolocFileData);
489+
this._addLocFile(pseudolocaleName, localizedResourcePath, pseudolocFileData);
450490
});
451491

452-
return additionalLoadedFiles;
492+
return { additionalLoadedFilePaths, errors };
453493
}
454494

455495
/**
@@ -459,18 +499,15 @@ export class LocalizationPlugin implements Webpack.Plugin {
459499
return this._stringPlaceholderMap.get(serialNumber);
460500
}
461501

462-
/**
463-
* @returns A list of paths to translation files that were loaded
464-
*/
465-
private _addLocFile(localeName: string, locFilePath: string, locFileData: ILocaleFileData): void {
502+
private _addLocFile(localeName: string, localizedFilePath: string, localizedFileData: ILocaleFileData): void {
466503
const filesMap: Map<string, Map<string, string>> = this._resolvedLocalizedStrings.get(localeName)!;
467504

468505
const stringsMap: Map<string, string> = new Map<string, string>();
469-
filesMap.set(locFilePath, stringsMap);
506+
filesMap.set(localizedFilePath, stringsMap);
470507

471-
for (const stringName in locFileData) {
472-
if (locFileData.hasOwnProperty(stringName)) {
473-
const stringKey: string = `${locFilePath}?${stringName}`;
508+
for (const stringName in localizedFileData) {
509+
if (!localizedFileData.hasOwnProperty || localizedFileData.hasOwnProperty(stringName)) {
510+
const stringKey: string = `${localizedFilePath}?${stringName}`;
474511
if (!this.stringKeys.has(stringKey)) {
475512
const placeholder: IStringPlaceholder = this._getPlaceholderString();
476513
this.stringKeys.set(stringKey, placeholder);
@@ -484,13 +521,13 @@ export class LocalizationPlugin implements Webpack.Plugin {
484521
values: {
485522
[this._passthroughLocaleName]: stringName
486523
},
487-
locFilePath: locFilePath,
524+
locFilePath: localizedFilePath,
488525
stringName: stringName
489526
}
490527
);
491528
}
492529

493-
const stringValue: string = locFileData[stringName];
530+
const stringValue: string = localizedFileData[stringName];
494531

495532
this._stringPlaceholderMap.get(placeholder.suffix)!.values[localeName] = stringValue;
496533

webpack/localization-plugin/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export {
1111
ILocaleData,
1212
ILocaleElementMap,
1313
ILocaleFileData,
14-
ILocalizationFile as _ILocFile,
14+
ILocalizationFile as _ILocalizationFile,
1515
ILocalizationPluginOptions,
1616
ILocalizationStats,
1717
ILocalizationStatsChunkGroup,
@@ -23,6 +23,7 @@ export {
2323
IPassthroughLocaleOptions,
2424
IPseudolocaleOptions,
2525
IPseudolocalesOptions,
26+
IResolvedMissingTranslations,
2627
ITypingsGenerationOptions
2728
} from './interfaces';
2829

webpack/localization-plugin/src/interfaces.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ export interface ILocalizedData {
100100
*/
101101
translatedStrings: ILocalizedStrings;
102102

103+
/**
104+
* Use this paramter to specify a function used to load translations missing from
105+
* the {@link ILocalizedData.translatedStrings} parameter.
106+
*/
107+
resolveMissingTranslatedStrings?: (locales: string[], filePath: string) => IResolvedMissingTranslations;
108+
103109
/**
104110
* Options around including a passthrough locale.
105111
*/
@@ -184,6 +190,13 @@ export interface ILocaleFileData {
184190
[stringName: string]: string;
185191
}
186192

193+
/**
194+
* @public
195+
*/
196+
export interface IResolvedMissingTranslations {
197+
[localeName: string]: string | ILocaleFileData;
198+
}
199+
187200
/**
188201
* @public
189202
*/

webpack/localization-plugin/src/loaders/LocLoader.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ export default loaderFactory(
3030
const locFileData: ILocalizationFile = LocFileParser.parseLocFile({
3131
content,
3232
terminal,
33-
filePath: locFilePath,
33+
filePath: locFilePath
3434
});
35-
const additionalFiles: string[] = pluginInstance.addDefaultLocFile(terminal, locFilePath, locFileData);
36-
for (const additionalFile of additionalFiles) {
35+
const { additionalLoadedFilePaths, errors } = pluginInstance.addDefaultLocFile(terminal, locFilePath, locFileData);
36+
for (const additionalFile of additionalLoadedFilePaths) {
3737
this.dependency(additionalFile);
3838
}
3939

40+
for (const error of errors) {
41+
this.emitError(error);
42+
}
43+
4044
const resultObject: { [stringName: string]: string } = {};
4145
for (const stringName in locFileData) { // eslint-disable-line guard-for-in
4246
const stringKey: string = `${locFilePath}?${stringName}`;

0 commit comments

Comments
 (0)
< 2A44 footer class="footer pt-8 pb-6 f6 color-fg-muted p-responsive" role="contentinfo" >

Footer

© 2025 GitHub, Inc.
0