diff --git a/integration/ivy-i18n/e2e/de/app.e2e-spec.ts b/integration/ivy-i18n/e2e/de/app.e2e-spec.ts index 0bc71b5b29..e147aa8348 100644 --- a/integration/ivy-i18n/e2e/de/app.e2e-spec.ts +++ b/integration/ivy-i18n/e2e/de/app.e2e-spec.ts @@ -15,7 +15,13 @@ describe('cli-hello-world-ivy App', () => { expect(page.getParagraph('message')).toEqual('Willkommen in der i18n App. (inline)'); }); - it('should display the locale', () => { expect(page.getParagraph('locale')).toEqual('de'); }); + it('should display extra message', () => { + expect(page.getParagraph('extra')).toEqual('Zusätzliche Nachricht'); + }); + + it('should display the locale', () => { + expect(page.getParagraph('locale')).toEqual('de'); + }); // TODO : Re-enable when CLI translation inlining supports locale inlining (and so we can use it // to load the correct locale data) diff --git a/integration/ivy-i18n/e2e/en/app.e2e-spec.ts b/integration/ivy-i18n/e2e/en/app.e2e-spec.ts index b7935f18cb..66d2d1b7f9 100644 --- a/integration/ivy-i18n/e2e/en/app.e2e-spec.ts +++ b/integration/ivy-i18n/e2e/en/app.e2e-spec.ts @@ -7,13 +7,21 @@ describe('cli-hello-world-ivy App', () => { page.navigateTo(); }); - it('should display title', - () => { expect(page.getHeading()).toEqual('Hello cli-hello-world-ivy-compat!'); }); + it('should display title', () => { + expect(page.getHeading()).toEqual('Hello cli-hello-world-ivy-compat!'); + }); - it('should display welcome message', - () => { expect(page.getParagraph('message')).toEqual('Welcome to the i18n app.'); }); + it('should display welcome message', () => { + expect(page.getParagraph('message')).toEqual('Welcome to the i18n app.'); + }); - it('should display the locale', () => { expect(page.getParagraph('locale')).toEqual('en-US'); }); + it('should display extra message', () => { + expect(page.getParagraph('extra')).toEqual('Extra message'); + }); + + it('should display the locale', () => { + expect(page.getParagraph('locale')).toEqual('en-US'); + }); it('the date pipe should show the localized month', () => { page.navigateTo(); diff --git a/integration/ivy-i18n/e2e/fr/app.e2e-spec.ts b/integration/ivy-i18n/e2e/fr/app.e2e-spec.ts index 795b80e169..93876089bb 100644 --- a/integration/ivy-i18n/e2e/fr/app.e2e-spec.ts +++ b/integration/ivy-i18n/e2e/fr/app.e2e-spec.ts @@ -7,14 +7,21 @@ describe('cli-hello-world-ivy App', () => { page.navigateTo(); }); - it('should display title', - () => { expect(page.getHeading()).toEqual('Bonjour, cli-hello-world-ivy-compat! (inline)'); }); + it('should display title', () => { + expect(page.getHeading()).toEqual('Bonjour, cli-hello-world-ivy-compat! (inline)'); + }); it('should display welcome message', () => { expect(page.getParagraph('message')).toEqual('Bienvenue sur l\'application i18n. (inline)'); }); - it('should display the locale', () => { expect(page.getParagraph('locale')).toEqual('fr'); }); + it('should display extra message', () => { + expect(page.getParagraph('extra')).toEqual('Message supplémentaire'); + }); + + it('should display the locale', () => { + expect(page.getParagraph('locale')).toEqual('fr'); + }); it('the date pipe should show the localized month', () => { page.navigateTo(); diff --git a/integration/ivy-i18n/package.json b/integration/ivy-i18n/package.json index c3ecf05c23..1d8cf1a87c 100644 --- a/integration/ivy-i18n/package.json +++ b/integration/ivy-i18n/package.json @@ -11,7 +11,7 @@ "start": "ng serve", "pretest": "ng version", "test": "ng test && yarn e2e --configuration=ci && yarn e2e --configuration=ci-production && yarn translated:test && yarn translated:legacy-xlf:test && yarn translated:legacy-xmb:test", - "translate": "localize-translate -r \"dist/\" -s \"**/*\" -l \"en-US\" -t \"src/locales/messages.de.json\" \"src/locales/messages.fr.json\" -o \"../tmp/translations/{{LOCALE}}\"", + "translate": "localize-translate -r \"dist/\" -s \"**/*\" -l \"en-US\" -t \"[src/locales/messages.de.json, src/locales/extra.de.json]\" [src/locales/messages.fr.json,src/locales/extra.fr.json] -o \"../tmp/translations/{{LOCALE}}\"", "runtime:test": "yarn e2e --configuration=runtime-translations", "translated:test": "yarn build && yarn translate && yarn translated:fr:e2e && yarn translated:de:e2e && yarn translated:en:e2e", "translated:fr:serve": "serve ../tmp/translations/fr --listen 4200", @@ -72,4 +72,4 @@ "resolutions": { "**/webdriver-manager": "file:../../node_modules/webdriver-manager" } -} +} \ No newline at end of file diff --git a/integration/ivy-i18n/src/app/app.component.html b/integration/ivy-i18n/src/app/app.component.html index eb7616861d..819c20e303 100644 --- a/integration/ivy-i18n/src/app/app.component.html +++ b/integration/ivy-i18n/src/app/app.component.html @@ -1,25 +1,10 @@ - -
+

Hello {{ title }}!

-

{{ message }}

- Angular Logo +

{{extra}}

{{ locale }}

{{ 1 | percent }} awesome

-

{{ jan | date : 'LLLL' }}

-

Here are some links to help you start:

- - +

{{ jan | date : 'LLLL' }}

