[CSA-27] Use new dependency-free locale service for WebAuthN translations (#4557)

This commit is contained in:
Matt Bishop 2023-02-04 09:23:42 -05:00 committed by GitHub
parent dc3a0b25cb
commit dcc7846138
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 306 additions and 254 deletions

View File

@ -1,5 +1,7 @@
import { I18nService as BaseI18nService } from "@bitwarden/common/services/i18n.service";
import { SupportedTranslationLocales } from "../../translation-constants";
export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage || "en-US", localesDirectory, async (formattedLocale: string) => {
@ -14,61 +16,6 @@ export class I18nService extends BaseI18nService {
return locales;
});
// Please leave 'en' where it is, as it's our fallback language in case no translation can be found
this.supportedTranslationLocales = [
"en",
"af",
"ar",
"az",
"be",
"bg",
"bn",
"bs",
"ca",
"cs",
"da",
"de",
"el",
"en-GB",
"en-IN",
"eo",
"es",
"et",
"eu",
"fi",
"fil",
"fr",
"he",
"hi",
"hr",
"hu",
"id",
"it",
"ja",
"ka",
"km",
"kn",
"ko",
"lv",
"ml",
"nb",
"nl",
"nn",
"pl",
"pt-PT",
"pt-BR",
"ro",
"ru",
"si",
"sk",
"sl",
"sr",
"sv",
"tr",
"uk",
"vi",
"zh-CN",
"zh-TW",
];
this.supportedTranslationLocales = SupportedTranslationLocales;
}
}

View File

@ -0,0 +1,31 @@
import { TranslationService as BaseTranslationService } from "@bitwarden/common/services/translation.service";
import { SupportedTranslationLocales } from "../translation-constants";
export class TranslationService extends BaseTranslationService {
private _translationLocale: string;
constructor(systemLanguage: string, localesDirectory: string) {
super(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;
});
this.supportedTranslationLocales = SupportedTranslationLocales;
}
get translationLocale(): string {
return this._translationLocale;
}
set translationLocale(locale: string) {
this._translationLocale = locale;
}
}

View File

