From e83d7cb2d309625e5f3de545df9cf1a0343d238c Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Fri, 4 Jun 2021 15:52:16 -0700 Subject: [PATCH] refactor(compiler-cli): support xi18n in ngtsc (#42485) xi18n is the operation of extracting i18n messages from templates in the compilation. Previously, only View Engine was able to perform xi18n. This commit implements xi18n in the Ivy compiler, and a copy of the View Engine test for Ivy verifies that the results are identical. PR Close #42485 --- .../compiler-cli/compiler_options.d.ts | 3 + .../src/ngtsc/annotations/BUILD.bazel | 1 + .../src/ngtsc/annotations/src/component.ts | 8 + .../compiler-cli/src/ngtsc/core/BUILD.bazel | 1 + .../src/ngtsc/core/api/src/public_options.ts | 16 + .../src/ngtsc/core/src/compiler.ts | 11 + packages/compiler-cli/src/ngtsc/program.ts | 32 +- .../src/ngtsc/transform/BUILD.bazel | 1 + .../src/ngtsc/transform/src/api.ts | 7 + .../src/ngtsc/transform/src/compilation.ts | 20 ++ .../compiler-cli/src/ngtsc/xi18n/BUILD.bazel | 15 + .../compiler-cli/src/ngtsc/xi18n/index.ts | 9 + .../src/ngtsc/xi18n/src/context.ts | 27 ++ packages/compiler-cli/src/transformers/api.ts | 7 - packages/compiler-cli/test/ngtsc/env.ts | 16 + .../compiler-cli/test/ngtsc/xi18n_spec.ts | 334 ++++++++++++++++++ 16 files changed, 497 insertions(+), 11 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/xi18n/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/xi18n/index.ts create mode 100644 packages/compiler-cli/src/ngtsc/xi18n/src/context.ts create mode 100644 packages/compiler-cli/test/ngtsc/xi18n_spec.ts diff --git a/goldens/public-api/compiler-cli/compiler_options.d.ts b/goldens/public-api/compiler-cli/compiler_options.d.ts index e972a3e99c..2c3fb6bda2 100644 --- a/goldens/public-api/compiler-cli/compiler_options.d.ts +++ b/goldens/public-api/compiler-cli/compiler_options.d.ts @@ -7,6 +7,9 @@ export interface I18nOptions { enableI18nLegacyMessageIdFormat?: boolean; i18nInLocale?: string; i18nNormalizeLineEndingsInICUs?: boolean; + i18nOutFile?: string; + i18nOutFormat?: string; + i18nOutLocale?: string; i18nUseExternalIds?: boolean; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index b10a14584e..fed9544064 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -27,6 +27,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/ngtsc/typecheck/diagnostics", "//packages/compiler-cli/src/ngtsc/util", + "//packages/compiler-cli/src/ngtsc/xi18n", "@npm//@types/node", "@npm//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index c497ab6564..3ec40f3265 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -25,6 +25,7 @@ import {ComponentScopeReader, LocalModuleScopeRegistry, TypeCheckScopeRegistry} import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform'; import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck/api'; import {SubsetOfKeys} from '../../util/src/typescript'; +import {Xi18nContext} from '../../xi18n'; import {ResourceLoader} from './api'; import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagnostics} from './diagnostics'; @@ -837,6 +838,13 @@ export class ComponentDecoratorHandler implements return {data}; } + xi18n(ctx: Xi18nContext, node: ClassDeclaration, analysis: Readonly): + void { + ctx.updateFromTemplate( + analysis.template.content, analysis.template.declaration.resolvedTemplateUrl, + analysis.template.interpolationConfig ?? DEFAULT_INTERPOLATION_CONFIG); + } + updateResources(node: ClassDeclaration, analysis: ComponentAnalysisData): void { const containingFile = node.getSourceFile().fileName; diff --git a/packages/compiler-cli/src/ngtsc/core/BUILD.bazel b/packages/compiler-cli/src/ngtsc/core/BUILD.bazel index 03f7b7aef0..04122d3613 100644 --- a/packages/compiler-cli/src/ngtsc/core/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/core/BUILD.bazel @@ -38,6 +38,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/ngtsc/typecheck/diagnostics", "//packages/compiler-cli/src/ngtsc/util", + "//packages/compiler-cli/src/ngtsc/xi18n", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts b/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts index 7af887196d..9a3d1cb2bd 100644 --- a/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts +++ b/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts @@ -309,6 +309,22 @@ export interface I18nOptions { */ i18nInLocale?: string; + /** + * Export format (xlf, xlf2 or xmb) when the xi18n operation is requested. + */ + i18nOutFormat?: string; + + /** + * Path to the extracted message file to emit when the xi18n operation is requested. + */ + i18nOutFile?: string; + + + /** + * Locale of the application (used when xi18n is requested). + */ + i18nOutLocale?: string; + /** * Render `$localize` messages with legacy format ids. * diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index f7c2c605a6..fa95ac48e9 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -31,6 +31,7 @@ import {aliasTransformFactory, CompilationMode, declarationTransformFactory, Dec import {TemplateTypeCheckerImpl} from '../../typecheck'; import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig} from '../../typecheck/api'; import {getSourceFileOrNull, isDtsPath, resolveModuleName, toUnredirectedSourceFile} from '../../util/src/typescript'; +import {Xi18nContext} from '../../xi18n'; import {LazyRoute, NgCompilerAdapter, NgCompilerOptions} from '../api'; import {compileUndecoratedClassesWithAngularFeatures} from './config'; @@ -674,6 +675,16 @@ export class NgCompiler { return generateAnalysis(context); } + /** + * Collect i18n messages into the `Xi18nContext`. + */ + xi18n(ctx: Xi18nContext): void { + // Note that the 'resolve' phase is not strictly necessary for xi18n, but this is not currently + // optimized. + const compilation = this.ensureAnalyzed(); + compilation.traitCompiler.xi18n(ctx); + } + private ensureAnalyzed(this: NgCompiler): LazyCompilationState { if (this.compilation === null) { this.analyzeSync(); diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 8fab53d43a..2f967a3f9c 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -6,15 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {GeneratedFile} from '@angular/compiler'; +import {GeneratedFile, HtmlParser, MessageBundle} from '@angular/compiler'; import * as ts from 'typescript'; import * as api from '../transformers/api'; +import {i18nExtract} from '../transformers/i18n'; import {verifySupportedTypeScriptVersion} from '../typescript_support'; import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, NgCompilerHost} from './core'; import {NgCompilerOptions} from './core/api'; -import {absoluteFrom, AbsoluteFsPath, getFileSystem} from './file_system'; +import {absoluteFrom, AbsoluteFsPath, getFileSystem, resolve} from './file_system'; import {TrackedIncrementalBuildStrategy} from './incremental'; import {IndexedComponent} from './indexer'; import {ActivePerfRecorder, PerfCheckpoint as PerfCheckpoint, PerfEvent, PerfPhase} from './perf'; @@ -23,8 +24,6 @@ import {DeclarationNode} from './reflection'; import {retagAllTsFiles, untagAllTsFiles} from './shims'; import {OptimizeFor} from './typecheck/api'; - - /** * Entrypoint to the Angular Compiler (Ivy+) which sits behind the `api.Program` interface, allowing * it to be a drop-in replacement for the legacy View Engine compiler to tooling such as the @@ -227,6 +226,14 @@ export class NgtscProgram implements api.Program { return this.compiler.listLazyRoutes(entryRoute); } + private emitXi18n(): void { + const ctx = new MessageBundle(new HtmlParser(), [], {}, this.options.i18nOutLocale ?? null); + this.compiler.xi18n(ctx); + i18nExtract( + this.options.i18nOutFormat ?? null, this.options.i18nOutFile ?? null, this.host, + this.options, ctx, resolve); + } + emit(opts?: { emitFlags?: api.EmitFlags|undefined; cancellationToken?: ts.CancellationToken | undefined; @@ -234,6 +241,23 @@ export class NgtscProgram implements api.Program { emitCallback?: api.TsEmitCallback | undefined; mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback | undefined; }|undefined): ts.EmitResult { + // Check if emission of the i18n messages bundle was requested. + if (opts !== undefined && opts.emitFlags !== undefined && + opts.emitFlags & api.EmitFlags.I18nBundle) { + this.emitXi18n(); + + // `api.EmitFlags` is a View Engine compiler concept. We only pay attention to the absence of + // the other flags here if i18n emit was requested (since this is usually done in the xi18n + // flow, where we don't want to emit JS at all). + if (!(opts.emitFlags & api.EmitFlags.JS)) { + return { + diagnostics: [], + emitSkipped: true, + emittedFiles: [], + }; + } + } + this.compiler.perfRecorder.memory(PerfCheckpoint.PreEmit); const res = this.compiler.perfRecorder.inPhase(PerfPhase.TypeScriptEmit, () => { diff --git a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel index f0d4e36d21..86d831269d 100644 --- a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel @@ -20,6 +20,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/ngtsc/util", + "//packages/compiler-cli/src/ngtsc/xi18n", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 01054b5f71..4387a651c5 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -15,6 +15,7 @@ import {IndexingContext} from '../../indexer'; import {ClassDeclaration, Decorator} from '../../reflection'; import {ImportManager} from '../../translator'; import {TypeCheckContext} from '../../typecheck/api'; +import {Xi18nContext} from '../../xi18n'; /** * Specifies the compilation mode that is used for the compilation. @@ -176,6 +177,12 @@ export interface DecoratorHandler { */ resolve?(node: ClassDeclaration, analysis: Readonly, symbol: S): ResolveResult; + /** + * Extract i18n messages into the `Xi18nContext`, which is useful for generating various formats + * of message file outputs. + */ + xi18n?(bundle: Xi18nContext, node: ClassDeclaration, analysis: Readonly): void; + typeCheck? (ctx: TypeCheckContext, node: ClassDeclaration, analysis: Readonly, resolution: Readonly): void; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index a189f5d4a9..a5bf2798ae 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -17,6 +17,7 @@ import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost} from '../../reflection'; import {ProgramTypeCheckAdapter, TypeCheckContext} from '../../typecheck/api'; import {getSourceFile, isExported} from '../../util/src/typescript'; +import {Xi18nContext} from '../../xi18n'; import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, HandlerFlags, HandlerPrecedence, ResolveResult} from './api'; import {DtsTransformRegistry} from './declaration'; @@ -494,6 +495,25 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } } + xi18n(bundle: Xi18nContext): void { + for (const clazz of this.classes.keys()) { + const record = this.classes.get(clazz)!; + for (const trait of record.traits) { + if (trait.state !== TraitState.Analyzed && trait.state !== TraitState.Resolved) { + // Skip traits that haven't been analyzed successfully. + continue; + } else if (trait.handler.xi18n === undefined) { + // Skip traits that don't support xi18n. + continue; + } + + if (trait.analysis !== null) { + trait.handler.xi18n(bundle, clazz, trait.analysis); + } + } + } + } + updateResources(clazz: DeclarationNode): void { if (!this.reflector.isClass(clazz) || !this.classes.has(clazz)) { return; diff --git a/packages/compiler-cli/src/ngtsc/xi18n/BUILD.bazel b/packages/compiler-cli/src/ngtsc/xi18n/BUILD.bazel new file mode 100644 index 0000000000..4097839c15 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/xi18n/BUILD.bazel @@ -0,0 +1,15 @@ +load("//tools:defaults.bzl", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "xi18n", + srcs = glob([ + "*.ts", + "src/**/*.ts", + ]), + deps = [ + "//packages/compiler", + "@npm//typescript", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/xi18n/index.ts b/packages/compiler-cli/src/ngtsc/xi18n/index.ts new file mode 100644 index 0000000000..aebb55aca9 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/xi18n/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './src/context'; diff --git a/packages/compiler-cli/src/ngtsc/xi18n/src/context.ts b/packages/compiler-cli/src/ngtsc/xi18n/src/context.ts new file mode 100644 index 0000000000..7a3f0a90af --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/xi18n/src/context.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {InterpolationConfig} from '@angular/compiler'; + +/** + * Captures template information intended for extraction of i18n messages from a template. + * + * This interface is compatible with the View Engine compiler's `MessageBundle` class, which is used + * to implement xi18n for VE. Due to the dependency graph of ngtsc, an interface is needed as it + * can't depend directly on `MessageBundle`. + */ +export interface Xi18nContext { + /** + * Capture i18n messages from the template. + * + * In `MessageBundle` itself, this returns any `ParseError`s from the template. In this interface, + * the return type is declared as `void` for simplicity, since any parse errors would be reported + * as diagnostics anyway. + */ + updateFromTemplate(html: string, url: string, interpolationConfig: InterpolationConfig): void; +} diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index bb10b826c7..e9d39f4810 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -91,13 +91,6 @@ export interface CompilerOptions extends NgCompilerOptions, ts.CompilerOptions { // position. disableExpressionLowering?: boolean; - // Locale of the application - i18nOutLocale?: string; - // Export format (xlf, xlf2 or xmb) - i18nOutFormat?: string; - // Path to the extracted message file - i18nOutFile?: string; - // Import format if different from `i18nFormat` i18nInFormat?: string; // Path to the translation file diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index b456573d72..27db4c96a8 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -11,6 +11,7 @@ import * as api from '@angular/compiler-cli/src/transformers/api'; import * as ts from 'typescript'; import {createCompilerHost, createProgram} from '../../index'; +import {mainXi18n} from '../../src/extract_i18n'; import {main, mainDiagnosticsForTest, readNgcCommandLineAndConfiguration} from '../../src/main'; import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, relativeFrom} from '../../src/ngtsc/file_system'; import {Folder, MockFileSystem} from '../../src/ngtsc/file_system/testing'; @@ -275,6 +276,21 @@ export class NgtscTestEnvironment { const program = createProgram({rootNames, host, options}); return (program as NgtscProgram).getIndexedComponents(); } + + driveXi18n(format: string, outputFileName: string, locale: string|null = null): void { + const errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error); + const args = [ + ...this.commandLineArgs, + `--i18nFormat=${format}`, + `--outFile=${outputFileName}`, + ]; + if (locale !== null) { + args.push(`--locale=${locale}`); + } + const exitCode = mainXi18n(args, errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toEqual(0); + } } class AugmentedCompilerHost extends NgtscTestCompilerHost { diff --git a/packages/compiler-cli/test/ngtsc/xi18n_spec.ts b/packages/compiler-cli/test/ngtsc/xi18n_spec.ts new file mode 100644 index 0000000000..2fa94355bf --- /dev/null +++ b/packages/compiler-cli/test/ngtsc/xi18n_spec.ts @@ -0,0 +1,334 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; +import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; +import {platform} from 'os'; + +import {NgtscTestEnvironment} from './env'; + +const testFiles = loadStandardTestFiles({fakeCore: true, fakeCommon: true}); + +runInEachFileSystem(os => { + let env!: NgtscTestEnvironment; + + if (os === 'Windows' || platform() === 'win32') { + // xi18n tests are skipped on Windows as the paths in the expected message files are platform- + // sensitive. These tests will be deleted when xi18n is removed, so it's not a major priority + // to make them work with Windows. + return; + } + + describe('ngtsc xi18n', () => { + beforeEach(() => { + env = NgtscTestEnvironment.setup(testFiles); + env.tsconfig(); + writeTestCode(env); + }); + + it('should extract xmb', () => { + env.driveXi18n('xmb', 'messages.xmb'); + expect(env.getContents('messages.xmb')).toEqual(EXPECTED_XMB); + }); + + it('should extract xlf', () => { + // Note that only in XLF mode do we pass a locale into the extraction. + env.driveXi18n('xlf', 'messages.xlf', 'fr'); + expect(env.getContents('messages.xlf')).toEqual(EXPECTED_XLIFF); + }); + + it('should extract xlf', () => { + env.driveXi18n('xlf2', 'messages.xliff2.xlf'); + expect(env.getContents('messages.xliff2.xlf')).toEqual(EXPECTED_XLIFF2); + }); + + it('should not emit js', () => { + env.driveXi18n('xlf2', 'messages.xliff2.xlf'); + env.assertDoesNotExist('src/module.js'); + }); + }); +}); + +const EXPECTED_XMB = ` + + + + + + + + + + + + + + + + + + + +]> + + src/basic.html:1src/comp2.ts:1src/basic.html:1translate me + src/basic.html:3,4src/comp2.ts:3,4src/comp2.ts:2,3src/basic.html:3,4 + Welcome + src/icu.html:1,3src/icu.html:5{VAR_PLURAL, plural, =1 {book} other {books} } + src/icu.html:4,6 + foo { count, plural, =1 {...} other {...}}{ count, plural, =1 {...} other {...}} + + src/placeholders.html:1,3Name: <b><b>{{ + name // i18n(ph="name") + }}{{ + name // i18n(ph="name") + }}</b></b> + +`; + +const EXPECTED_XLIFF = ` + + + + + translate me + + src/basic.html + 1 + + + src/comp2.ts + 1 + + + src/basic.html + 1 + + desc + meaning + + + + Welcome + + src/basic.html + 3 + + + src/comp2.ts + 3 + + + src/comp2.ts + 2 + + + src/basic.html + 3 + + + + {VAR_PLURAL, plural, =1 {book} other {books} } + + src/icu.html + 1 + + with ICU + + + + foo + + + src/icu.html + 4 + + with ICU and other things + + + {VAR_PLURAL, plural, =1 {book} other {books} } + + src/icu.html + 5 + + + + Name: + + src/placeholders.html + 1 + + with placeholders + + + + +`; + +const EXPECTED_XLIFF2 = ` + + + + + desc + meaning + src/basic.html:1 + src/comp2.ts:1 + src/basic.html:1 + + + translate me + + + + + src/basic.html:3,4 + src/comp2.ts:3,4 + src/comp2.ts:2,3 + src/basic.html:3,4 + + + + Welcome + + + + + with ICU + src/icu.html:1,3 + src/icu.html:5 + + + {VAR_PLURAL, plural, =1 {book} other {books} } + + + + + with ICU and other things + src/icu.html:4,6 + + + + foo + + + + + + with placeholders + src/placeholders.html:1,3 + + + Name: + + + + +`; + +/** + * Note: the indentation here is load-bearing. + */ +function writeTestCode(env: NgtscTestEnvironment): void { + const welcomeMessage = ` + + Welcome + `; + env.write('src/basic.html', `
+

${welcomeMessage}

`); + + env.write('src/comp1.ts', ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'basic', + templateUrl: './basic.html', + }) + export class BasicCmp1 {}`); + + env.write('src/comp2.ts', ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'basic2', + template: \`
+

${welcomeMessage}

\`, + }) + export class BasicCmp2 {} + @Component({ + selector: 'basic4', + template: \`

${welcomeMessage}

\`, + }) + export class BasicCmp4 {}`); + + env.write('src/comp3.ts', ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'basic3', + templateUrl: './basic.html', + }) + export class BasicCmp3 {}`); + + env.write('src/placeholders.html', `
Name: {{ + name // i18n(ph="name") + }}
`); + + env.write('src/placeholder_cmp.ts', ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'placeholders', + templateUrl: './placeholders.html', + }) + export class PlaceholderCmp { name = 'whatever'; }`); + + env.write('src/icu.html', `
{ + count, plural, =1 {book} other {books} + }
+
+ foo { count, plural, =1 {book} other {books} } +
`); + + env.write('src/icu_cmp.ts', ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'icu', + templateUrl: './icu.html', + }) + export class IcuCmp { count = 3; }`); + + env.write('src/module.ts', ` + import {NgModule} from '@angular/core'; + import {CommonModule} from '@angular/common'; + import {BasicCmp1} from './comp1'; + import {BasicCmp2, BasicCmp4} from './comp2'; + import {BasicCmp3} from './comp3'; + import {PlaceholderCmp} from './placeholder_cmp'; + import {IcuCmp} from './icu_cmp'; + + @NgModule({ + declarations: [ + BasicCmp1, + BasicCmp2, + BasicCmp3, + BasicCmp4, + PlaceholderCmp, + IcuCmp, + ], + imports: [CommonModule], + }) + export class I18nModule {} + `); +}