From 817af28c0b98dcfee2da09906c8523a7a1d872b2 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 7 Mar 2025 11:53:52 +0100 Subject: [PATCH 1/2] PoC using Angular Localize --- angular.json | 15 +- .../app/auth/login/login-v1.component.html | 8 +- .../src/app/auth/login/login-v1.component.ts | 2 + apps/web/src/app/core/i18n.service.ts | 10 +- apps/web/src/locales/messages.sv-se.json | 10 + apps/web/src/main.ts | 16 +- apps/web/src/polyfills.ts | 2 + apps/web/webpack.config.js | 2 +- apps/web/webpack.i18n.js | 218 ++++++++ babel.config.json => babel2.config.json | 0 bitwarden_license/bit-web/src/main.ts | 16 +- package-lock.json | 476 +++++++++++++++++- package.json | 2 + 13 files changed, 757 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/locales/messages.sv-se.json create mode 100644 apps/web/webpack.i18n.js rename babel.config.json => babel2.config.json (100%) diff --git a/angular.json b/angular.json index 665d810cf4e..c3cf4b69182 100644 --- a/angular.json +++ b/angular.json @@ -18,17 +18,26 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-builders/custom-webpack:browser", "options": { + "customWebpackConfig": { + "path": "./apps/web/webpack.i18n.js" + }, "outputPath": "dist/web", "index": "apps/web/src/index.html", - "main": "apps/web/src/app/main.ts", - "polyfills": "apps/web/src/app/polyfills.ts", + "main": "apps/web/src/main.ts", + "polyfills": "apps/web/src/polyfills.ts", "tsConfig": "apps/web/tsconfig.json", "assets": ["apps/web/src/favicon.ico"], "styles": [], "scripts": [] } + }, + "extract-i18n": { + "builder": "@angular-builders/custom-webpack:extract-i18n", + "options": { + "buildTarget": "web:build" + } } } }, diff --git a/apps/web/src/app/auth/login/login-v1.component.html b/apps/web/src/app/auth/login/login-v1.component.html index b41e55a03b0..b83f81c3749 100644 --- a/apps/web/src/app/auth/login/login-v1.component.html +++ b/apps/web/src/app/auth/login/login-v1.component.html @@ -1,3 +1,9 @@ +

Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{ 5 }} minutes ago}}

+ +

This is a inline link to Settings with text before and after.

+ +

A phrase we really need to highlight!

