Skip to content

Commit ab06e9f

Browse files
authored
Merge pull request microsoft#3455 from dmichon-msft/localization-utilities-refactor
[localization-utilities] Expose parsers directly
2 parents e6335fe + e54c2ad commit ab06e9f

18 files changed

+327
-74
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/localization-utilities",
5+
"comment": "Expose each parser directly so that they can be used with arbitrary file extensions.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/localization-utilities"
10+
}

common/config/rush/pnpm-lock.yaml

Lines changed: 2 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/config/rush/repo-state.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
22
{
3-
"pnpmShrinkwrapHash": "964142cc110c45eed3898aebab1f9361ebe58b3c",
3+
"pnpmShrinkwrapHash": "065e82bf4b7942542ba0062d3f18fb4059dd59e6",
44
"preferredVersionsHash": "d2a5d015a5e5f4861bc36581c3c08cb789ed7fab"
55
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ export interface ILocalizedString {
2525
value: string;
2626
}
2727

28+
// @public (undocumented)
29+
export interface IParseFileOptions {
30+
// (undocumented)
31+
content: string;
32+
// (undocumented)
33+
filePath: string;
34+
}
35+
2836
// @public (undocumented)
2937
export interface IParseLocFileOptions {
3038
// (undocumented)
@@ -34,6 +42,8 @@ export interface IParseLocFileOptions {
3442
// (undocumented)
3543
ignoreMissingResxComments: boolean | undefined;
3644
// (undocumented)
45+
parser?: ParserKind;
46+
// (undocumented)
3747
resxNewlineNormalization: NewlineKind | undefined;
3848
// (undocumented)
3949
terminal: ITerminal;
@@ -94,6 +104,15 @@ export interface ITypingsGeneratorOptions {
94104
// @public (undocumented)
95105
export function parseLocFile(options: IParseLocFileOptions): ILocalizationFile;
96106

107+
// @public (undocumented)
108+
export function parseLocJson(options: IParseFileOptions): ILocalizationFile;
109+
110+
// @public (undocumented)
111+
export function parseResJson(options: IParseFileOptions): ILocalizationFile;
112+
113+
// @public (undocumented)
114+
export type ParserKind = 'resx' | 'loc.json' | 'resjson';
115+
97116
// @public (undocumented)
98117
export function readResxAsLocFile(resxContents: string, options: IResxReaderOptions): ILocalizationFile;
99118

libraries/localization-utilities/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@
1212
},
1313
"scripts": {
1414
"build": "heft build --clean",
15-
"_phase:build": "heft build --clean"
15+
"_phase:build": "heft build --clean",
16+
"_phase:test": "heft test --no-build"
1617
},
1718
"dependencies": {
1819
"@rushstack/node-core-library": "workspace:*",
1920
"@rushstack/typings-generator": "workspace:*",
20-
"decache": "~4.5.1",
2121
"pseudolocale": "~1.1.0",
2222
"xmldoc": "~1.1.2"
2323
},
2424
"devDependencies": {
2525
"@rushstack/eslint-config": "workspace:*",
2626
"@rushstack/heft": "workspace:*",
2727
"@rushstack/heft-node-rig": "workspace:*",
28+
"@types/heft-jest": "1.0.1",
2829
"@types/node": "12.20.24",
2930
"@types/xmldoc": "1.1.4"
3031
}
Lines changed: 36 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import { ITerminal, NewlineKind, JsonFile, JsonSchema } from '@rushstack/node-core-library';
4+
import { ITerminal, NewlineKind } from '@rushstack/node-core-library';
55

66
import { ILocalizationFile } from './interfaces';
7+
import { parseLocJson } from './parsers/parseLocJson';
8+
import { parseResJson } from './parsers/parseResJson';
79
import { readResxAsLocFile } from './ResxReader';
810

9-
const LOC_JSON_SCHEMA: JsonSchema = JsonSchema.fromFile(`${__dirname}/schemas/locJson.schema.json`);
11+
/**
12+
* @public
13+
*/
14+
export type ParserKind = 'resx' | 'loc.json' | 'resjson';
1015

1116
/**
1217
* @public
@@ -15,6 +20,7 @@ export interface IParseLocFileOptions {
1520
terminal: ITerminal;
1621
filePath: string;
1722
content: string;
23+
parser?: ParserKind;
1824
resxNewlineNormalization: NewlineKind | undefined;
1925
ignoreMissingResxComments: boolean | undefined;
2026
}
@@ -26,65 +32,47 @@ interface IParseCacheEntry {
2632

2733
const parseCache: Map<string, IParseCacheEntry> = new Map<string, IParseCacheEntry>();
2834

35+
export function selectParserByFilePath(filePath: string): ParserKind {
36+
if (/\.resx$/i.test(filePath)) {
37+
return 'resx';
38+
} else if (/\.(resx|loc)\.json$/i.test(filePath)) {
39+
return 'loc.json';
40+
} else if (/\.resjson$/i.test(filePath)) {
41+
return 'resjson';
42+
} else {
43+
throw new Error(`Unsupported file extension in file: ${filePath}`);
44+
}
45+
}
46+
2947
/**
3048
* @public
3149
*/
3250
export function parseLocFile(options: IParseLocFileOptions): ILocalizationFile {
33-
const fileCacheKey: string = `${options.filePath}?${options.resxNewlineNormalization || 'none'}`;
34-
if (parseCache.has(fileCacheKey)) {
35-
const entry: IParseCacheEntry = parseCache.get(fileCacheKey)!;
36-
if (entry.content === options.content) {
37-
return entry.parsedFile;
38-
}
39-
}
51+
const { parser = selectParserByFilePath(options.filePath) } = options;
4052

4153
let parsedFile: ILocalizationFile;
42-
if (/\.resx$/i.test(options.filePath)) {
54+
if (parser === 'resx') {
55+
const fileCacheKey: string = `${options.filePath}?${options.resxNewlineNormalization || 'none'}`;
56+
if (parseCache.has(fileCacheKey)) {
57+
const entry: IParseCacheEntry = parseCache.get(fileCacheKey)!;
58+
if (entry.content === options.content) {
59+
return entry.parsedFile;
60+
}
61+
}
4362
parsedFile = readResxAsLocFile(options.content, {
4463
terminal: options.terminal,
4564
resxFilePath: options.filePath,
4665
newlineNormalization: options.resxNewlineNormalization,
4766
warnOnMissingComment: !options.ignoreMissingResxComments
4867
});
49-
} else if (/\.(resx|loc)\.json$/i.test(options.filePath)) {
50-
parsedFile = JsonFile.parseString(options.content);
51-
try {
52-
LOC_JSON_SCHEMA.validateObject(parsedFile, options.filePath);
53-
} catch (e) {
54-
options.terminal.writeError(`The loc file is invalid. Error: ${e}`);
55-
}
56-
} else if (/\.resjson$/i.test(options.filePath)) {
57-
const resjsonFile: Record<string, string> = JsonFile.parseString(options.content);
58-
parsedFile = {};
59-
const comments: Map<string, string> = new Map();
60-
for (const [key, value] of Object.entries(resjsonFile)) {
61-
if (key.startsWith('_') && key.endsWith('.comment')) {
62-
const commentKey: string = key.substring(1, key.length - '.comment'.length);
63-
comments.set(commentKey, value);
64-
} else {
65-
parsedFile[key] = { value };
66-
}
67-
}
68+
parseCache.set(fileCacheKey, { content: options.content, parsedFile });
6869

69-
const orphanComments: string[] = [];
70-
for (const [key, comment] of comments) {
71-
if (parsedFile[key]) {
72-
parsedFile[key].comment = comment;
73-
} else {
74-
orphanComments.push(key);
75-
}
76-
}
77-
78-
if (orphanComments.length > 0) {
79-
options.terminal.writeErrorLine(
80-
'The resjson file is invalid. Comments exist for the following string keys ' +
81-
`that don't have values: ${orphanComments.join(', ')}.`
82-
);
83-
}
70+
return parsedFile;
71+
} else if (parser === 'loc.json') {
72+
return parseLocJson(options);
73+
} else if (parser === 'resjson') {
74+
return parseResJson(options);
8475
} else {
85-
throw new Error(`Unsupported file extension in file: ${options.filePath}`);
76+
throw new Error(`Unsupported parser: ${parser}`);
8677
}
87-
88-
parseCache.set(fileCacheKey, { content: options.content, parsedFile });
89-
return parsedFile;
9078
}
Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import decache from 'decache';
4+
import vm from 'vm';
5+
import { FileSystem } from '@rushstack/node-core-library';
56

67
import { IPseudolocaleOptions } from './interfaces';
78

9+
const pseudolocalePath: string = require.resolve('pseudolocale/pseudolocale.min.js');
10+
11+
interface IPseudolocale {
12+
option: IPseudolocaleOptions;
13+
str(str: string): string;
14+
}
15+
816
/**
917
* Get a function that pseudolocalizes a string.
1018
*
1119
* @public
1220
*/
1321
export function getPseudolocalizer(options: IPseudolocaleOptions): (str: string) => string {
14-
// pseudolocale maintains static state, so we need to load it as isolated modules
15-
decache('pseudolocale');
16-
const pseudolocale = require('pseudolocale'); // eslint-disable-line
17-
18-
pseudolocale.option = {
19-
...pseudolocale.option,
20-
...options
22+
const pseudolocaleCode: string = FileSystem.readFile(pseudolocalePath);
23+
const context: {
24+
pseudolocale: IPseudolocale | undefined;
25+
} = {
26+
pseudolocale: undefined
2127
};
28+
29+
// Load pseudolocale in an isolated context because the configuration for is stored on a singleton
30+
vm.runInNewContext(pseudolocaleCode, context);
31+
const { pseudolocale } = context;
32+
if (!pseudolocale) {
33+
throw new Error(`Failed to load pseudolocale module`);
34+
}
35+
36+
Object.assign(pseudolocale.option, options);
37+
// `pseudolocale.str` captures `pseudolocale` in its closure and refers to `pseudolocale.option`.
2238
return pseudolocale.str;
2339
}

libraries/localization-utilities/src/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
export { ILocalizationFile, ILocalizedString, IPseudolocaleOptions } from './interfaces';
5-
export { parseLocFile, IParseLocFileOptions } from './LocFileParser';
4+
export type {
5+
ILocalizationFile,
6+
ILocalizedString,
7+
IPseudolocaleOptions,
8+
IParseFileOptions
9+
} from './interfaces';
10+
export { parseLocJson } from './parsers/parseLocJson';
11+
export { parseResJson } from './parsers/parseResJson';
12+
export { parseLocFile, IParseLocFileOptions, ParserKind } from './LocFileParser';
613
export {
714
ITypingsGeneratorOptions,
815
LocFileTypingsGenerator as TypingsGenerator

libraries/localization-utilities/src/interfaces.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,11 @@ export interface ILocalizedString {
3333
value: string;
3434
comment?: string;
3535
}
36+
37+
/**
38+
* @public
39+
*/
40+
export interface IParseFileOptions {
41+
content: string;
42+
filePath: string;
43+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import { resolve } from 'path';
5+
6+
import { JsonFile, JsonSchema } from '@rushstack/node-core-library';
7+
8+
import { ILocalizationFile, IParseFileOptions } from '../interfaces';
9+
10+
const LOC_JSON_SCHEMA_PATH: string = resolve(__dirname, '../schemas/locJson.schema.json');
11+
const LOC_JSON_SCHEMA: JsonSchema = JsonSchema.fromFile(LOC_JSON_SCHEMA_PATH);
12+
13+
/**
14+
* @public
15+
*/
16+
export function parseLocJson(options: IParseFileOptions): ILocalizationFile {
17+
const parsedFile: ILocalizationFile = JsonFile.parseString(options.content);
18+
try {
19+
LOC_JSON_SCHEMA.validateObject(parsedFile, options.filePath);
20+
} catch (e) {
21+
throw new Error(`The loc file is invalid. Error: ${e}`);
22+
}
23+
return parsedFile;
24+
}

0 commit comments

Comments
 (0)