From 74433c3e05734aa1194049fcbcaa92c70ce61c74 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 12 Dec 2019 20:53:15 +0100 Subject: refactor: re-structure type definitions --- src/webex/i18n.tsx | 267 +++++++++++++++++++++++++++++++++++++++ src/webex/messages.ts | 9 +- src/webex/pages/add-auditor.tsx | 2 +- src/webex/pages/auditors.tsx | 2 +- src/webex/pages/benchmark.tsx | 4 +- src/webex/pages/pay.tsx | 4 +- src/webex/pages/payback.tsx | 2 +- src/webex/pages/popup.tsx | 6 +- src/webex/pages/refund.tsx | 2 +- src/webex/pages/return-coins.tsx | 4 +- src/webex/pages/tip.tsx | 4 +- src/webex/pages/welcome.tsx | 2 +- src/webex/pages/withdraw.tsx | 4 +- src/webex/renderHtml.tsx | 6 +- src/webex/wxApi.ts | 4 +- src/webex/wxBackend.ts | 14 +- 16 files changed, 302 insertions(+), 34 deletions(-) create mode 100644 src/webex/i18n.tsx (limited to 'src/webex') diff --git a/src/webex/i18n.tsx b/src/webex/i18n.tsx new file mode 100644 index 000000000..3923654e7 --- /dev/null +++ b/src/webex/i18n.tsx @@ -0,0 +1,267 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + */ + +/** + * Translation helpers for React components and template literals. + */ + +/** + * Imports. + */ +import {strings} from "../i18n/strings"; + +// @ts-ignore: no type decl for this library +import * as jedLib from "jed"; + +import * as React from "react"; + + +const jed = setupJed(); + +let enableTracing = false; + + +/** + * Set up jed library for internationalization, + * based on browser language settings. + */ +function setupJed(): any { + let lang: string; + try { + lang = chrome.i18n.getUILanguage(); + // Chrome gives e.g. "en-US", but Firefox gives us "en_US" + lang = lang.replace("_", "-"); + } catch (e) { + lang = "en"; + console.warn("i18n default language not available"); + } + + if (!strings[lang]) { + lang = "en-US"; + console.log(`language ${lang} not found, defaulting to english`); + } + return new jedLib.Jed(strings[lang]); +} + + +/** + * Convert template strings to a msgid + */ +function toI18nString(stringSeq: ReadonlyArray) { + let s = ""; + for (let i = 0; i < stringSeq.length; i++) { + s += stringSeq[i]; + if (i < stringSeq.length - 1) { + s += `%${i + 1}$s`; + } + } + return s; +} + + +/** + * Internationalize a string template with arbitrary serialized values. + */ +export function str(stringSeq: TemplateStringsArray, ...values: any[]) { + const s = toI18nString(stringSeq); + const tr = jed.translate(s).ifPlural(1, s).fetch(...values); + return tr; +} + + +interface TranslateSwitchProps { + target: number; +} + + +function stringifyChildren(children: any): string { + let n = 1; + const ss = React.Children.map(children, (c) => { + if (typeof c === "string") { + return c; + } + return `%${n++}$s`; + }); + const s = ss.join("").replace(/ +/g, " ").trim(); + enableTracing && console.log("translation lookup", JSON.stringify(s)); + return s; +} + + +interface TranslateProps { + /** + * Component that the translated element should be wrapped in. + * Defaults to "div". + */ + wrap?: any; + + /** + * Props to give to the wrapped component. + */ + wrapProps?: any; +} + + +/** + * Translate text node children of this component. + * If a child component might produce a text node, it must be wrapped + * in a another non-text element. + * + * Example: + * ``` + * + * Hello. Your score is + * + * ``` + */ +export class Translate extends React.Component { + render(): JSX.Element { + const s = stringifyChildren(this.props.children); + const tr = jed.ngettext(s, s, 1).split(/%(\d+)\$s/).filter((e: any, i: number) => i % 2 === 0); + const childArray = React.Children.toArray(this.props.children!); + for (let i = 0; i < childArray.length - 1; ++i) { + if ((typeof childArray[i]) === "string" && (typeof childArray[i + 1]) === "string") { + childArray[i + 1] = (childArray[i] as string).concat(childArray[i + 1] as string); + childArray.splice(i, 1); + } + } + const result = []; + while (childArray.length > 0) { + const x = childArray.shift(); + if (x === undefined) { + continue; + } + if (typeof x === "string") { + const t = tr.shift(); + result.push(t); + } else { + result.push(x); + } + } + if (!this.props.wrap) { + return
{result}
; + } + return React.createElement(this.props.wrap, this.props.wrapProps, result); + } +} + + +/** + * Switch translation based on singular or plural based on the target prop. + * Should only contain TranslateSingular and TransplatePlural as children. + * + * Example: + * ``` + * + * I have {n} apple. + * I have {n} apples. + * + * ``` + */ +export class TranslateSwitch extends React.Component { + render(): JSX.Element { + let singular: React.ReactElement | undefined; + let plural: React.ReactElement | undefined; + const children = this.props.children; + if (children) { + React.Children.forEach(children, (child: any) => { + if (child.type === TranslatePlural) { + plural = child; + } + if (child.type === TranslateSingular) { + singular = child; + } + }); + } + if ((!singular) || (!plural)) { + console.error("translation not found"); + return React.createElement("span", {}, ["translation not found"]); + } + singular.props.target = this.props.target; + plural.props.target = this.props.target; + // We're looking up the translation based on the + // singular, even if we must use the plural form. + return singular; + } +} + + +interface TranslationPluralProps { + target: number; +} + +/** + * See [[TranslateSwitch]]. + */ +export class TranslatePlural extends React.Component { + render(): JSX.Element { + const s = stringifyChildren(this.props.children); + const tr = jed.ngettext(s, s, 1).split(/%(\d+)\$s/).filter((e: any, i: number) => i % 2 === 0); + const childArray = React.Children.toArray(this.props.children!); + for (let i = 0; i < childArray.length - 1; ++i) { + if ((typeof childArray[i]) === "string" && (typeof childArray[i + 1]) === "string") { + childArray[i + i] = childArray[i] as string + childArray[i + 1] as string; + childArray.splice(i, 1); + } + } + const result = []; + while (childArray.length > 0) { + const x = childArray.shift(); + if (x === undefined) { + continue; + } + if (typeof x === "string") { + const t = tr.shift(); + result.push(t); + } else { + result.push(x); + } + } + return
{result}
; + } +} + + +/** + * See [[TranslateSwitch]]. + */ +export class TranslateSingular extends React.Component { + render(): JSX.Element { + const s = stringifyChildren(this.props.children); + const tr = jed.ngettext(s, s, 1).split(/%(\d+)\$s/).filter((e: any, i: number) => i % 2 === 0); + const childArray = React.Children.toArray(this.props.children!); + for (let i = 0; i < childArray.length - 1; ++i) { + if ((typeof childArray[i]) === "string" && (typeof childArray[i + 1]) === "string") { + childArray[i + i] = childArray[i] as string + childArray[i + 1] as string; + childArray.splice(i, 1); + } + } + const result = []; + while (childArray.length > 0) { + const x = childArray.shift(); + if (x === undefined) { + continue; + } + if (typeof x === "string") { + const t = tr.shift(); + result.push(t); + } else { + result.push(x); + } + } + return
{result}
; + } +} diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 4aaf75b2b..579dd4347 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -22,11 +22,12 @@ /* tslint:disable:completed-docs */ import { AmountJson } from "../util/amounts"; -import * as dbTypes from "../dbTypes"; -import * as talerTypes from "../talerTypes"; -import * as walletTypes from "../walletTypes"; +import * as dbTypes from "../types/dbTypes"; +import * as talerTypes from "../types/talerTypes"; +import * as walletTypes from "../types/walletTypes"; import { UpgradeResponse } from "./wxApi"; +import { HistoryEvent } from "../types/history"; /** * Message type information. @@ -79,7 +80,7 @@ export interface MessageMap { }; "get-history": { request: {}; - response: walletTypes.HistoryEvent[]; + response: HistoryEvent[]; }; "get-coins": { request: { exchangeBaseUrl: string }; diff --git a/src/webex/pages/add-auditor.tsx b/src/webex/pages/add-auditor.tsx index 766db9c5d..0f681aae4 100644 --- a/src/webex/pages/add-auditor.tsx +++ b/src/webex/pages/add-auditor.tsx @@ -20,7 +20,7 @@ * @author Florian Dold */ -import { CurrencyRecord } from "../../dbTypes"; +import { CurrencyRecord } from "../../types/dbTypes"; import { getCurrencies, updateCurrency } from "../wxApi"; import React, { useState } from "react"; import { registerMountPage } from "../renderHtml"; diff --git a/src/webex/pages/auditors.tsx b/src/webex/pages/auditors.tsx index 276a7e8e1..876cf326b 100644 --- a/src/webex/pages/auditors.tsx +++ b/src/webex/pages/auditors.tsx @@ -25,7 +25,7 @@ import { AuditorRecord, CurrencyRecord, ExchangeForCurrencyRecord, -} from "../../dbTypes"; +} from "../../types/dbTypes"; import { getCurrencies, diff --git a/src/webex/pages/benchmark.tsx b/src/webex/pages/benchmark.tsx index b250bc20a..fe874f2b7 100644 --- a/src/webex/pages/benchmark.tsx +++ b/src/webex/pages/benchmark.tsx @@ -21,9 +21,9 @@ * @author Florian Dold */ -import * as i18n from "../../i18n"; +import * as i18n from "../i18n"; -import { BenchmarkResult } from "../../walletTypes"; +import { BenchmarkResult } from "../../types/walletTypes"; import * as wxApi from "../wxApi"; diff --git a/src/webex/pages/pay.tsx b/src/webex/pages/pay.tsx index cff2f9461..eca115e78 100644 --- a/src/webex/pages/pay.tsx +++ b/src/webex/pages/pay.tsx @@ -22,9 +22,9 @@ /** * Imports. */ -import * as i18n from "../../i18n"; +import * as i18n from "../i18n"; -import { PreparePayResult } from "../../walletTypes"; +import { PreparePayResult } from "../../types/walletTypes"; import { renderAmount, ProgressButton, registerMountPage } from "../renderHtml"; import * as wxApi from "../wxApi"; diff --git a/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx index 806bef17c..a25b5c6b2 100644 --- a/src/webex/pages/payback.tsx +++ b/src/webex/pages/payback.tsx @@ -23,7 +23,7 @@ /** * Imports. */ -import { ReserveRecord } from "../../dbTypes"; +import { ReserveRecord } from "../../types/dbTypes"; import { renderAmount, registerMountPage } from "../renderHtml"; import { getPaybackReserves, withdrawPaybackReserve } from "../wxApi"; import * as React from "react"; diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx index 27d5dddba..3a2856d64 100644 --- a/src/webex/pages/popup.tsx +++ b/src/webex/pages/popup.tsx @@ -24,16 +24,15 @@ /** * Imports. */ -import * as i18n from "../../i18n"; +import * as i18n from "../i18n"; import { AmountJson } from "../../util/amounts"; import * as Amounts from "../../util/amounts"; import { - HistoryEvent, WalletBalance, WalletBalanceEntry, -} from "../../walletTypes"; +} from "../../types/walletTypes"; import { abbrev, @@ -44,6 +43,7 @@ import { import * as wxApi from "../wxApi"; import * as React from "react"; +import { HistoryEvent } from "../../types/history"; function onUpdateNotification(f: () => void): () => void { const port = chrome.runtime.connect({ name: "notifications" }); diff --git a/src/webex/pages/refund.tsx b/src/webex/pages/refund.tsx index 5196c9ea6..2a3f65d21 100644 --- a/src/webex/pages/refund.tsx +++ b/src/webex/pages/refund.tsx @@ -24,7 +24,7 @@ import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom"; import * as wxApi from "../wxApi"; -import { PurchaseDetails } from "../../walletTypes"; +import { PurchaseDetails } from "../../types/walletTypes"; import { AmountView } from "../renderHtml"; function RefundStatusView(props: { talerRefundUri: string }) { diff --git a/src/webex/pages/return-coins.tsx b/src/webex/pages/return-coins.tsx index be65b4121..7c835da0a 100644 --- a/src/webex/pages/return-coins.tsx +++ b/src/webex/pages/return-coins.tsx @@ -31,9 +31,9 @@ import * as Amounts from "../../util/amounts"; import { SenderWireInfos, WalletBalance, -} from "../../walletTypes"; +} from "../../types/walletTypes"; -import * as i18n from "../../i18n"; +import * as i18n from "../i18n"; import * as wire from "../../util/wire"; diff --git a/src/webex/pages/tip.tsx b/src/webex/pages/tip.tsx index ac904cf0d..c44b343a4 100644 --- a/src/webex/pages/tip.tsx +++ b/src/webex/pages/tip.tsx @@ -24,7 +24,7 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; -import * as i18n from "../../i18n"; +import * as i18n from "../i18n"; import { acceptTip, getReserveCreationInfo, getTipStatus } from "../wxApi"; @@ -32,7 +32,7 @@ import { WithdrawDetailView, renderAmount, ProgressButton } from "../renderHtml" import * as Amounts from "../../util/amounts"; import { useState, useEffect } from "react"; -import { TipStatus } from "../../walletTypes"; +import { TipStatus } from "../../types/walletTypes"; function TipDisplay(props: { talerTipUri: string }) { diff --git a/src/webex/pages/welcome.tsx b/src/webex/pages/welcome.tsx index 1026e6e6e..e8f7028ed 100644 --- a/src/webex/pages/welcome.tsx +++ b/src/webex/pages/welcome.tsx @@ -23,7 +23,7 @@ import React, { useState, useEffect } from "react"; import { getDiagnostics } from "../wxApi"; import { registerMountPage, PageLink } from "../renderHtml"; -import { WalletDiagnostics } from "../../walletTypes"; +import { WalletDiagnostics } from "../../types/walletTypes"; function Diagnostics() { const [timedOut, setTimedOut] = useState(false); diff --git a/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx index 3ee0f768a..9d84ff3a6 100644 --- a/src/webex/pages/withdraw.tsx +++ b/src/webex/pages/withdraw.tsx @@ -22,11 +22,11 @@ */ -import * as i18n from "../../i18n"; +import * as i18n from "../i18n"; import { WithdrawDetails, -} from "../../walletTypes"; +} from "../../types/walletTypes"; import { WithdrawDetailView, renderAmount } from "../renderHtml"; diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx index bf9cdc76f..767058ebf 100644 --- a/src/webex/renderHtml.tsx +++ b/src/webex/renderHtml.tsx @@ -25,10 +25,10 @@ */ import { AmountJson } from "../util/amounts"; import * as Amounts from "../util/amounts"; -import { DenominationRecord } from "../dbTypes"; -import { ExchangeWithdrawDetails } from "../walletTypes"; +import { DenominationRecord } from "../types/dbTypes"; +import { ExchangeWithdrawDetails } from "../types/walletTypes"; import * as moment from "moment"; -import * as i18n from "../i18n"; +import * as i18n from "./i18n"; import React from "react"; import ReactDOM from "react-dom"; diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index b0af7ac29..1383ffbc3 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -30,7 +30,7 @@ import { ExchangeRecord, PlanchetRecord, ReserveRecord, -} from "../dbTypes"; +} from "../types/dbTypes"; import { BenchmarkResult, ConfirmPayResult, @@ -40,7 +40,7 @@ import { WalletBalance, PurchaseDetails, WalletDiagnostics, -} from "../walletTypes"; +} from "../types/walletTypes"; import { MessageMap, MessageType } from "./messages"; diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 27141247e..f3f4d80eb 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -30,11 +30,11 @@ import { CreateReserveRequest, ReturnCoinsRequest, WalletDiagnostics, -} from "../walletTypes"; +} from "../types/walletTypes"; import { Wallet } from "../wallet"; import { isFirefox } from "./compat"; -import { WALLET_DB_VERSION } from "../dbTypes"; -import { openTalerDb, exportDb, importDb, deleteDb } from "../db"; +import { WALLET_DB_VERSION } from "../types/dbTypes"; +import { openDatabase, exportDatabase, importDatabase, deleteDatabase } from "../db"; import { ChromeBadge } from "./chromeBadge"; import { MessageType } from "./messages"; import * as wxApi from "./wxApi"; @@ -73,11 +73,11 @@ async function handleMessage( } case "dump-db": { const db = needsWallet().db; - return exportDb(db); + return exportDatabase(db); } case "import-db": { const db = needsWallet().db; - return importDb(db, detail.dump); + return importDatabase(db, detail.dump); } case "ping": { return Promise.resolve(); @@ -91,7 +91,7 @@ async function handleMessage( tx.objectStore(db.objectStoreNames[i]).clear(); } } - deleteDb(indexedDB); + deleteDatabase(indexedDB); setBadgeText({ text: "" }); console.log("reset done"); if (!currentWallet) { @@ -423,7 +423,7 @@ async function reinitWallet() { setBadgeText({ text: "" }); const badge = new ChromeBadge(); try { - currentDatabase = await openTalerDb( + currentDatabase = await openDatabase( indexedDB, reinitWallet, handleUpgradeUnsupported, -- cgit v1.2.3