@ -1,5 +1,6 @@
import { b64Decode, getQsParam } from "./common";
import { buildDataString, parseWebauthnJson } from "./common-webauthn";
import { TranslationService } from "./translation.service";
require("./webauthn.scss");
@ -7,9 +8,8 @@ let parsed = false;
let webauthnJson: any;
let parentUrl: string = null;
let sentSuccess = false;
let locale = "en";
let locales: any = {};
let locale: string = null;
let localeService: TranslationService = null;
function parseParameters() {
if (parsed) {
@ -24,7 +24,7 @@ function parseParameters() {
parentUrl = decodeURIComponent(parentUrl);
}
locale = getQsParam("locale").replace("-", "_");
locale = getQsParam("locale") ?? "en";
const version = getQsParam("v");
@ -61,18 +61,19 @@ function parseParametersV2() {
document.addEventListener("DOMContentLoaded", async () => {
parseParameters();
try {
locales = await loadLocales(locale);
localeService = new TranslationService(locale, "locales");
} catch {
// eslint-disable-next-line
console.error("Failed to load the locale", locale);
locales = await loadLocales("en");
error("Failed to load the provided locale " + locale);
localeService = new TranslationService("en", "locales");
}
document.getElementById("msg").innerText = translate("webAuthnFallbackMsg");
document.getElementById("remember-label").innerText = translate("rememberMe");
await localeService.init();
document.getElementById("msg").innerText = localeService.t("webAuthnFallbackMsg");
document.getElementById("remember-label").innerText = localeService.t("rememberMe");
const button = document.getElementById("webauthn-button");
button.innerText = translate("webAuthnAuthenticate");
button.innerText = localeService.t("webAuthnAuthenticate");
button.onclick = start;
document.getElementById("spinner").classList.add("d-none");
@ -81,23 +82,13 @@ document.addEventListener("DOMContentLoaded", async () => {
content.classList.remove("d-none");
});
async function loadLocales(newLocale: string) {
const filePath = `locales/${newLocale}/messages.json?cache=${process.env.CACHE_TAG}`;
const localesResult = await fetch(filePath);
return await localesResult.json();
}
function translate(id: string) {
return locales[id]?.message || "";
}
function start() {
if (sentSuccess) {
return;
}
if (!("credentials" in navigator)) {
error(translate("webAuthnNotSupported"));
error(localeService.t("webAuthnNotSupported"));
return;
}
@ -133,7 +124,7 @@ async function initWebAuthn(obj: any) {
window.postMessage({ command: "webAuthnResult", data: dataString, remember: remember }, "*");
sentSuccess = true;
success(translate("webAuthnSuccess"));
success(localeService.t("webAuthnSuccess"));
} catch (err) {
error(err);
}

View File

@ -0,0 +1,56 @@
// Please leave 'en' where it is, as it's our fallback language in case no translation can be found
export const SupportedTranslationLocales: string[] = [
"en",
"af",
"ar",
"az",
"be",
"bg",
"bn",
"bs",
"ca",
"cs",
"da",
"de",
"el",
"en-GB",
"en-IN",
"eo",
"es",
"et",
"eu",
"fi",
"fil",
"fr",
"he",
"hi",
"hr",
"hu",
"id",
"it",
"ja",
"ka",
"km",
"kn",
"ko",
"lv",
"ml",
"nb",
"nl",
"nn",
"pl",
"pt-PT",
"pt-BR",
"ro",
"ru",
"si",
"sk",
"sl",
"sr",
"sv",
"tr",
"uk",
"vi",
"zh-CN",
"zh-TW",
];

View File

@ -1,11 +1,7 @@
import { Observable } from "rxjs";
export abstract class I18nService {
import { TranslationService } from "./translation.service";
export abstract class I18nService extends TranslationService {
locale$: Observable<string>;
supportedTranslationLocales: string[];
translationLocale: string;
collator: Intl.Collator;
localeNames: Map<string, string>;
t: (id: string, p1?: string | number, p2?: string | number, p3?: string | number) => string;
translate: (id: string, p1?: string, p2?: string, p3?: string) => string;
}

View File

@ -0,0 +1,8 @@
export abstract class TranslationService {
supportedTranslationLocales: string[];
translationLocale: string;
collator: Intl.Collator;
localeNames: Map<string, string>;
t: (id: string, p1?: string | number, p2?: string | number, p3?: string | number) => string;
translate: (id: string, p1?: string, p2?: string, p3?: string) => string;
}

View File

@ -2,184 +2,27 @@ import { Observable, ReplaySubject } from "rxjs";
import { I18nService as I18nServiceAbstraction } from "../abstractions/i18n.service";
export class I18nService implements I18nServiceAbstraction {
protected _locale = new ReplaySubject<string>(1);
locale$: Observable<string> = this._locale.asObservable();
// First locale is the default (English)
supportedTranslationLocales: string[] = ["en"];
defaultLocale = "en";
translationLocale: string;
collator: Intl.Collator;
localeNames = new Map<string, string>([
["af", "Afrikaans"],
["ar", "العربية الفصحى"],
["az", "Azərbaycanca"],
["be", "Беларуская"],
["bg", "български"],
["bn", "বাংলা"],
["bs", "bosanski jezik"],
["ca", "català"],
["cs", "čeština"],
["da", "dansk"],
["de", "Deutsch"],
["el", "Ελληνικά"],
["en", "English"],
["en-GB", "English (British)"],
["en-IN", "English (India)"],
["eo", "Esperanto"],
["es", "español"],
["et", "eesti"],
["eu", "euskara"],
["fa", "فارسی"],
["fi", "suomi"],
["fil", "Wikang Filipino"],
["fr", "français"],
["he", "עברית"],
["hi", "हिन्दी"],
["hr", "hrvatski"],
["hu", "magyar"],
["id", "Bahasa Indonesia"],
["it", "italiano"],
["ja", "日本語"],
["ka", "ქართული"],
["km", "ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ"],
["kn", "ಕನ್ನಡ"],
["ko", "한국어"],
["lt", "lietuvių kalba"],
["lv", "Latvietis"],
["me", "црногорски"],
["ml", "മലയാളം"],
["nb", "norsk (bokmål)"],
["nl", "Nederlands"],
["nn", "Norsk Nynorsk"],
["pl", "polski"],
["pt-BR", "português do Brasil"],
["pt-PT", "português"],
["ro", "română"],
["ru", "русский"],
["si", "සිංහල"],
["sk", "slovenčina"],
["sl", "Slovenski jezik, Slovenščina"],
["sr", "Српски"],
["sv", "svenska"],
["th", "ไทย"],
["tr", "Türkçe"],
["uk", "українська"],
["vi", "Tiếng Việt"],
["zh-CN", "中文(中国大陆)"],
["zh-TW", "中文(台灣)"],
]);
import { TranslationService } from "./translation.service";
protected inited: boolean;
protected defaultMessages: any = {};
protected localeMessages: any = {};
export class I18nService extends TranslationService implements I18nServiceAbstraction {
protected _locale = new ReplaySubject<string>(1);
private _translationLocale: string;
locale$: Observable<string> = this._locale.asObservable();
constructor(
protected systemLanguage: string,
protected localesDirectory: string,
protected getLocalesJson: (formattedLocale: string) => Promise<any>
) {
this.systemLanguage = systemLanguage.replace("_", "-");
super(systemLanguage, localesDirectory, getLocalesJson);
}
async init(locale?: string) {
if (this.inited) {
throw new Error("i18n already initialized.");
}
if (this.supportedTranslationLocales == null || this.supportedTranslationLocales.length === 0) {
throw new Error("supportedTranslationLocales not set.");
}
this.inited = true;
this.translationLocale = locale != null ? locale : this.systemLanguage;
this._locale.next(this.translationLocale);
try {
this.collator = new Intl.Collator(this.translationLocale, {
numeric: true,
sensitivity: "base",
});
} catch {
this.collator = null;
}
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
this.translationLocale = this.translationLocale.slice(0, 2);
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
this.translationLocale = this.defaultLocale;
}
}
if (this.localesDirectory != null) {
await this.loadMessages(this.translationLocale, this.localeMessages);
if (this.translationLocale !== this.defaultLocale) {
await this.loadMessages(this.defaultLocale, this.defaultMessages);
}
}
get translationLocale(): string {
return this._translationLocale;
}
t(id: string, p1?: string, p2?: string, p3?: string): string {
return this.translate(id, p1, p2, p3);
}
translate(id: string, p1?: string | number, p2?: string | number, p3?: string | number): string {
let result: string;
// eslint-disable-next-line
if (this.localeMessages.hasOwnProperty(id) && this.localeMessages[id]) {
result = this.localeMessages[id];
// eslint-disable-next-line
} else if (this.defaultMessages.hasOwnProperty(id) && this.defaultMessages[id]) {
result = this.defaultMessages[id];
} else {
result = "";
}
if (result !== "") {
if (p1 != null) {
result = result.split("__$1__").join(p1.toString());
}
if (p2 != null) {
result = result.split("__$2__").join(p2.toString());
}
if (p3 != null) {
result = result.split("__$3__").join(p3.toString());
}
}
return result;
}
private async loadMessages(locale: string, messagesObj: any): Promise<any> {
const formattedLocale = locale.replace("-", "_");
const locales = await this.getLocalesJson(formattedLocale);
for (const prop in locales) {
// eslint-disable-next-line
if (!locales.hasOwnProperty(prop)) {
continue;
}
messagesObj[prop] = locales[prop].message;
if (locales[prop].placeholders) {
for (const placeProp in locales[prop].placeholders) {
if (
!locales[prop].placeholders.hasOwnProperty(placeProp) || // eslint-disable-line
!locales[prop].placeholders[placeProp].content
) {
continue;
}
const replaceToken = "\\$" + placeProp.toUpperCase() + "\\$";
let replaceContent = locales[prop].placeholders[placeProp].content;
if (replaceContent === "$1" || replaceContent === "$2" || replaceContent === "$3") {
replaceContent = "__$" + replaceContent + "__";
}
messagesObj[prop] = messagesObj[prop].replace(
new RegExp(replaceToken, "g"),
replaceContent
);
}
}
}
set translationLocale(locale: string) {
this._translationLocale = locale;
this._locale.next(locale);
}
}

View File

@ -0,0 +1,180 @@
import { TranslationService as TranslationServiceAbstraction } from "../abstractions/translation.service";
export abstract class TranslationService implements TranslationServiceAbstraction {
// First locale is the default (English)
supportedTranslationLocales: string[] = ["en"];
defaultLocale = "en";
abstract translationLocale: string;
collator: Intl.Collator;
localeNames = new Map<string, string>([
["af", "Afrikaans"],
["ar", "العربية الفصحى"],
["az", "Azərbaycanca"],
["be", "Беларуская"],
["bg", "български"],
["bn", "বাংলা"],
["bs", "bosanski jezik"],
["ca", "català"],
["cs", "čeština"],
["da", "dansk"],
["de", "Deutsch"],
["el", "Ελληνικά"],
["en", "English"],
["en-GB", "English (British)"],
["en-IN", "English (India)"],
["eo", "Esperanto"],
["es", "español"],
["et", "eesti"],
["eu", "euskara"],
["fa", "فارسی"],
["fi", "suomi"],
["fil", "Wikang Filipino"],
["fr", "français"],
["he", "עברית"],
["hi", "हिन्दी"],
["hr", "hrvatski"],
["hu", "magyar"],
["id", "Bahasa Indonesia"],
["it", "italiano"],
["ja", "日本語"],
["ka", "ქართული"],
["km", "ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ"],
["kn", "ಕನ್ನಡ"],
["ko", "한국어"],
["lt", "lietuvių kalba"],
["lv", "Latvietis"],
["me", "црногорски"],
["ml", "മലയാളം"],
["nb", "norsk (bokmål)"],
["nl", "Nederlands"],
["nn", "Norsk Nynorsk"],
["pl", "polski"],
["pt-BR", "português do Brasil"],
["pt-PT", "português"],
["ro", "română"],
["ru", "русский"],
["si", "සිංහල"],
["sk", "slovenčina"],
["sl", "Slovenski jezik, Slovenščina"],
["sr", "Српски"],
["sv", "svenska"],
["th", "ไทย"],
["tr", "Türkçe"],
["uk", "українська"],
["vi", "Tiếng Việt"],
["zh-CN", "中文(中国大陆)"],
["zh-TW", "中文(台灣)"],
]);
protected inited: boolean;
protected defaultMessages: any = {};
protected localeMessages: any = {};
constructor(
protected systemLanguage: string,
protected localesDirectory: string,
protected getLocalesJson: (formattedLocale: string) => Promise<any>
) {
this.systemLanguage = systemLanguage.replace("_", "-");
}
async init(locale?: string) {
if (this.inited) {
throw new Error("i18n already initialized.");
}
if (this.supportedTranslationLocales == null || this.supportedTranslationLocales.length === 0) {
throw new Error("supportedTranslationLocales not set.");
}
this.inited = true;
this.translationLocale = locale != null ? locale : this.systemLanguage;
try {
this.collator = new Intl.Collator(this.translationLocale, {
numeric: true,
sensitivity: "base",
});
} catch {
this.collator = null;
}
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
this.translationLocale = this.translationLocale.slice(0, 2);
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
this.translationLocale = this.defaultLocale;
}
}
if (this.localesDirectory != null) {
await this.loadMessages(this.translationLocale, this.localeMessages);
if (this.translationLocale !== this.defaultLocale) {
await this.loadMessages(this.defaultLocale, this.defaultMessages);
}
}
}
t(id: string, p1?: string, p2?: string, p3?: string): string {
return this.translate(id, p1, p2, p3);
}
translate(id: string, p1?: string | number, p2?: string | number, p3?: string | number): string {
let result: string;
// eslint-disable-next-line
if (this.localeMessages.hasOwnProperty(id) && this.localeMessages[id]) {
result = this.localeMessages[id];
// eslint-disable-next-line
} else if (this.defaultMessages.hasOwnProperty(id) && this.defaultMessages[id]) {
result = this.defaultMessages[id];
} else {
result = "";
}
if (result !== "") {
if (p1 != null) {
result = result.split("__$1__").join(p1.toString());
}
if (p2 != null) {
result = result.split("__$2__").join(p2.toString());
}
if (p3 != null) {
result = result.split("__$3__").join(p3.toString());
}
}
return result;
}
protected async loadMessages(locale: string, messagesObj: any): Promise<any> {
const formattedLocale = locale.replace("-", "_");
const locales = await this.getLocalesJson(formattedLocale);
for (const prop in locales) {
// eslint-disable-next-line
if (!locales.hasOwnProperty(prop)) {
continue;
}
messagesObj[prop] = locales[prop].message;
if (locales[prop].placeholders) {
for (const placeProp in locales[prop].placeholders) {
if (
!locales[prop].placeholders.hasOwnProperty(placeProp) || // eslint-disable-line
!locales[prop].placeholders[placeProp].content
) {
continue;
}
const replaceToken = "\\$" + placeProp.toUpperCase() + "\\$";
let replaceContent = locales[prop].placeholders[placeProp].content;
if (replaceContent === "$1" || replaceContent === "$2" || replaceContent === "$3") {
replaceContent = "__$" + replaceContent + "__";
}
messagesObj[prop] = messagesObj[prop].replace(
new RegExp(replaceToken, "g"),
replaceContent
);
}
}
}
}
}