diff --git a/README.md b/README.md index e062446..d4ad935 100644 --- a/README.md +++ b/README.md @@ -38,22 +38,22 @@ ng extract-i18n # yes, same as before - this replaces the original builder In your `angular.json` the target `extract-i18n` that can be configured with the following options: -| Name | Default | Description | -|------------------------------|-------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `browserTarget` | Inferred from current setup by `ng add` | A browser builder target to extract i18n messages in the format of `project:target[:configuration]`. See https://angular.io/cli/extract-i18n#options | -| `format` | Inferred from current setup by `ng add` | Any of `xlf`, `xlif`, `xliff`, `xlf2`, `xliff2` | -| `outputPath` | Inferred from current setup by `ng add` | Path to folder containing all (source and target) translation files. | -| `targetFiles` | Inferred from current setup by `ng add` | Filenames (relative to `outputPath` of all target translation files (e.g. `["messages.fr.xlf", "messages.de.xlf"]`). | -| `sourceLanguageTargetFile` | Unused | If this is set (to one of the `targetFiles`), new translations in that target file will be set to `state="final"` (instead of default `state="new"`). | -| `sourceFile` | `messages.xlf`. `ng add` tries to infer non default setups. | Filename (relative to `outputPath` of source translation file (e.g. `"translations-source.xlf"`). | -| `removeIdsWithPrefix` | `[]` | List of prefix strings. All translation units with matching `id` attribute are removed. Useful for excluding duplicate library translations. | -| `fuzzyMatch` | `true` | Whether translation units without matching IDs are fuzzy matched by source text. | -| `resetTranslationState` | `true` | Reset the translation state to new/initial for new/changed units. | -| `collapseWhitespace` | `true` | Collapsing of multiple whitespaces/line breaks in translation sources and targets. | -| `trim` | `false` | Trim translation sources and targets. | -| `includeContext` | `false` | Whether to include the context information (like notes) in the translation files. This is useful for sending the target translation files to translation agencies/services. | -| `newTranslationTargetsBlank` | `false` | When `false` (default) the "target" of new translation units is set to the "source" value. When `true`, an empty string is used. When `'omit'`, no target element is created. | -| `sort` | `"stableAppendNew"` | Sorting of all translation units in source and target translation files. Supported: `"idAsc"` (sort by translation IDs), `"stableAppendNew"` (keep existing sorting, append new translations at the end) | +| Name | Default | Description | +|------------------------------|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `browserTarget` | Inferred from current setup by `ng add` | A browser builder target to extract i18n messages in the format of `project:target[:configuration]`. See https://angular.io/cli/extract-i18n#options | +| `format` | Inferred from current setup by `ng add` | Any of `xlf`, `xlif`, `xliff`, `xlf2`, `xliff2` | +| `outputPath` | Inferred from current setup by `ng add` | Path to folder containing all (source and target) translation files. | +| `targetFiles` | Inferred from current setup by `ng add` | Filenames (relative to `outputPath` of all target translation files (e.g. `["messages.fr.xlf", "messages.de.xlf"]`). | +| `sourceLanguageTargetFile` | Unused | If this is set (to one of the `targetFiles`), new translations in that target file will be set to `state="final"` (instead of default `state="new"`). | +| `sourceFile` | `messages.xlf`. `ng add` tries to infer non default setups. | Filename (relative to `outputPath` of source translation file (e.g. `"translations-source.xlf"`). | +| `removeIdsWithPrefix` | `[]` | List of prefix strings. All translation units with matching `id` attribute are removed. Useful for excluding duplicate library translations. | +| `fuzzyMatch` | `true` | Whether translation units without matching IDs are fuzzy matched by source text. | +| `resetTranslationState` | `true` | Reset the translation state to new/initial for new/changed units. | +| `collapseWhitespace` | `true` | Collapsing of multiple whitespaces/line breaks in translation sources and targets. | +| `trim` | `false` | Trim translation sources and targets. | +| `includeContext` | `false` | Whether to include the context information (like notes) in the translation files. This is useful for sending the target translation files to translation agencies/services. When `sourceFileOnly` the context is retained only in the `sourceFile`. | +| `newTranslationTargetsBlank` | `false` | When `false` (default) the "target" of new translation units is set to the "source" value. When `true`, an empty string is used. When `'omit'`, no target element is created. | +| `sort` | `"stableAppendNew"` | Sorting of all translation units in source and target translation files. Supported: `"idAsc"` (sort by translation IDs), `"stableAppendNew"` (keep existing sorting, append new translations at the end) | ## Contribute diff --git a/src/builder.spec.ts b/src/builder.spec.ts index 70f6641..e780160 100644 --- a/src/builder.spec.ts +++ b/src/builder.spec.ts @@ -675,6 +675,55 @@ describe('Builder', () => { }); }); + test('extract-and-merge xlf 1.2 with newTranslationTargetsBlank=omit', async () => { + await runTest( + { + messagesBefore: '\n' + + ' \n' + + ' \n' + + ' \n' + + ' source val\n' + + ' \n' + + ' \n' + + ' source val2\n' + + ' \n' + + ' \n' + + ' \n' + + '', + messagesFrBefore: '\n' + + ' \n' + + ' \n' + + ' \n' + + ' source val\n' + + ' target val\n' + + ' \n' + + ' \n' + + ' \n' + + '', + options: { + format: 'xlf', + targetFiles: ['messages.fr.xlf'], + sourceLanguageTargetFile: "messages.fr.xlf", + outputPath: 'builder-test', + removeIdsWithPrefix: ['removeMe'], + newTranslationTargetsBlank: 'omit' + }, + messagesFrExpected: '\n' + + ' \n' + + ' \n' + + ' \n' + + ' source val\n' + + ' target val\n' + + ' \n' + + ' \n' + + ' source val2\n' + + ' \n' + + ' \n' + + ' \n' + + '' + }); + }); + test('extract-and-merge with xml definition without newline', async () => { await runTest( { @@ -1102,7 +1151,7 @@ describe('Builder', () => { ' \n' + ' \n' + ' \n' + - '' + ''; test('add new context groups', async () => { await runTest( { @@ -1243,6 +1292,69 @@ describe('Builder', () => { }); }); + test('retain context in sourceFile only, when includeContext=sourceFileOnly', async () => { + const messagesBefore = '\n' + + ' \n' + + ' \n' + + ' \n' + + ' Some text\n' + + ' \n' + + ' src/app/app-routing.module.ts\n' + + ' 12\n' + + ' \n' + + ' \n' + + ' \n' + + ' Some text2\n' + + ' \n' + + ' src/app/app.component.html\n' + + ' 4\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ''; + await runTest( + { + messagesBefore: messagesBefore, + messagesFrBefore: '\n' + + ' \n' + + ' \n' + + ' \n' + + ' Some text\n' + + ' Some text\n' + + ' \n' + + ' src/app/app-routing.module.ts\n' + + ' 12\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '', + options: { + format: 'xlf', + targetFiles: ['messages.fr.xlf'], + includeContext: 'sourceFileOnly', + outputPath: 'builder-test' + }, + messagesExpected: messagesBefore, + messagesFrExpected: '\n' + + ' \n' + + ' \n' + + ' \n' + + ' Some text\n' + + ' Some text\n' + + ' \n' + + ' \n' + + ' Some text2\n' + + ' Some text2\n' + + ' \n' + + ' \n' + + ' \n' + + '' + } + ); + }); + test('retain whitespace between interpolations', async () => { await runTest( { diff --git a/src/builder.ts b/src/builder.ts index 24ac663..a4beb14 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -18,7 +18,7 @@ export interface Options extends JsonObject { resetTranslationState: boolean, collapseWhitespace: boolean, trim: boolean, - includeContext: boolean, + includeContext: boolean | 'sourceFileOnly', newTranslationTargetsBlank: boolean | 'omit', sort: 'idAsc' | 'stableAppendNew', browserTarget: string @@ -92,9 +92,17 @@ async function extractI18nMergeBuilder(options: Options, context: BuilderContext context.logger.info(`normalize ${sourcePath} ...`); const translationSourceFile = await fs.readFile(sourcePath, 'utf8'); - const removePaths = [ - ...(options.includeContext ? [] : [isXliffV2 ? '/xliff/file/unit/notes' : '/xliff/file/body/trans-unit/context-group']), - ...(options.removeIdsWithPrefix ?? []).map(removePrefix => isXliffV2 ? `/xliff/file/unit[starts-with(@id,"${removePrefix}")]` : `/xliff/file/body/trans-unit[starts-with(@id,"${removePrefix}")]`) + + const removeIdsWithPrefixPaths = (options.removeIdsWithPrefix ?? []).map(removePrefix => isXliffV2 ? `/xliff/file/unit[starts-with(@id,"${removePrefix}")]` : `/xliff/file/body/trans-unit[starts-with(@id,"${removePrefix}")]`); + const removeContextPaths = (includeContext: boolean) => includeContext ? [] : [isXliffV2 ? '/xliff/file/unit/notes' : '/xliff/file/body/trans-unit/context-group']; + + const removePathsSourceFile = [ + ...(removeContextPaths(options.includeContext === true || options.includeContext === 'sourceFileOnly')), + ...removeIdsWithPrefixPaths + ]; + const removePathsTargetFiles = [ + ...(removeContextPaths(options.includeContext === true)), + ...removeIdsWithPrefixPaths ]; const idPath = isXliffV2 ? '/xliff/file/unit/@id' : '/xliff/file/body/trans-unit/@id'; const sort: Options['sort'] = options.sort ?? 'stableAppendNew'; @@ -103,7 +111,7 @@ async function extractI18nMergeBuilder(options: Options, context: BuilderContext trim: options.trim ?? false, normalizeWhitespace: options.collapseWhitespace ?? true, sortPath: sort === 'idAsc' ? idPath : undefined, - removePath: removePaths + removePath: removePathsSourceFile }); let idMapping: { [id: string]: string } = {}; @@ -123,7 +131,7 @@ async function extractI18nMergeBuilder(options: Options, context: BuilderContext normalizeWhitespace: options.collapseWhitespace, // no sorting for 'stableAppendNew' as this is the default merge behaviour: sortPath: sort === 'idAsc' ? idPath : undefined, - removePath: removePaths + removePath: removePathsTargetFiles }); await fs.writeFile(targetPath, normalizedTarget); idMapping = {...idMapping, ...mapping}; diff --git a/src/schema.json b/src/schema.json index 8a66136..5c98119 100644 --- a/src/schema.json +++ b/src/schema.json @@ -84,9 +84,18 @@ "description": "Trim translation sources and targets." }, "includeContext": { - "type": "boolean", + "anyOf": [ + { + "type": "boolean" + }, + { + "enum": [ + "sourceFileOnly" + ] + } + ], "default": false, - "description": "Whether to include the context information (like notes) in the translation files. This is useful for sending the target translation files to translation agencies/services." + "description": "Whether to include the context information (like notes) in the translation files. This is useful for sending the target translation files to translation agencies/services. When `sourceFileOnly` the context is retained only in the `sourceFile`." }, "browserTarget": { "type": "string",