+
- {{ "emailAddress" | i18n }} + Email address
diff --git a/apps/web/src/app/auth/login/login-v1.component.ts b/apps/web/src/app/auth/login/login-v1.component.ts index a3099d991d9..fa6213c0a97 100644 --- a/apps/web/src/app/auth/login/login-v1.component.ts +++ b/apps/web/src/app/auth/login/login-v1.component.ts @@ -46,6 +46,8 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit { enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions; policies: Policy[]; + protected minutes = 5; + constructor( private acceptOrganizationInviteService: AcceptOrganizationInviteService, devicesApiService: DevicesApiServiceAbstraction, diff --git a/apps/web/src/app/core/i18n.service.ts b/apps/web/src/app/core/i18n.service.ts index 744b11d56e6..ad2f2eab2b3 100644 --- a/apps/web/src/app/core/i18n.service.ts +++ b/apps/web/src/app/core/i18n.service.ts @@ -13,15 +13,7 @@ export class I18nService extends BaseI18nService { systemLanguage || "en-US", localesDirectory, async (formattedLocale: string) => { - const filePath = - this.localesDirectory + - "/" + - formattedLocale + - "/messages.json?cache=" + - process.env.CACHE_TAG; - const localesResult = await fetch(filePath); - const locales = await localesResult.json(); - return locales; + return Promise.resolve({}); }, globalStateProvider, ); diff --git a/apps/web/src/locales/messages.sv-se.json b/apps/web/src/locales/messages.sv-se.json new file mode 100644 index 00000000000..2f655dd1276 --- /dev/null +++ b/apps/web/src/locales/messages.sv-se.json @@ -0,0 +1,10 @@ +{ + "locale": "sv-Se", + "translations": { + "4606963464835766483": "Uppdaterad {$ICU}", + "2002272803511843863": "{VAR_PLURAL, plural, =0 {precis nu} =1 {en minut sedan} other {för {INTERPOLATION} minuter sedan}}", + "1150463724722084961": "Detta är en flytande länk till {$START_LINK}Inställningar{$CLOSE_LINK} med text före och efter.", + "5010897546053474360": "En fras som vi verkligen {$START_TAG_STRONG}behöver{$CLOSE_TAG_STRONG} framhäva!", + "email": "E-postadress" + } +} diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index b202a170d26..99e7999b7e6 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -1,4 +1,5 @@ import { enableProdMode } from "@angular/core"; +import { loadTranslations } from "@angular/localize"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import "bootstrap"; @@ -11,4 +12,17 @@ if (process.env.NODE_ENV === "production") { enableProdMode(); } -void platformBrowserDynamic().bootstrapModule(AppModule); +void initLanguage("sv-se").then(() => { + return platformBrowserDynamic().bootstrapModule(AppModule); +}); + +async function initLanguage(locale: string): Promise { + if (locale === "en") { + return; + } + + const json = await fetch("/locales/messages." + locale + ".json").then((r) => r.json()); + + loadTranslations(json); + $localize.locale = locale; +} diff --git a/apps/web/src/polyfills.ts b/apps/web/src/polyfills.ts index 3971ed3207f..d9ce0b4302e 100644 --- a/apps/web/src/polyfills.ts +++ b/apps/web/src/polyfills.ts @@ -2,6 +2,8 @@ import "core-js/stable"; import "core-js/proposals/explicit-resource-management"; import "zone.js"; +import "@angular/localize/init"; + if (process.env.NODE_ENV === "production") { // Production } else { diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index 28fe5ce1f35..cfaaca03251 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -80,7 +80,7 @@ const moduleRules = [ { loader: "babel-loader", options: { - configFile: "../../babel.config.json", + configFile: "../../babel2.config.json", }, }, ], diff --git a/apps/web/webpack.i18n.js b/apps/web/webpack.i18n.js new file mode 100644 index 00000000000..1c14d3fa04c --- /dev/null +++ b/apps/web/webpack.i18n.js @@ -0,0 +1,218 @@ +const path = require("path"); + +const { AngularWebpackPlugin } = require("@ngtools/webpack"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const HtmlWebpackInjector = require("html-webpack-injector"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const TerserPlugin = require("terser-webpack-plugin"); +const webpack = require("webpack"); + +const config = require("./config.js"); +const pjson = require("./package.json"); + +const ENV = process.env.ENV == null ? "development" : process.env.ENV; +const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV; +const LOGGING = process.env.LOGGING != "false"; + +const envConfig = config.load(ENV); +if (LOGGING) { + config.log(envConfig); +} + +const moduleRules = [ + { + test: /\.(html)$/, + loader: "html-loader", + }, + { + test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, + exclude: /loading(|-white).svg/, + generator: { + filename: "fonts/[name].[contenthash][ext]", + }, + type: "asset/resource", + }, + { + test: /\.(jpe?g|png|gif|svg|webp|avif)$/i, + exclude: /.*(bwi-font)\.svg/, + generator: { + filename: "images/[name][ext]", + }, + type: "asset/resource", + }, + { + test: /\.scss$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + "css-loader", + "resolve-url-loader", + { + loader: "sass-loader", + options: { + sourceMap: true, + }, + }, + ], + }, + { + test: /\.css$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + "css-loader", + "resolve-url-loader", + { + loader: "postcss-loader", + options: { + sourceMap: true, + }, + }, + ], + }, + + { + test: /\.[jt]sx?$/, + use: [ + { + loader: "@ngtools/webpack", + }, + ], + }, + { + test: /argon2(-simd)?\.wasm$/, + loader: "base64-loader", + type: "javascript/auto", + }, +]; + +const plugins = [ + new HtmlWebpackPlugin({ + template: "./apps/web/src/index.html", + filename: "index.html", + chunks: ["theme_head", "app/polyfills", "app/vendor", "app/main", "styles"], + }), + new HtmlWebpackInjector(), + new HtmlWebpackPlugin({ + template: "./apps/web/src/404.html", + filename: "404.html", + chunks: ["styles"], + // 404 page is a wildcard, this ensures it uses absolute paths. + publicPath: "/", + }), + new CopyWebpackPlugin({ + patterns: [ + { from: "./apps/web/src/.nojekyll" }, + { from: "./apps/web/src/manifest.json" }, + { from: "./apps/web/src/favicon.ico" }, + { from: "./apps/web/src/browserconfig.xml" }, + { from: "./apps/web/src/app-id.json" }, + { from: "./apps/web/src/images", to: "images" }, + { from: "./apps/web/src/locales", to: "locales" }, + { from: "./node_modules/qrious/dist/qrious.min.js", to: "scripts" }, + { from: "./node_modules/braintree-web-drop-in/dist/browser/dropin.js", to: "scripts" }, + { + from: "./apps/web/src/version.json", + transform(content, path) { + return content.toString().replace("process.env.APPLICATION_VERSION", pjson.version); + }, + }, + ], + }), + new MiniCssExtractPlugin({ + filename: "[name].[contenthash].css", + chunkFilename: "[id].[contenthash].css", + }), + new webpack.ProvidePlugin({ + process: "process/browser.js", + }), + new webpack.EnvironmentPlugin({ + ENV: ENV, + NODE_ENV: NODE_ENV === "production" ? "production" : "development", + APPLICATION_VERSION: pjson.version, + CACHE_TAG: Math.random().toString(36).substring(7), + URLS: envConfig["urls"] ?? {}, + STRIPE_KEY: envConfig["stripeKey"] ?? "", + BRAINTREE_KEY: envConfig["braintreeKey"] ?? "", + PAYPAL_CONFIG: envConfig["paypal"] ?? {}, + FLAGS: envConfig["flags"] ?? {}, + DEV_FLAGS: NODE_ENV === "development" ? envConfig["devFlags"] : {}, + ADDITIONAL_REGIONS: envConfig["additionalRegions"] ?? [], + }), + new AngularWebpackPlugin({ + tsconfig: "apps/web/tsconfig.build.json", + entryModule: "src/app/app.module#AppModule", + sourceMap: true, + }), +]; + +const webpackConfig = { + mode: NODE_ENV, + devtool: "source-map", + target: "web", + entry: { + "app/polyfills": "./apps/web/src/polyfills.ts", + "app/main": "./apps/web/src/main.ts", + styles: ["./apps/web/src/scss/styles.scss", "./apps/web/src/scss/tailwind.css"], + theme_head: "./apps/web/src/theme.ts", + }, + optimization: { + splitChunks: { + cacheGroups: { + commons: { + test: /[\\/]node_modules[\\/]/, + name: "app/vendor", + chunks: (chunk) => { + return chunk.name === "app/main"; + }, + }, + }, + }, + minimizer: [ + new TerserPlugin({ + terserOptions: { + safari10: true, + // Replicate Angular CLI behaviour + compress: { + global_defs: { + ngDevMode: false, + ngI18nClosureMode: false, + }, + }, + }, + }), + ], + }, + resolve: { + extensions: [".ts", ".js"], + symlinks: false, + modules: [path.resolve("../../node_modules")], + fallback: { + buffer: false, + util: require.resolve("util/"), + assert: false, + url: false, + fs: false, + process: false, + path: require.resolve("path-browserify"), + }, + }, + output: { + filename: "[name].[contenthash].js", + path: path.resolve(__dirname, "build"), + clean: true, + }, + module: { + noParse: /argon2(-simd)?\.wasm$/, + rules: moduleRules, + }, + experiments: { + asyncWebAssembly: true, + }, + plugins: plugins, +}; + +module.exports = webpackConfig; diff --git a/babel.config.json b/babel2.config.json similarity index 100% rename from babel.config.json rename to babel2.config.json diff --git a/bitwarden_license/bit-web/src/main.ts b/bitwarden_license/bit-web/src/main.ts index b202a170d26..7bb0d409d6a 100644 --- a/bitwarden_license/bit-web/src/main.ts +++ b/bitwarden_license/bit-web/src/main.ts @@ -1,4 +1,5 @@ import { enableProdMode } from "@angular/core"; +import { loadTranslations } from "@angular/localize"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import "bootstrap"; @@ -11,4 +12,17 @@ if (process.env.NODE_ENV === "production") { enableProdMode(); } -void platformBrowserDynamic().bootstrapModule(AppModule); +void initLanguage("sv-se").then(() => { + return platformBrowserDynamic().bootstrapModule(AppModule); +}); + +async function initLanguage(locale: string): Promise { + if (locale === "en") { + return; + } + + const json = await fetch("/locales/messages." + locale + ".json").then((r) => r.json()); + + loadTranslations(json.translations); + $localize.locale = locale; +} diff --git a/package-lock.json b/package-lock.json index 142a4e13c21..c65829664a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,10 +75,12 @@ "zxcvbn": "4.4.2" }, "devDependencies": { + "@angular-builders/custom-webpack": "18.0.0", "@angular-devkit/build-angular": "18.2.12", "@angular-eslint/schematics": "18.4.3", "@angular/cli": "18.2.12", "@angular/compiler-cli": "18.2.13", + "@angular/localize": "18.2.13", "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", @@ -395,6 +397,282 @@ "node": ">=6.0.0" } }, + "node_modules/@angular-builders/common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-2.0.0.tgz", + "integrity": "sha512-O5YJc++DtJVJhqA/OomRKN2jGYzvU/YXtfrPAqcA9Is3Ob5jvV0L0JHSAjSw/KaLvk/FjBIqoRVcYdLp5LKddA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "^18.0.0", + "ts-node": "^10.0.0", + "tsconfig-paths": "^4.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + } + }, + "node_modules/@angular-builders/common/node_modules/@angular-devkit/core": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.14.tgz", + "integrity": "sha512-UGIGOjXuOyCW+5S4tINu7e6LOu738CmTw3h7Ui1I8OzdTIYJcYJrei8sgrwDwOYADRal+p0MeMlnykH3TM5XBA==", + "dev": true, + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-builders/common/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@angular-builders/common/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@angular-builders/common/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@angular-builders/common/node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-builders/common/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-builders/common/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-builders/custom-webpack": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-18.0.0.tgz", + "integrity": "sha512-XSynPSXHq5+nrh7J2snfrcbvm6YGwUGQRzr7OuO3wURJ6CHOD9C+xEAmvEUWW8c1YjEslVNG7aLtCGz7LA4ymw==", + "dev": true, + "dependencies": { + "@angular-builders/common": "2.0.0", + "@angular-devkit/architect": ">=0.1800.0 < 0.1900.0", + "@angular-devkit/build-angular": "^18.0.0", + "@angular-devkit/core": "^18.0.0", + "lodash": "^4.17.15", + "webpack-merge": "^5.7.3" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/architect": { + "version": "0.1802.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.14.tgz", + "integrity": "sha512-eplaGCXSlPwf1f4XwyzsYTd8/lJ0/Adm6XsODsBxvkZlIpLcps80/h2lH5MVJpoDREzIFu1BweDpYCoNK5yYZg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "18.2.14", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/core": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.14.tgz", + "integrity": "sha512-UGIGOjXuOyCW+5S4tINu7e6LOu738CmTw3h7Ui1I8OzdTIYJcYJrei8sgrwDwOYADRal+p0MeMlnykH3TM5XBA==", + "dev": true, + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@angular-devkit/architect": { "version": "0.1901.8", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.8.tgz", @@ -2520,6 +2798,75 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/localize": { + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.13.tgz", + "integrity": "sha512-qQaIYdDS/l1w6tr/wpOoimjpmoJU0WmB8AGbNeKLoM36K+ix6hkvn67+UgkpZtaDHZylm8GsGW1NjzpM2tr3pA==", + "dev": true, + "dependencies": { + "@babel/core": "7.25.2", + "@types/babel__core": "7.20.5", + "fast-glob": "3.3.2", + "yargs": "^17.2.1" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "18.2.13", + "@angular/compiler-cli": "18.2.13" + } + }, + "node_modules/@angular/localize/node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/localize/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@angular/localize/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@angular/platform-browser": { "version": "18.2.13", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.13.tgz", @@ -5298,6 +5645,28 @@ "node": ">= 10.0.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -10773,6 +11142,30 @@ "tinyglobby": "^0.2.9" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -10827,7 +11220,6 @@ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -10842,7 +11234,6 @@ "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.0.0" } @@ -10853,7 +11244,6 @@ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -10865,7 +11255,6 @@ "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.20.7" } @@ -16662,6 +17051,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/credit-card-type": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-10.0.1.tgz", @@ -17269,6 +17664,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -34064,6 +34468,55 @@ "code-block-writer": "^13.0.3" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -35247,6 +35700,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -37218,6 +37677,15 @@ "node": ">= 4.0.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index cb941238fc2..824ccd25dd2 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,12 @@ "libs/**/*" ], "devDependencies": { + "@angular-builders/custom-webpack": "18.0.0", "@angular-devkit/build-angular": "18.2.12", "@angular-eslint/schematics": "18.4.3", "@angular/cli": "18.2.12", "@angular/compiler-cli": "18.2.13", + "@angular/localize": "18.2.13", "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", From ccd3a3d3daba1c85a66cb0cabfcbed4c53a8e30d Mon Sep 17 00:00:00 2001 From: Hinton Date: Wed, 19 Mar 2025 23:02:43 +0900 Subject: [PATCH 2/2] Add PoC to browser --- apps/browser/src/_locales/messages.sv-se.json | 11 ++++++++++ apps/browser/src/autofill/notification/bar.ts | 20 +++++++++++++++++-- apps/browser/src/popup/main.ts | 18 +++++++++++++---- apps/browser/src/popup/polyfills.ts | 1 + apps/browser/webpack.config.js | 2 +- .../src/angular/login/login.component.html | 8 +++++++- .../auth/src/angular/login/login.component.ts | 1 + 7 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 apps/browser/src/_locales/messages.sv-se.json diff --git a/apps/browser/src/_locales/messages.sv-se.json b/apps/browser/src/_locales/messages.sv-se.json new file mode 100644 index 00000000000..eab53678cd2 --- /dev/null +++ b/apps/browser/src/_locales/messages.sv-se.json @@ -0,0 +1,11 @@ +{ + "locale": "sv-Se", + "translations": { + "4606963464835766483": "Uppdaterad {$ICU}", + "2002272803511843863": "{VAR_PLURAL, plural, =0 {precis nu} =1 {en minut sedan} other {för {INTERPOLATION} minuter sedan}}", + "1150463724722084961": "Detta är en flytande länk till {$START_LINK}Inställningar{$CLOSE_LINK} med text före och efter.", + "5010897546053474360": "En fras som vi verkligen {$START_TAG_STRONG}behöver{$CLOSE_TAG_STRONG} framhäva!", + "email": "E-postadress", + "616177228530588915": "En översatt sträng med en länk. {$PH}" + } +} diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 617b1e58c14..dd75786f7f1 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -1,3 +1,5 @@ +import "@angular/localize/init"; +import { loadTranslations } from "@angular/localize"; import { render } from "lit"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; @@ -17,6 +19,19 @@ import { NotificationType, } from "./abstractions/notification-bar"; +async function initLanguage(locale: string): Promise { + if (locale === "en") { + return; + } + + const json = await fetch("/_locales/messages." + locale + ".json").then((r) => r.json()); + + loadTranslations(json.translations); + $localize.locale = locale; +} + +void initLanguage("sv-se"); + const logService = new ConsoleLogService(false); let notificationBarIframeInitData: NotificationBarIframeInitData = {}; let windowMessageOrigin: string; @@ -48,6 +63,7 @@ function applyNotificationBarStyle() { } function getI18n() { + const a = 5; return { appName: chrome.i18n.getMessage("appName"), close: chrome.i18n.getMessage("close"), @@ -60,7 +76,7 @@ function getI18n() { never: chrome.i18n.getMessage("never"), notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"), notificationAddSave: chrome.i18n.getMessage("notificationAddSave"), - notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), + notificationChangeDesc: $localize`Test translated message in content script with link ${a}`, notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"), notificationEdit: chrome.i18n.getMessage("edit"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), @@ -111,7 +127,7 @@ const findElementById = ( function setElementText(template: HTMLTemplateElement, elementId: string, text: string): void { const element = template.content.getElementById(elementId); if (element) { - element.textContent = text; + element.innerHTML = text; } } diff --git a/apps/browser/src/popup/main.ts b/apps/browser/src/popup/main.ts index bb975f48e5d..e30462cf9e8 100644 --- a/apps/browser/src/popup/main.ts +++ b/apps/browser/src/popup/main.ts @@ -1,4 +1,5 @@ import { enableProdMode } from "@angular/core"; +import { loadTranslations } from "@angular/localize"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { PopupSizeService } from "../platform/popup/layout/popup-size.service"; @@ -22,8 +23,17 @@ if (process.env.ENV === "production") { enableProdMode(); } -function init() { - void platformBrowserDynamic().bootstrapModule(AppModule); -} +void initLanguage("sv-se").then(() => { + return platformBrowserDynamic().bootstrapModule(AppModule); +}); -init(); +async function initLanguage(locale: string): Promise { + if (locale === "en") { + return; + } + + const json = await fetch("/_locales/messages." + locale + ".json").then((r) => r.json()); + + loadTranslations(json.translations); + $localize.locale = locale; +} diff --git a/apps/browser/src/popup/polyfills.ts b/apps/browser/src/popup/polyfills.ts index 4bb2aa0bbee..3d8085a833c 100644 --- a/apps/browser/src/popup/polyfills.ts +++ b/apps/browser/src/popup/polyfills.ts @@ -1,3 +1,4 @@ import "core-js/stable"; import "core-js/proposals/explicit-resource-management"; import "zone.js"; +import "@angular/localize/init"; diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index ff5331ae459..50581fc0930 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -80,7 +80,7 @@ const moduleRules = [ { loader: "babel-loader", options: { - configFile: "../../babel.config.json", + configFile: "../../babel2.config.json", }, }, ], diff --git a/libs/auth/src/angular/login/login.component.html b/libs/auth/src/angular/login/login.component.html index b04a54da425..c9db7d4f154 100644 --- a/libs/auth/src/angular/login/login.component.html +++ b/libs/auth/src/angular/login/login.component.html @@ -1,4 +1,4 @@ - +

Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{ 5 }} minutes ago}}

+ +

This is a inline link to Settings with text before and after.

+ +

A phrase we really need to highlight!

+
diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index cc38ec5dfb3..b76bd802cf2 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -73,6 +73,7 @@ export class LoginComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); readonly Icons = { WaveIcon, VaultIcon }; + protected minutes = 5; clientType: ClientType; ClientType = ClientType;