Skip to content

Commit 32f0c49

Browse files
authored
Merge pull request microsoft#1812 from iclanton/ianc/normalize-newlines
[localization-plugin] Add support for normalization of newlines in RESX files.
2 parents 0d4620a + 823a89d commit 32f0c49

File tree

14 files changed

+130
-32
lines changed

14 files changed

+130
-32
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ module.exports = function(env) {
6666
passthroughLocale: {
6767
usePassthroughLocale: true,
6868
passthroughLocaleName: 'default'
69-
}
69+
},
70+
normalizeResxNewlines: 'crlf'
7071
},
7172
typingsOptions: {
7273
generatedTsFolder: path.resolve(__dirname, 'temp', 'loc-json-ts'),

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ module.exports = function(env) {
104104
append: '##--!!]',
105105
prepend: '[!!--##'
106106
}
107-
}
107+
},
108+
normalizeResxNewlines: 'lf'
108109
},
109110
typingsOptions: {
110111
generatedTsFolder: path.resolve(__dirname, 'temp', 'loc-json-ts'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/localization-plugin",
5+
"comment": "Add support for normalization of newlines in RESX files.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/localization-plugin",
10+
"email": "[email protected]"
11+
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
```ts
66

7+
import { NewlineKind } from '@rushstack/node-core-library';
78
import { StringValuesTypingsGenerator } from '@rushstack/typings-generator';
89
import { Terminal } from '@rushstack/node-core-library';
910
import * as Webpack from 'webpack';
@@ -80,6 +81,7 @@ export interface ILocalizationStatsOptions {
8081
// @public (undocumented)
8182
export interface ILocalizedData {
8283
defaultLocale: IDefaultLocaleOptions;
84+
normalizeResxNewlines?: 'lf' | 'crlf';
8385
passthroughLocale?: IPassthroughLocaleOptions;
8486
pseudolocales?: IPseudolocalesOptions;
8587
resolveMissingTranslatedStrings?: (locales: string[], filePath: string) => IResolvedMissingTranslations;
@@ -115,6 +117,8 @@ export interface _IParseLocFileOptions {
115117
// (undocumented)
116118
filePath: string;
117119
// (undocumented)
120+
resxNewlineNormalization: NewlineKind | undefined;
121+
// (undocumented)
118122
terminal: Terminal;
119123
}
120124

@@ -178,6 +182,8 @@ export interface ITypingsGeneratorOptions {
178182
// (undocumented)
179183
generatedTsFolder: string;
180184
// (undocumented)
185+
resxNewlineNormalization?: NewlineKind | undefined;
186+
// (undocumented)
181187
srcFolder: string;
182188
// (undocumented)
183189
terminal?: Terminal;

webpack/localization-plugin/README.md

+8
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ This option allows pseudolocales to be generated from the strings in the default
178178
an option with pseudolocales as keys and options for the
179179
[pseudolocale package](https://www.npmjs.com/package/pseudolocale) as values.
180180

181+
#### `localizedData.normalizeResxNewlines = 'crlf' | 'lf'`
182+
183+
This option allows normalization of newlines in RESX files. RESX files are XML, so newlines can be
184+
specified by including a newline in the `<value>` element. For files stored on source control systems,
185+
clones on Windows can end up with CRLF newlines and clones on 'nix operating systems can end up with LF
186+
newlines. This option can be used to help make compilations run on different platforms produce the same
187+
result.
188+
181189
### `filesToIgnore = [ ]`
182190

183191
This option is used to specify `.resx` and `.loc.json` files that should not be processed by this plugin.

webpack/localization-plugin/src/LocFileTypingsGenerator.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {
55
StringValuesTypingsGenerator,
66
IStringValueTyping
77
} from '@rushstack/typings-generator';
8-
import { Terminal } from '@rushstack/node-core-library';
8+
import {
9+
Terminal,
10+
NewlineKind
11+
} from '@rushstack/node-core-library';
912

1013
import { ILocalizationFile } from './interfaces';
1114
import { LocFileParser } from './utilities/LocFileParser';
@@ -19,6 +22,7 @@ export interface ITypingsGeneratorOptions {
1922
terminal?: Terminal;
2023
exportAsDefault?: boolean;
2124
filesToIgnore?: string[];
25+
resxNewlineNormalization?: NewlineKind | undefined;
2226
}
2327

2428
/**
@@ -35,7 +39,8 @@ export class LocFileTypingsGenerator extends StringValuesTypingsGenerator {
3539
const locFileData: ILocalizationFile = LocFileParser.parseLocFile({
3640
filePath: filePath,
3741
content: fileContents,
38-
terminal: this._options.terminal!
42+
terminal: this._options.terminal!,
43+
resxNewlineNormalization: options.resxNewlineNormalization
3944
});
4045

4146
const typings: IStringValueTyping[] = [];

webpack/localization-plugin/src/LocalizationPlugin.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import {
55
JsonFile,
66
FileSystem,
7-
Terminal
7+
Terminal,
8+
NewlineKind
89
} from '@rushstack/node-core-library';
910
import * as Webpack from 'webpack';
1011
import * as path from 'path';
@@ -111,6 +112,7 @@ export class LocalizationPlugin implements Webpack.Plugin {
111112
private _noStringsLocaleName: string;
112113
private _fillMissingTranslationStrings: boolean;
113114
private _pseudolocalizers: Map<string, (str: string) => string> = new Map<string, (str: string) => string>();
115+
private _resxNewlineNormalization: NewlineKind | undefined;
114116

115117
/**
116118
* The outermost map's keys are the locale names.
@@ -173,7 +175,8 @@ export class LocalizationPlugin implements Webpack.Plugin {
173175
pluginInstance: this,
174176
configuration: compiler.options,
175177
filesToIgnore: this._filesToIgnore,
176-
localeNameOrPlaceholder: Constants.LOCALE_NAME_PLACEHOLDER
178+
localeNameOrPlaceholder: Constants.LOCALE_NAME_PLACEHOLDER,
179+
resxNewlineNormalization: this._resxNewlineNormalization
177180
};
178181

179182
if (errors.length > 0 || warnings.length > 0) {
@@ -432,7 +435,8 @@ export class LocalizationPlugin implements Webpack.Plugin {
432435
const localizationFile: ILocalizationFile = LocFileParser.parseLocFile({
433436
filePath: localizedData,
434437
content: FileSystem.readFile(localizedData),
435-
terminal: terminal
438+
terminal: terminal,
439+
resxNewlineNormalization: this._resxNewlineNormalization
436440
});
437441

438442
return this._convertLocalizationFileToLocData(localizationFile);
@@ -690,6 +694,30 @@ export class LocalizationPlugin implements Webpack.Plugin {
690694
}
691695
}
692696
// END options.localizedData.pseudoLocales
697+
698+
// START options.localizedData.normalizeResxNewlines
699+
if (this._options.localizedData.normalizeResxNewlines) {
700+
switch (this._options.localizedData.normalizeResxNewlines) {
701+
case 'crlf': {
702+
this._resxNewlineNormalization = NewlineKind.CrLf;
703+
break;
704+
}
705+
706+
case 'lf': {
707+
this._resxNewlineNormalization = NewlineKind.Lf;
708+
break;
709+
}
710+
711+
default: {
712+
errors.push(new Error(
713+
`Unexpected value "${this._options.localizedData.normalizeResxNewlines}" for option ` +
714+
'"localizedData.normalizeResxNewlines"'
715+
));
716+
break;
717+
}
718+
}
719+
}
720+
// END options.localizedData.normalizeResxNewlines
693721
} else if (!isWebpackDevServer) {
694722
throw new Error('Localized data must be provided unless webpack dev server is running.');
695723
}

webpack/localization-plugin/src/WebpackConfigurationUpdater.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import * as path from 'path';
55
import * as Webpack from 'webpack';
66
import * as SetPublicPathPluginPackageType from '@rushstack/set-webpack-public-path-plugin';
7+
import { NewlineKind } from '@rushstack/node-core-library';
78

89
import { Constants } from './utilities/Constants';
910
import { LocalizationPlugin } from './LocalizationPlugin';
@@ -15,13 +16,15 @@ export interface IWebpackConfigurationUpdaterOptions {
1516
configuration: Webpack.Configuration;
1617
filesToIgnore: Set<string>;
1718
localeNameOrPlaceholder: string;
19+
resxNewlineNormalization: NewlineKind | undefined;
1820
}
1921

2022
export class WebpackConfigurationUpdater {
2123
public static amendWebpackConfigurationForMultiLocale(options: IWebpackConfigurationUpdaterOptions): void {
2224
const loader: string = path.resolve(__dirname, 'loaders', 'LocLoader.js');
2325
const loaderOptions: ILocLoaderOptions = {
24-
pluginInstance: options.pluginInstance
26+
pluginInstance: options.pluginInstance,
27+
resxNewlineNormalization: options.resxNewlineNormalization
2528
};
2629

2730
WebpackConfigurationUpdater._addLoadersForLocFiles(options, loader, loaderOptions);
@@ -31,7 +34,9 @@ export class WebpackConfigurationUpdater {
3134

3235
public static amendWebpackConfigurationForInPlaceLocFiles(options: IWebpackConfigurationUpdaterOptions): void {
3336
const loader: string = path.resolve(__dirname, 'loaders', 'InPlaceLocFileLoader.js');
34-
const loaderOptions: IBaseLoaderOptions = {}
37+
const loaderOptions: IBaseLoaderOptions = {
38+
resxNewlineNormalization: options.resxNewlineNormalization
39+
};
3540

3641
WebpackConfigurationUpdater._addRulesToConfiguration(
3742
options.configuration,

webpack/localization-plugin/src/interfaces.ts

+5
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ export interface ILocalizedData {
115115
* Options for pseudo-localization.
116116
*/
117117
pseudolocales?: IPseudolocalesOptions;
118+
119+
/**
120+
* Normalize newlines in RESX files to either CRLF (Windows-style) or LF ('nix style)
121+
*/
122+
normalizeResxNewlines?: 'lf' | 'crlf';
118123
}
119124

120125
/**

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

+24-13
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,30 @@ import { Terminal } from '@rushstack/node-core-library';
66

77
import { ILocalizationFile } from '../interfaces';
88
import { LocFileParser } from '../utilities/LocFileParser';
9-
import { loaderFactory } from './LoaderFactory';
9+
import {
10+
loaderFactory,
11+
IBaseLoaderOptions
12+
} from './LoaderFactory';
1013
import { LoaderTerminalProvider } from '../utilities/LoaderTerminalProvider';
1114

12-
export default loaderFactory(function (this: loader.LoaderContext, locFilePath: string, content: string) {
13-
const locFileData: ILocalizationFile = LocFileParser.parseLocFile({
14-
content,
15-
filePath: locFilePath,
16-
terminal: new Terminal(LoaderTerminalProvider.getTerminalProviderForLoader(this))
17-
});
18-
const resultObject: { [stringName: string]: string } = {};
19-
for (const stringName in locFileData) { // eslint-disable-line guard-for-in
20-
resultObject[stringName] = locFileData[stringName].value;
21-
}
15+
export default loaderFactory(
16+
function (
17+
this: loader.LoaderContext,
18+
locFilePath: string,
19+
content: string,
20+
options: IBaseLoaderOptions
21+
) {
22+
const locFileData: ILocalizationFile = LocFileParser.parseLocFile({
23+
content,
24+
filePath: locFilePath,
25+
terminal: new Terminal(LoaderTerminalProvider.getTerminalProviderForLoader(this)),
26+
resxNewlineNormalization: options.resxNewlineNormalization
27+
});
28+
const resultObject: { [stringName: string]: string } = {};
29+
for (const stringName in locFileData) { // eslint-disable-line guard-for-in
30+
resultObject[stringName] = locFileData[stringName].value;
31+
}
2232

23-
return resultObject;
24-
});
33+
return resultObject;
34+
}
35+
);

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
import { loader } from 'webpack';
55
import * as loaderUtils from 'loader-utils';
6+
import { NewlineKind } from '@rushstack/node-core-library';
67

7-
export interface IBaseLoaderOptions { }
8+
export interface IBaseLoaderOptions {
9+
resxNewlineNormalization: NewlineKind | undefined;
10+
}
811

912
export interface ILoaderResult {
1013
[stringName: string]: string

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export default loaderFactory(
3030
const locFileData: ILocalizationFile = LocFileParser.parseLocFile({
3131
content,
3232
terminal,
33-
filePath: locFilePath
33+
filePath: locFilePath,
34+
resxNewlineNormalization: options.resxNewlineNormalization
3435
});
3536
const { additionalLoadedFilePaths, errors } = pluginInstance.addDefaultLocFile(terminal, locFilePath, locFileData);
3637
for (const additionalFile of additionalLoadedFilePaths) {

webpack/localization-plugin/src/utilities/LocFileParser.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// See LICENSE in the project root for license information.
33

44
import * as jju from 'jju';
5-
import { Terminal } from '@rushstack/node-core-library';
5+
import {
6+
Terminal,
7+
NewlineKind
8+
} from '@rushstack/node-core-library';
69

710
import { ILocalizationFile } from '../interfaces';
811
import { ResxReader } from './ResxReader';
@@ -15,6 +18,7 @@ export interface IParseLocFileOptions {
1518
terminal: Terminal;
1619
filePath: string;
1720
content: string;
21+
resxNewlineNormalization: NewlineKind | undefined;
1822
}
1923

2024
interface IParseCacheEntry {
@@ -29,8 +33,9 @@ const parseCache: Map<string, IParseCacheEntry> = new Map<string, IParseCacheEnt
2933
*/
3034
export class LocFileParser {
3135
public static parseLocFile(options: IParseLocFileOptions): ILocalizationFile {
32-
if (parseCache.has(options.filePath)) {
33-
const entry: IParseCacheEntry = parseCache.get(options.filePath)!;
36+
const fileCacheKey: string = `${options.filePath}?${options.resxNewlineNormalization || 'none'}`;
37+
if (parseCache.has(fileCacheKey)) {
38+
const entry: IParseCacheEntry = parseCache.get(fileCacheKey)!;
3439
if (entry.content === options.content) {
3540
return entry.parsedFile;
3641
}
@@ -42,7 +47,8 @@ export class LocFileParser {
4247
options.content,
4348
{
4449
terminal: options.terminal,
45-
resxFilePath: options.filePath
50+
resxFilePath: options.filePath,
51+
newlineNormalization: options.resxNewlineNormalization
4652
}
4753
);
4854
} else {
@@ -54,7 +60,7 @@ export class LocFileParser {
5460
}
5561
}
5662

57-
parseCache.set(options.filePath, { content: options.content, parsedFile });
63+
parseCache.set(fileCacheKey, { content: options.content, parsedFile });
5864
return parsedFile;
5965
}
6066
}

webpack/localization-plugin/src/utilities/ResxReader.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
import {
55
FileSystem,
6-
Terminal
6+
Terminal,
7+
Text,
8+
NewlineKind
79
} from '@rushstack/node-core-library';
810
import {
911
XmlDocument,
@@ -20,6 +22,7 @@ const STRING_NAME_RESX: RegExp = /^[A-z_$][A-z0-9_$]*$/;
2022
export interface IResxReaderOptions {
2123
resxFilePath: string;
2224
terminal: Terminal;
25+
newlineNormalization: NewlineKind | undefined
2326
}
2427

2528
interface ILoggingFunctions {
@@ -33,9 +36,9 @@ interface IResxReaderOptionsInternal {
3336
resxFilePath: string;
3437
resxContents: string;
3538
loggingFunctions: ILoggingFunctions;
39+
newlineNormalization: NewlineKind | undefined
3640
}
3741

38-
3942
export class ResxReader {
4043
public static readResxFileAsLocFile(options: IResxReaderOptions): ILocalizationFile {
4144
const resxContents: string = FileSystem.readFile(options.resxFilePath);
@@ -71,7 +74,8 @@ export class ResxReader {
7174
return this._readResxAsLocFileInternal({
7275
resxFilePath: options.resxFilePath,
7376
resxContents,
74-
loggingFunctions
77+
loggingFunctions,
78+
newlineNormalization: options.newlineNormalization
7579
})
7680
}
7781

@@ -173,6 +177,9 @@ export class ResxReader {
173177
} else {
174178
foundValueElement = true;
175179
value = ResxReader._readTextElement(options, childNode);
180+
if (value && options.newlineNormalization) {
181+
value = Text.convertTo(value, options.newlineNormalization);
182+
}
176183
}
177184

178185
break;

0 commit comments

Comments
 (0)