\ No newline at end of file diff --git a/integration/ivy-i18n/src/app/app.component.ts b/integration/ivy-i18n/src/app/app.component.ts index c6aafcce33..ddf6cb97d0 100644 --- a/integration/ivy-i18n/src/app/app.component.ts +++ b/integration/ivy-i18n/src/app/app.component.ts @@ -5,6 +5,7 @@ import {Component, Inject, LOCALE_ID} from '@angular/core'; export class AppComponent { constructor(@Inject(LOCALE_ID) public locale: string) {} title = `cli-hello-world-ivy-compat`; - message = $localize `Welcome to the i18n app.`; + message = $localize`Welcome to the i18n app.`; jan = new Date(2000, 0, 1); + extra = $localize`:@@custom:Extra message`; } diff --git a/integration/ivy-i18n/src/locales/extra.de.json b/integration/ivy-i18n/src/locales/extra.de.json new file mode 100644 index 0000000000..ce3cbf509c --- /dev/null +++ b/integration/ivy-i18n/src/locales/extra.de.json @@ -0,0 +1,6 @@ +{ + "locale": "de", + "translations": { + "custom": "Zusätzliche Nachricht" + } +} \ No newline at end of file diff --git a/integration/ivy-i18n/src/locales/extra.fr.json b/integration/ivy-i18n/src/locales/extra.fr.json new file mode 100644 index 0000000000..10c9704f7c --- /dev/null +++ b/integration/ivy-i18n/src/locales/extra.fr.json @@ -0,0 +1,6 @@ +{ + "locale": "fr", + "translations": { + "custom": "Message supplémentaire" + } +} \ No newline at end of file diff --git a/integration/ivy-i18n/yarn.lock b/integration/ivy-i18n/yarn.lock index db8afee3a5..45fd3a3efa 100644 --- a/integration/ivy-i18n/yarn.lock +++ b/integration/ivy-i18n/yarn.lock @@ -2,25 +2,25 @@ # yarn lockfile v1 -"@angular-devkit/architect@0.900.0-rc.11": - version "0.900.0-rc.11" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.900.0-rc.11.tgz#e9f3e5e372d467a220027cf53231b88e8e857fbc" - integrity sha512-rRbq4ipppnY4FvVo89Cv+yC7rlt1/VFE/jaB77Ra2tI6zVlFWCTjnMzuc9TYz/3jK1ssThzgEA2sebPDmjH47w== +"@angular-devkit/architect@0.900.3": + version "0.900.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.900.3.tgz#9c396733abd12fbb1d5bbc4542b2ee52418adb02" + integrity sha512-4UHc58Dlc5XHY3eiYSX9gytLyPNYixGSRwLcc/LRwuPgrmUFKPzCN3nwgB+9kc03/HN89CsJ1rS1scid6N6vxQ== dependencies: - "@angular-devkit/core" "9.0.0-rc.11" + "@angular-devkit/core" "9.0.3" rxjs "6.5.3" "@angular-devkit/build-angular@file:../../node_modules/@angular-devkit/build-angular": - version "0.900.0-rc.11" + version "0.900.3" dependencies: - "@angular-devkit/architect" "0.900.0-rc.11" - "@angular-devkit/build-optimizer" "0.900.0-rc.11" - "@angular-devkit/build-webpack" "0.900.0-rc.11" - "@angular-devkit/core" "9.0.0-rc.11" + "@angular-devkit/architect" "0.900.3" + "@angular-devkit/build-optimizer" "0.900.3" + "@angular-devkit/build-webpack" "0.900.3" + "@angular-devkit/core" "9.0.3" "@babel/core" "7.7.7" "@babel/generator" "7.7.7" "@babel/preset-env" "7.7.7" - "@ngtools/webpack" "9.0.0-rc.11" + "@ngtools/webpack" "9.0.3" ajv "6.10.2" autoprefixer "9.7.1" babel-loader "8.0.6" @@ -75,10 +75,10 @@ webpack-subresource-integrity "1.3.4" worker-plugin "3.2.0" -"@angular-devkit/build-optimizer@0.900.0-rc.11": - version "0.900.0-rc.11" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.900.0-rc.11.tgz#96c2446fa9cd2e90700ab8a68312b28b3907f6d9" - integrity sha512-GJC+7H7ER6bxDC2UdAGwW357EYHpv8ISKKmS19wdJV5gZPMPANcpbg9FIpl27SDhUyZX9C2DOrcATvYYFoYgDQ== +"@angular-devkit/build-optimizer@0.900.3": + version "0.900.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.900.3.tgz#91f90c56affb0be9f7910dfc1d414f16c21c2c3f" + integrity sha512-VLAWtAXpOzOoYUJrN6sT90UdIdvrVIipkzGz7nfI1kscDvxUFwVZnsNNHtFinaY2SfZAunHhYQOA/B9FJ8WPdQ== dependencies: loader-utils "1.2.3" source-map "0.7.3" @@ -86,19 +86,19 @@ typescript "3.6.4" webpack-sources "1.4.3" -"@angular-devkit/build-webpack@0.900.0-rc.11": - version "0.900.0-rc.11" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.900.0-rc.11.tgz#d9a91c2b67a629f6adfe87980d26e495f2e30e0a" - integrity sha512-utBAnkO6WLi323Rto1s7TJpaDRqDNR8jkD0C0PG5Zm3y1U9ARbAjTkugkrB/7bc4gEIqWZD+1dLYaaJCidye2Q== +"@angular-devkit/build-webpack@0.900.3": + version "0.900.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.900.3.tgz#4a2fd13cebe190c091606e18397a1f7cccfab6bb" + integrity sha512-9gSTLWf7yq/XBOec0CtZcjNMsC7L8IuVDProBQHps2SvTfr982DtHfEge95J2lc9BjRbqidv+phImFsQ1J3mFA== dependencies: - "@angular-devkit/architect" "0.900.0-rc.11" - "@angular-devkit/core" "9.0.0-rc.11" + "@angular-devkit/architect" "0.900.3" + "@angular-devkit/core" "9.0.3" rxjs "6.5.3" -"@angular-devkit/core@9.0.0-rc.11": - version "9.0.0-rc.11" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-9.0.0-rc.11.tgz#9e69545eb21284a573ad78e4c33003f2ea25afd5" - integrity sha512-ki7Sln+mQdCctJNBalzy70tiFn2hOCY2Yyte8B0xKWVHnofZySvG+ANzoLgodnKFOBH18AQy35FhgzZM++N9tQ== +"@angular-devkit/core@9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-9.0.3.tgz#a027862d2edd981afcc6245176e9f27768c631c9" + integrity sha512-3+abmv9K9d+BVgUAolYgoOqlGAA2Jb1pWo2biapSDG6KjUZHUCJdnsKigLtLorCdv0SrjTp56FFplkcqKsFQgA== dependencies: ajv "6.10.2" fast-json-stable-stringify "2.0.0" @@ -106,26 +106,26 @@ rxjs "6.5.3" source-map "0.7.3" -"@angular-devkit/schematics@9.0.0-rc.11": - version "9.0.0-rc.11" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-9.0.0-rc.11.tgz#e0d4d271d8d783ebf05eced576262f20e6c3562c" - integrity sha512-aJqOLzsoAkVj3AVTf1ehH2hA9wHHz1+7TTtfqI+Yx+S3jFyvGmnKrNBCKtMuIV5JdEHiXmhhuGbNBHwRFWpOow== +"@angular-devkit/schematics@9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-9.0.3.tgz#e65fa1ce08a3d5ef0af594b623024439c1110a0d" + integrity sha512-BQnZtFQPLZZOijhuEndtzL6cOnhaE8nNxupkRHavWohOMStnLsRyvVJj6JVDkf37wvT5koqTNjHhbdMxcCRc6A== dependencies: - "@angular-devkit/core" "9.0.0-rc.11" + "@angular-devkit/core" "9.0.3" ora "4.0.2" rxjs "6.5.3" "@angular/animations@file:../../dist/packages-dist/animations": - version "9.0.0-rc.1" + version "10.0.0-next.2" "@angular/cli@file:../../node_modules/@angular/cli": - version "9.0.0-rc.11" + version "9.0.3" dependencies: - "@angular-devkit/architect" "0.900.0-rc.11" - "@angular-devkit/core" "9.0.0-rc.11" - "@angular-devkit/schematics" "9.0.0-rc.11" - "@schematics/angular" "9.0.0-rc.11" - "@schematics/update" "0.900.0-rc.11" + "@angular-devkit/architect" "0.900.3" + "@angular-devkit/core" "9.0.3" + "@angular-devkit/schematics" "9.0.3" + "@schematics/angular" "9.0.3" + "@schematics/update" "0.900.3" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.1" debug "^4.1.1" @@ -143,10 +143,10 @@ uuid "^3.3.2" "@angular/common@file:../../dist/packages-dist/common": - version "9.0.0-rc.1" + version "10.0.0-next.2" "@angular/compiler-cli@file:../../dist/packages-dist/compiler-cli": - version "9.0.0-rc.1" + version "10.0.0-next.2" dependencies: canonical-path "1.0.0" chokidar "^3.0.0" @@ -158,35 +158,36 @@ reflect-metadata "^0.1.2" semver "^6.3.0" source-map "^0.6.1" - yargs "13.1.0" + sourcemap-codec "^1.4.8" + yargs "15.3.0" "@angular/compiler@file:../../dist/packages-dist/compiler": - version "9.0.0-rc.1" + version "10.0.0-next.2" "@angular/core@file:../../dist/packages-dist/core": - version "9.0.0-rc.1" + version "10.0.0-next.2" "@angular/forms@file:../../dist/packages-dist/forms": - version "9.0.0-rc.1" + version "10.0.0-next.2" "@angular/language-service@file:../../dist/packages-dist/language-service": - version "9.0.0-rc.1" + version "10.0.0-next.2" "@angular/localize@file:../../dist/packages-dist/localize": - version "9.0.0-rc.1" + version "10.0.0-next.2" dependencies: "@babel/core" "7.8.3" glob "7.1.2" - yargs "13.1.0" + yargs "15.3.0" "@angular/platform-browser-dynamic@file:../../dist/packages-dist/platform-browser-dynamic": - version "9.0.0-rc.1" + version "10.0.0-next.2" "@angular/platform-browser@file:../../dist/packages-dist/platform-browser": - version "9.0.0-rc.1" + version "10.0.0-next.2" "@angular/router@file:../../dist/packages-dist/router": - version "9.0.0-rc.1" + version "10.0.0-next.2" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": version "7.5.5" @@ -1057,31 +1058,31 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@ngtools/webpack@9.0.0-rc.11": - version "9.0.0-rc.11" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-9.0.0-rc.11.tgz#10b5997bec7cf48d1b144c8b4d46ffd0039c522a" - integrity sha512-qeW81ISiO8GVEndOaCYv0k6fzRIxzZs6jrXGl1pcLH1H6qv2mxhA5DA0vC/9TN6wenrS43RGjDIQpp+RvkiLwA== +"@ngtools/webpack@9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-9.0.3.tgz#d05b5a15584909262a4db027919f03ccb074dc11" + integrity sha512-pMIXfq1IJLbvwmkPonGs7nrpuBCXrlZTf9A4OYsMBZcfU8JMn0pRdx7G2+bC9Q/f+uSw2uvPSv76xJXLBOntmA== dependencies: - "@angular-devkit/core" "9.0.0-rc.11" + "@angular-devkit/core" "9.0.3" enhanced-resolve "4.1.1" rxjs "6.5.3" webpack-sources "1.4.3" -"@schematics/angular@9.0.0-rc.11": - version "9.0.0-rc.11" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-9.0.0-rc.11.tgz#d544c0d4e7b3dd59ed56be5183e038ebe06a165e" - integrity sha512-9InC+F71KiPXE0jl7Ow4iPFJ2AZZDbfTM6yWZoYLk3hzTCohAZZciBl00Tfyu2uerGshx8akbJMLySjXtf+q0g== +"@schematics/angular@9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-9.0.3.tgz#8b0fb91fa18dd909001ac0d888479a96810aa640" + integrity sha512-6XSnPW4G7aoKXccg0FTpZ02y/yi9y/bj7swnSL9Z4RRPIvPVapDjB7uJPg8sm8+PTIpcMhEFQrchIqM3LXW4zA== dependencies: - "@angular-devkit/core" "9.0.0-rc.11" - "@angular-devkit/schematics" "9.0.0-rc.11" + "@angular-devkit/core" "9.0.3" + "@angular-devkit/schematics" "9.0.3" -"@schematics/update@0.900.0-rc.11": - version "0.900.0-rc.11" - resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.900.0-rc.11.tgz#d22df30f13a6f38970b759db61ad84d3f9b03a78" - integrity sha512-nV0oCPzzd0vi2Exo1910rWXwz/RnMc4zF9FxSOCZzsIv+AkwIehhL815OKyjUSCzU9+IM0/o1LKkPPrSWK7QEA== +"@schematics/update@0.900.3": + version "0.900.3" + resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.900.3.tgz#9141ee2e1b6356e66f6269b92c284c86e4faf065" + integrity sha512-mlRsm3/HM1f/10Wdz4xMYA+mpW3EDCB+whlV5cJ7PGMhjUMaxA9DuWvoP06h05le6XmgnjIEoxL6NJ7CgesHcA== dependencies: - "@angular-devkit/core" "9.0.0-rc.11" - "@angular-devkit/schematics" "9.0.0-rc.11" + "@angular-devkit/core" "9.0.3" + "@angular-devkit/schematics" "9.0.3" "@yarnpkg/lockfile" "1.1.0" ini "1.3.5" npm-package-arg "^7.0.0" @@ -1090,6 +1091,11 @@ semver "6.3.0" semver-intersect "1.4.0" +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + "@types/estree@*": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -1114,15 +1120,11 @@ resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.4.2.tgz#49f672de24043b3c1fb919901fd3cd36f027bc93" integrity sha512-SaSSGOzwUnBEn64c+HTyVTJhRf8F1CXZLnxYx2ww3UrgGBmEEw38RSux2l3fYiT9brVLP67DU5omWA6V9OHI5Q== -"@types/jasmine@3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.4.4.tgz#be3fbd73e72725edb44e6f7f509cd52912d1550c" - integrity sha512-+/sHcTPyDS1JQacDRRRWb+vNrjBwnD+cKvTaWlxlJ/uOOFvzCkjOwNaqVjYMLfsjzNi0WtDH9RyReDXPG1Cdug== +"@types/jasmine@file:../../node_modules/@types/jasmine": + version "3.5.10" -"@types/jasminewd2@2.0.8": +"@types/jasminewd2@file:../../node_modules/@types/jasminewd2": version "2.0.8" - resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.8.tgz#67afe5098d5ef2386073a7b7384b69a840dfe93b" - integrity sha512-d9p31r7Nxk0ZH0U39PTH0hiDlJ+qNVGjlt1ucOoTUptxb2v+Y5VMnsxfwN+i3hK4yQnqBi3FMmoMFcd1JHDxdg== dependencies: "@types/jasmine" "*" @@ -1142,7 +1144,7 @@ integrity sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw== "@types/node@file:../../node_modules/@types/node": - version "12.11.1" + version "12.12.34" "@types/q@^0.0.32": version "0.0.32" @@ -1493,6 +1495,11 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -1505,6 +1512,14 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -2392,6 +2407,15 @@ cliui@^4.0.0: strip-ansi "^4.0.0" wrap-ansi "^2.0.0" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2455,12 +2479,19 @@ color-convert@^1.9.0, color-convert@^1.9.1: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -2723,7 +2754,7 @@ core-js@^3.1.3: integrity sha512-0xmD4vUJRY8nfLyV9zcpC17FtSie5STXzw+HyYw2t8IIvmDnbq7RJUULECCo+NstpJtwK9kx8S+898iyqgeUow== "core-js@file:../../node_modules/core-js": - version "2.5.7" + version "2.6.11" core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -3353,11 +3384,6 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -3889,7 +3915,7 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@^4.0.0: +find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -6323,7 +6349,7 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^3.0.0, os-locale@^3.1.0: +os-locale@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== @@ -7145,7 +7171,7 @@ punycode@^2.1.0: integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== "puppeteer@file:../../node_modules/puppeteer": - version "2.1.0" + version "2.1.1" dependencies: "@types/mime-types" "^2.1.0" debug "^4.1.0" @@ -7623,15 +7649,15 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@6.5.3, "rxjs@file:../../node_modules/rxjs": +rxjs@6.5.3: version "6.5.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" + integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== dependencies: tslib "^1.9.0" -rxjs@^6.4.0: +rxjs@^6.4.0, "rxjs@file:../../node_modules/rxjs": version "6.5.4" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" - integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== dependencies: tslib "^1.9.0" @@ -8124,6 +8150,11 @@ sourcemap-codec@^1.4.4: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg== +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" @@ -8310,15 +8341,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - string-width@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" @@ -8328,6 +8350,15 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^5.2.0" +string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string.prototype.padend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" @@ -8404,6 +8435,13 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -8717,7 +8755,7 @@ tslib@1.10.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== "tslib@file:../../node_modules/tslib": - version "1.10.0" + version "1.11.1" tslint@5.18.0: version "5.18.0" @@ -8786,7 +8824,7 @@ typescript@3.6.4: integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg== "typescript@file:../../node_modules/typescript": - version "3.7.4" + version "3.8.3" uglify-js@^3.1.4: version "3.6.1" @@ -9259,6 +9297,15 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -9332,10 +9379,10 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^13.0.0: - version "13.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" - integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== +yargs-parser@^18.1.0: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" @@ -9358,22 +9405,22 @@ yargs@12.0.5: y18n "^3.2.1 || ^4.0.0" yargs-parser "^11.1.1" -yargs@13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.1.0.tgz#b2729ce4bfc0c584939719514099d8a916ad2301" - integrity sha512-1UhJbXfzHiPqkfXNHYhiz79qM/kZqjTE8yGlEjZa85Q+3+OwcV6NRkV7XOV1W2Eom2bzILeUn55pQYffjVOLAg== +yargs@15.3.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.0.tgz#403af6edc75b3ae04bf66c94202228ba119f0976" + integrity sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA== dependencies: - cliui "^4.0.0" - find-up "^3.0.0" + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" get-caller-file "^2.0.1" - os-locale "^3.1.0" require-directory "^2.1.1" require-main-filename "^2.0.0" set-blocking "^2.0.0" - string-width "^3.0.0" + string-width "^4.2.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^13.0.0" + yargs-parser "^18.1.0" yauzl@2.4.1: version "2.4.1" diff --git a/packages/localize/src/tools/src/translate/main.ts b/packages/localize/src/tools/src/translate/main.ts index 55fa300e62..9178ed810d 100644 --- a/packages/localize/src/tools/src/translate/main.ts +++ b/packages/localize/src/tools/src/translate/main.ts @@ -10,8 +10,7 @@ import * as glob from 'glob'; import {resolve} from 'path'; import * as yargs from 'yargs'; -import {Diagnostics} from '../diagnostics'; -import {MissingTranslationStrategy} from '../source_file_utils'; +import {DiagnosticHandlingStrategy, Diagnostics} from '../diagnostics'; import {AssetTranslationHandler} from './asset_files/asset_translation_handler'; import {getOutputPathFn, OutputPathFn} from './output_path'; import {SourceFileTranslationHandler} from './source_files/source_file_translation_handler'; @@ -51,7 +50,10 @@ if (require.main === module) { array: true, describe: 'A list of paths to the translation files to load, either absolute or relative to the current working directory.\n' + - 'E.g. "-t src/locale/messages.en.xlf src/locale/messages.fr.xlf src/locale/messages.de.xlf".', + 'E.g. `-t src/locale/messages.en.xlf src/locale/messages.fr.xlf src/locale/messages.de.xlf`.\n' + + 'If you want to merge multiple translation files for each locale, then provide the list of files in an array.\n' + + 'Note that the arrays must be in double quotes if you include any whitespace within the array.\n' + + 'E.g. `-t "[src/locale/messages.en.xlf, src/locale/messages-2.en.xlf]" [src/locale/messages.fr.xlf,src/locale/messages-2.fr.xlf]`', }) .option('target-locales', { @@ -74,6 +76,14 @@ if (require.main === module) { choices: ['error', 'warning', 'ignore'], default: 'warning', }) + + .option('d', { + alias: 'duplicateTranslation', + describe: 'How to handle duplicate translations.', + choices: ['error', 'warning', 'ignore'], + default: 'warning', + }) + .strict() .help() .parse(args); @@ -81,10 +91,11 @@ if (require.main === module) { const sourceRootPath = options['r']; const sourceFilePaths = glob.sync(options['s'], {absolute: true, cwd: sourceRootPath, nodir: true}); - const translationFilePaths: string[] = options['t']; + const translationFilePaths: (string|string[])[] = convertArraysFromArgs(options['t']); const outputPathFn = getOutputPathFn(options['o']); const diagnostics = new Diagnostics(); const missingTranslation: DiagnosticHandlingStrategy = options['m']; + const duplicateTranslation: DiagnosticHandlingStrategy = options['d']; const sourceLocale: string|undefined = options['l']; const translationFileLocales: string[] = options['target-locales'] || []; @@ -96,6 +107,7 @@ if (require.main === module) { outputPathFn, diagnostics, missingTranslation, + duplicateTranslation, sourceLocale }); @@ -116,10 +128,29 @@ export interface TranslateFilesOptions { /** * An array of paths to the translation files to load, either absolute or relative to the current * working directory. + * + * For each locale to be translated, there should be an element in `translationFilePaths`. + * Each element is either an absolute path to the translation file, or an array of absolute paths + * to translation files, for that locale. + * + * If the element contains more than one translation file, then the translations are merged. + * + * If allowed by the `duplicateTranslation` property, when more than one translation has the same + * message id, the message from the earlier translation file in the array is used. + * + * For example, if the files are `[app.xlf, lib-1.xlf, lib-2.xlif]` then a message that appears in + * `app.xlf` will override the same message in `lib-1.xlf` or `lib-2.xlf`. */ - translationFilePaths: string[]; + translationFilePaths: (string|string[])[]; /** * A collection of the target locales for the translation files. + * + * If there is a locale provided in `translationFileLocales` then this is used rather than a + * locale extracted from the file itself. + * If there is neither a provided locale nor a locale parsed from the file, then an error is + * thrown. + * If there are both a provided locale and a locale parsed from the file, and they are not the + * same, then a warning is reported. */ translationFileLocales: (string|undefined)[]; /** @@ -135,6 +166,10 @@ export interface TranslateFilesOptions { * How to handle missing translations. */ missingTranslation: DiagnosticHandlingStrategy; + /** + * How to handle duplicate translations. + */ + duplicateTranslation: DiagnosticHandlingStrategy; /** * The locale of the source files. * If this is provided then a copy of the application will be created with no translation but just @@ -151,6 +186,7 @@ export function translateFiles({ outputPathFn, diagnostics, missingTranslation, + duplicateTranslation, sourceLocale }: TranslateFilesOptions) { const translationLoader = new TranslationLoader( @@ -160,7 +196,7 @@ export function translateFiles({ new XtbTranslationParser(), new SimpleJsonTranslationParser(), ], - diagnostics); + duplicateTranslation, diagnostics); const resourceProcessor = new Translator( [ @@ -169,8 +205,25 @@ export function translateFiles({ ], diagnostics); - const translations = translationLoader.loadBundles(translationFilePaths, translationFileLocales); + // Convert all the `translationFilePaths` elements to arrays. + const translationFilePathsArrays = + translationFilePaths.map(filePaths => Array.isArray(filePaths) ? filePaths : [filePaths]); + + const translations = + translationLoader.loadBundles(translationFilePathsArrays, translationFileLocales); sourceRootPath = resolve(sourceRootPath); resourceProcessor.translateFiles( sourceFilePaths, sourceRootPath, outputPathFn, translations, sourceLocale); } + +/** + * Parse each of the given string `args` and convert it to an array if it is of the form + * `[abc, def, ghi]`, i.e. it is enclosed in square brackets with comma delimited items. + * @param args The string to potentially convert to arrays. + */ +function convertArraysFromArgs(args: string[]): (string|string[])[] { + return args.map( + arg => (arg.startsWith('[') && arg.endsWith(']')) ? + arg.slice(1, -1).split(',').map(arg => arg.trim()) : + arg); +} \ No newline at end of file diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts index 58e51b7c47..a7cf016f82 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts @@ -5,9 +5,10 @@ * 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 {Diagnostics} from '../../diagnostics'; +import {DiagnosticHandlingStrategy, Diagnostics} from '../../diagnostics'; import {FileUtils} from '../../file_utils'; import {TranslationBundle} from '../translator'; + import {TranslationParser} from './translation_parsers/translation_parser'; /** @@ -16,61 +17,108 @@ import {TranslationParser} from './translation_parsers/translation_parser'; export class TranslationLoader { constructor( private translationParsers: TranslationParser[], + private duplicateTranslation: DiagnosticHandlingStrategy, /** @deprecated */ private diagnostics?: Diagnostics) {} /** * Load and parse the translation files into a collection of `TranslationBundles`. * - * If there is a locale provided in `translationFileLocales` then this is used rather than the + * @param translationFilePaths An array, per locale, of absolute paths to translation files. + * + * For each locale to be translated, there is an element in `translationFilePaths`. Each element + * is an array of absolute paths to translation files for that locale. + * If the array contains more than one translation file, then the translations are merged. + * If allowed by the `duplicateTranslation` property, when more than one translation has the same + * message id, the message from the earlier translation file in the array is used. + * For example, if the files are `[app.xlf, lib-1.xlf, lib-2.xlif]` then a message that appears in + * `app.xlf` will override the same message in `lib-1.xlf` or `lib-2.xlf`. + * + * @param translationFileLocales An array of locales for each of the translation files. + * + * If there is a locale provided in `translationFileLocales` then this is used rather than a * locale extracted from the file itself. * If there is neither a provided locale nor a locale parsed from the file, then an error is * thrown. * If there are both a provided locale and a locale parsed from the file, and they are not the - * same, then a warning is reported . - * - * @param translationFilePaths An array of absolute paths to the translation files. - * @param translationFileLocales An array of locales for each of the translation files. + * same, then a warning is reported. */ - loadBundles(translationFilePaths: string[], translationFileLocales: (string|undefined)[]): + loadBundles(translationFilePaths: string[][], translationFileLocales: (string|undefined)[]): TranslationBundle[] { - return translationFilePaths.map((filePath, index) => { - const fileContents = FileUtils.readFile(filePath); - for (const translationParser of this.translationParsers) { - const result = translationParser.canParse(filePath, fileContents); - if (!result) { - continue; - } - - const {locale: parsedLocale, translations, diagnostics} = - translationParser.parse(filePath, fileContents, result); - if (diagnostics.hasErrors) { - throw new Error(diagnostics.formatDiagnostics( - `The translation file "${filePath}" could not be parsed.`)); - } - - const providedLocale = translationFileLocales[index]; - const locale = providedLocale || parsedLocale; - if (locale === undefined) { - throw new Error(`The translation file "${ - filePath}" does not contain a target locale and no explicit locale was provided for this file.`); - } - - if (parsedLocale !== undefined && providedLocale !== undefined && - parsedLocale !== providedLocale) { - diagnostics.warn( - `The provided locale "${providedLocale}" does not match the target locale "${ - parsedLocale}" found in the translation file "${filePath}".`); - } - - // If we were passed a diagnostics object then copy the messages over to it. - if (this.diagnostics) { - this.diagnostics.merge(diagnostics); - } - - return {locale, translations, diagnostics}; - } - throw new Error( - `There is no "TranslationParser" that can parse this translation file: ${filePath}.`); + return translationFilePaths.map((filePaths, index) => { + const providedLocale = translationFileLocales[index]; + return this.mergeBundles(filePaths, providedLocale); }); } + + /** + * Load all the translations from the file at the given `filePath`. + */ + private loadBundle(filePath: string, providedLocale: string|undefined): TranslationBundle { + const fileContents = FileUtils.readFile(filePath); + for (const translationParser of this.translationParsers) { + const result = translationParser.canParse(filePath, fileContents); + if (!result) { + continue; + } + + const {locale: parsedLocale, translations, diagnostics} = + translationParser.parse(filePath, fileContents, result); + if (diagnostics.hasErrors) { + throw new Error(diagnostics.formatDiagnostics( + `The translation file "${filePath}" could not be parsed.`)); + } + + const locale = providedLocale || parsedLocale; + if (locale === undefined) { + throw new Error(`The translation file "${ + filePath}" does not contain a target locale and no explicit locale was provided for this file.`); + } + + if (parsedLocale !== undefined && providedLocale !== undefined && + parsedLocale !== providedLocale) { + diagnostics.warn( + `The provided locale "${providedLocale}" does not match the target locale "${ + parsedLocale}" found in the translation file "${filePath}".`); + } + + // If we were passed a diagnostics object then copy the messages over to it. + if (this.diagnostics) { + this.diagnostics.merge(diagnostics); + } + + return {locale, translations, diagnostics}; + } + throw new Error( + `There is no "TranslationParser" that can parse this translation file: ${filePath}.`); + } + + /** + * There is more than one `filePath` for this locale, so load each as a bundle and then merge them + * all together. + */ + private mergeBundles(filePaths: string[], providedLocale: string|undefined): TranslationBundle { + const bundles = filePaths.map(filePath => this.loadBundle(filePath, providedLocale)); + const bundle = bundles[0]; + for (let i = 1; i < bundles.length; i++) { + const nextBundle = bundles[i]; + if (nextBundle.locale !== bundle.locale) { + if (this.diagnostics) { + const previousFiles = filePaths.slice(0, i).map(f => `"${f}"`).join(', '); + this.diagnostics.warn(`When merging multiple translation files, the target locale "${ + nextBundle.locale}" found in "${filePaths[i]}" does not match the target locale "${ + bundle.locale}" found in earlier files [${previousFiles}].`); + } + } + Object.keys(nextBundle.translations).forEach(messageId => { + if (bundle.translations[messageId] !== undefined) { + this.diagnostics?.add( + this.duplicateTranslation, + `Duplicate translations for message "${messageId}" when merging "${filePaths[i]}".`); + } else { + bundle.translations[messageId] = nextBundle.translations[messageId]; + } + }); + } + return bundle; + } } diff --git a/packages/localize/src/tools/test/translate/integration/locales/messages-extra.de.json b/packages/localize/src/tools/test/translate/integration/locales/messages-extra.de.json new file mode 100644 index 0000000000..b0edfd5010 --- /dev/null +++ b/packages/localize/src/tools/test/translate/integration/locales/messages-extra.de.json @@ -0,0 +1,6 @@ +{ + "locale": "de", + "translations": { + "customExtra": "Auf wiedersehen, {$PH}!" + } +} \ No newline at end of file diff --git a/packages/localize/src/tools/test/translate/integration/main_spec.ts b/packages/localize/src/tools/test/translate/integration/main_spec.ts index 07a9835196..94a9a30a11 100644 --- a/packages/localize/src/tools/test/translate/integration/main_spec.ts +++ b/packages/localize/src/tools/test/translate/integration/main_spec.ts @@ -35,7 +35,8 @@ describe('translateFiles()', () => { ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']), translationFileLocales: [], diagnostics, - missingTranslation: 'error' + missingTranslation: 'error', + duplicateTranslation: 'error', }); expect(diagnostics.messages.length).toEqual(0); @@ -71,6 +72,7 @@ describe('translateFiles()', () => { translationFileLocales: [], diagnostics, missingTranslation: 'error', + duplicateTranslation: 'error', }); expect(diagnostics.messages.length).toEqual(0); @@ -98,6 +100,7 @@ describe('translateFiles()', () => { translationFileLocales: ['xde', undefined, 'fr'], diagnostics, missingTranslation: 'error', + duplicateTranslation: 'error', }); expect(diagnostics.messages.length).toEqual(1); @@ -118,6 +121,40 @@ describe('translateFiles()', () => { .toEqual(`var name="World";var message="Ciao, "+name+"!";`); }); + it('should merge translation files, if more than one provided, and translate source-code', () => { + const diagnostics = new Diagnostics(); + const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}')); + translateFiles({ + sourceRootPath: resolve(__dirname, 'test_files'), + sourceFilePaths: resolveAll(__dirname + '/test_files', ['test-extra.js']), + outputPathFn, + translationFilePaths: resolveAllRecursive( + __dirname + '/locales', + [['messages.de.json', 'messages-extra.de.json'], 'messages.es.xlf']), + translationFileLocales: [], + diagnostics, + missingTranslation: 'error', + duplicateTranslation: 'error', + }); + + expect(diagnostics.messages.length).toEqual(1); + // There is no "extra" translation in the `es` locale translation file. + expect(diagnostics.messages[0]).toEqual({ + type: 'error', + message: 'No translation found for "customExtra" ("Goodbye, {$PH}!").' + }); + + // The `de` locale translates the `customExtra` message because it is in the + // `messages-extra.de.json` file that was merged. + expect(FileUtils.readFile(resolve(testDir, 'de', 'test-extra.js'))) + .toEqual( + `var name="World";var message="Guten Tag, "+name+"!";var message="Auf wiedersehen, "+name+"!";`); + // The `es` locale does not translate `customExtra` because there is no translation for it. + expect(FileUtils.readFile(resolve(testDir, 'es', 'test-extra.js'))) + .toEqual( + `var name="World";var message="Hola, "+name+"!";var message="Goodbye, "+name+"!";`); + }); + it('should transform and/or copy files to the destination folders', () => { const diagnostics = new Diagnostics(); const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}')); @@ -132,6 +169,7 @@ describe('translateFiles()', () => { translationFileLocales: [], diagnostics, missingTranslation: 'error', + duplicateTranslation: 'error', }); expect(diagnostics.messages.length).toEqual(0); @@ -167,3 +205,8 @@ describe('translateFiles()', () => { function resolveAll(rootPath: string, paths: string[]): string[] { return paths.map(p => resolve(rootPath, p)); } + +function resolveAllRecursive(rootPath: string, paths: (string|string[])[]): (string|string[])[] { + return paths.map( + p => Array.isArray(p) ? p.map(p2 => resolve(rootPath, p2)) : resolve(rootPath, p)); +} diff --git a/packages/localize/src/tools/test/translate/integration/test_files/test-extra.js b/packages/localize/src/tools/test/translate/integration/test_files/test-extra.js new file mode 100644 index 0000000000..5662eb3e5c --- /dev/null +++ b/packages/localize/src/tools/test/translate/integration/test_files/test-extra.js @@ -0,0 +1,3 @@ +var name = 'World'; +var message = $localize`Hello, ${name}!`; +var message = $localize`:@@customExtra:Goodbye, ${name}!`; \ No newline at end of file diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts index 48bd2be07b..147a12185c 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts @@ -5,24 +5,27 @@ * 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 {ɵParsedTranslation} from '@angular/localize'; +import {ɵParsedTranslation, ɵparseTranslation} from '@angular/localize'; -import {Diagnostics} from '../../../src/diagnostics'; +import {DiagnosticHandlingStrategy, Diagnostics} from '../../../src/diagnostics'; import {FileUtils} from '../../../src/file_utils'; import {TranslationLoader} from '../../../src/translate/translation_files/translation_loader'; import {TranslationParser} from '../../../src/translate/translation_files/translation_parsers/translation_parser'; describe('TranslationLoader', () => { describe('loadBundles()', () => { + const alwaysCanParse = () => true; + const neverCanParse = () => false; + beforeEach(() => { spyOn(FileUtils, 'readFile').and.returnValues('english messages', 'french messages'); }); - it('should `canParse()` and `parse()` for each file', () => { + it('should call `canParse()` and `parse()` for each file', () => { const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(true, 'fr'); - const loader = new TranslationLoader([parser], diagnostics); - loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], []); + const parser = new MockTranslationParser(alwaysCanParse, 'fr'); + const loader = new TranslationLoader([parser], 'error', diagnostics); + loader.loadBundles([['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []); expect(parser.log).toEqual([ 'canParse(/src/locale/messages.en.xlf, english messages)', 'parse(/src/locale/messages.en.xlf, english messages)', @@ -33,11 +36,11 @@ describe('TranslationLoader', () => { it('should stop at the first parser that can parse each file', () => { const diagnostics = new Diagnostics(); - const parser1 = new MockTranslationParser(false); - const parser2 = new MockTranslationParser(true, 'fr'); - const parser3 = new MockTranslationParser(true, 'en'); - const loader = new TranslationLoader([parser1, parser2, parser3], diagnostics); - loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], []); + const parser1 = new MockTranslationParser(neverCanParse); + const parser2 = new MockTranslationParser(alwaysCanParse, 'fr'); + const parser3 = new MockTranslationParser(alwaysCanParse, 'en'); + const loader = new TranslationLoader([parser1, parser2, parser3], 'error', diagnostics); + loader.loadBundles([['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []); expect(parser1.log).toEqual([ 'canParse(/src/locale/messages.en.xlf, english messages)', 'canParse(/src/locale/messages.fr.xlf, french messages)', @@ -53,10 +56,10 @@ describe('TranslationLoader', () => { it('should return locale and translations parsed from each file', () => { const translations = {}; const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(true, 'pl', translations); - const loader = new TranslationLoader([parser], diagnostics); - const result = - loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], []); + const parser = new MockTranslationParser(alwaysCanParse, 'pl', translations); + const loader = new TranslationLoader([parser], 'error', diagnostics); + const result = loader.loadBundles( + [['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []); expect(result).toEqual([ {locale: 'pl', translations, diagnostics: new Diagnostics()}, {locale: 'pl', translations, diagnostics: new Diagnostics()}, @@ -66,23 +69,70 @@ describe('TranslationLoader', () => { it('should return the provided locale if there is no parsed locale', () => { const translations = {}; const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(true, undefined, translations); - const loader = new TranslationLoader([parser], diagnostics); + const parser = new MockTranslationParser(alwaysCanParse, undefined, translations); + const loader = new TranslationLoader([parser], 'error', diagnostics); const result = loader.loadBundles( - ['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], ['en', 'fr']); + [['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], ['en', 'fr']); expect(result).toEqual([ {locale: 'en', translations, diagnostics: new Diagnostics()}, {locale: 'fr', translations, diagnostics: new Diagnostics()}, ]); }); + it('should merge multiple translation files, if given, for a each locale', () => { + const diagnostics = new Diagnostics(); + const parser1 = new MockTranslationParser( + f => f.includes('messages.fr'), 'fr', {'a': ɵparseTranslation('A')}); + const parser2 = new MockTranslationParser( + f => f.includes('extra.fr'), 'fr', {'b': ɵparseTranslation('B')}); + const loader = new TranslationLoader([parser1, parser2], 'error', diagnostics); + const result = + loader.loadBundles([['/src/locale/messages.fr.xlf', '/src/locale/extra.fr.xlf']], []); + expect(result).toEqual([ + { + locale: 'fr', + translations: {'a': ɵparseTranslation('A'), 'b': ɵparseTranslation('B')}, + diagnostics: new Diagnostics(), + }, + ]); + }); + + const allDiagnosticModes: DiagnosticHandlingStrategy[] = ['ignore', 'warning', 'error']; + allDiagnosticModes.forEach( + mode => it( + `should ${mode} on duplicate messages when merging multiple translation files`, () => { + const diagnostics = new Diagnostics(); + const parser1 = new MockTranslationParser( + f => f.includes('messages.fr'), 'fr', {'a': ɵparseTranslation('A')}); + const parser2 = new MockTranslationParser( + f => f.includes('extra.fr'), 'fr', {'a': ɵparseTranslation('B')}); + const loader = new TranslationLoader([parser1, parser2], mode, diagnostics); + const result = loader.loadBundles( + [['/src/locale/messages.fr.xlf', '/src/locale/extra.fr.xlf']], []); + expect(result).toEqual([ + { + locale: 'fr', + translations: {'a': ɵparseTranslation('A')}, + diagnostics: jasmine.any(Diagnostics), + }, + ]); + + if (mode === 'error' || mode === 'warning') { + expect(diagnostics.messages).toEqual([{ + type: mode, + message: + `Duplicate translations for message "a" when merging "/src/locale/extra.fr.xlf".` + }]); + } + })); + it('should warn if the provided locales do not match the parsed locales', () => { const translations = {}; const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(true, 'pl', translations); - const loader = new TranslationLoader([parser], diagnostics); + const parser = new MockTranslationParser(alwaysCanParse, 'pl', translations); + const loader = new TranslationLoader([parser], 'error', diagnostics); loader.loadBundles( - ['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], [undefined, 'FR']); + [['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], [undefined, 'FR']); expect(diagnostics.messages.length).toEqual(1); expect(diagnostics.messages) .toContain( @@ -94,23 +144,58 @@ describe('TranslationLoader', () => { ); }); + it('should warn on differing target locales when merging multiple translation files', () => { + const diagnostics = new Diagnostics(); + const parser1 = new MockTranslationParser( + f => f.includes('messages-1.fr'), 'fr', {'a': ɵparseTranslation('A')}); + const parser2 = new MockTranslationParser( + f => f.includes('messages-2.fr'), 'fr', {'b': ɵparseTranslation('B')}); + const parser3 = new MockTranslationParser( + f => f.includes('messages.de'), 'de', {'c': ɵparseTranslation('C')}); + const loader = new TranslationLoader([parser1, parser2, parser3], 'error', diagnostics); + const result = loader.loadBundles( + [[ + '/src/locale/messages-1.fr.xlf', '/src/locale/messages-2.fr.xlf', + '/src/locale/messages.de.xlf' + ]], + []); + expect(result).toEqual([ + { + locale: 'fr', + translations: { + 'a': ɵparseTranslation('A'), + 'b': ɵparseTranslation('B'), + 'c': ɵparseTranslation('C') + }, + diagnostics: jasmine.any(Diagnostics), + }, + ]); + + expect(diagnostics.messages).toEqual([{ + type: 'warning', + message: + `When merging multiple translation files, the target locale "de" found in "/src/locale/messages.de.xlf" ` + + `does not match the target locale "fr" found in earlier files ["/src/locale/messages-1.fr.xlf", "/src/locale/messages-2.fr.xlf"].` + }]); + }); + it('should throw an error if there is no provided nor parsed target locale', () => { const translations = {}; const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(true, undefined, translations); - const loader = new TranslationLoader([parser], diagnostics); - expect(() => loader.loadBundles(['/src/locale/messages.en.xlf'], [])) + const parser = new MockTranslationParser(alwaysCanParse, undefined, translations); + const loader = new TranslationLoader([parser], 'error', diagnostics); + expect(() => loader.loadBundles([['/src/locale/messages.en.xlf']], [])) .toThrowError( 'The translation file "/src/locale/messages.en.xlf" does not contain a target locale and no explicit locale was provided for this file.'); }); it('should error if none of the parsers can parse the file', () => { const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(false); - const loader = new TranslationLoader([parser], diagnostics); + const parser = new MockTranslationParser(neverCanParse); + const loader = new TranslationLoader([parser], 'error', diagnostics); expect( () => loader.loadBundles( - ['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], [])) + [['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], [])) .toThrowError( 'There is no "TranslationParser" that can parse this translation file: /src/locale/messages.en.xlf.'); }); @@ -120,12 +205,12 @@ describe('TranslationLoader', () => { class MockTranslationParser implements TranslationParser { log: string[] = []; constructor( - private _canParse: boolean = true, private _locale?: string, + private _canParse: (filePath: string) => boolean, private _locale?: string, private _translations: Record = {}) {} canParse(filePath: string, fileContents: string) { this.log.push(`canParse(${filePath}, ${fileContents})`); - return this._canParse; + return this._canParse(filePath); } parse(filePath: string, fileContents: string) {