diff --git a/packages/taler-util/src/i18n.ts b/packages/taler-util/src/i18n.ts index 0324d8e07..e452ffa9f 100644 --- a/packages/taler-util/src/i18n.ts +++ b/packages/taler-util/src/i18n.ts @@ -45,7 +45,7 @@ function toI18nString(stringSeq: ReadonlyArray): string { /** * Internationalize a string template with arbitrary serialized values. */ -export function str(stringSeq: TemplateStringsArray, ...values: any[]): string { +export function singular(stringSeq: TemplateStringsArray, ...values: any[]): string { const s = toI18nString(stringSeq); const tr = jed .translate(s) @@ -141,7 +141,9 @@ function stringifyArray(children: Array): string { } export const i18n = { - str, + str: singular, + singular, Translate, translate, }; + diff --git a/packages/taler-wallet-webextension/.storybook/preview.js b/packages/taler-wallet-webextension/.storybook/preview.js index af768dde8..169b726f9 100644 --- a/packages/taler-wallet-webextension/.storybook/preview.js +++ b/packages/taler-wallet-webextension/.storybook/preview.js @@ -14,15 +14,9 @@ GNU Taler; see the file COPYING. If not, see */ -import { setupI18n } from "@gnu-taler/taler-util" import { Fragment } from "preact" -import { strings } from '../src/i18n/strings.ts' import { NavBar } from '../src/popup/popup' - -const mockConfig = { - backendURL: 'http://demo.taler.net', - currency: 'KUDOS' -} +import { TranslationProvider } from '../src/context/translation' export const parameters = { controls: { expanded: true }, @@ -38,7 +32,7 @@ export const globalTypes = { icon: 'globe', items: [ { value: 'en', right: '🇺🇸', title: 'English' }, - { value: 'es', right: '🇪🇸', title: 'Spanish' }, + { value: 'de', right: '🇪🇸', title: 'German' }, ], }, }, @@ -58,7 +52,7 @@ export const decorators = [ } else { - const path = !isTestingHeader ? /popup(\/.*)\/.*/.exec(kind)[1] : '' + const path = !isTestingHeader ? /popup(\/.*).*/.exec(kind)[1] : '' // add a fake header so it looks similar return @@ -113,9 +107,7 @@ export const decorators = [ }, - (Story, { globals }) => { - setupI18n(globals.locale, strings); - return - }, - // (Story) => + (Story, { globals }) => + + , ]; diff --git a/packages/taler-wallet-webextension/clean_and_build.sh b/packages/taler-wallet-webextension/clean_and_build.sh index e862be37e..fb8b31c7e 100755 --- a/packages/taler-wallet-webextension/clean_and_build.sh +++ b/packages/taler-wallet-webextension/clean_and_build.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash # This file is in the public domain. [ "also-wallet" == "$1" ] && { pnpm -C ../taler-wallet-core/ compile || exit 1; } +[ "also-util" == "$1" ] && { pnpm -C ../taler-util/ prepare || exit 1; } pnpm clean && pnpm compile && rm -rf extension/ && ./pack.sh && (cd extension/ && unzip taler*.zip) diff --git a/packages/taler-wallet-webextension/src/components/SelectList.tsx b/packages/taler-wallet-webextension/src/components/SelectList.tsx new file mode 100644 index 000000000..2c4a106ee --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/SelectList.tsx @@ -0,0 +1,40 @@ +/* + This file is part of GNU Taler + (C) 2019 Taler Systems SA + + GNU 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. + + GNU 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 + GNU Taler; see the file COPYING. If not, see + */ + +import { VNode } from "preact"; +import { useRef, useState } from "preact/hooks"; +import { JSX } from "preact/jsx-runtime"; + +interface Props { + value: string; + onChange: (s: string) => void; + label: string; + list: { + [label: string]: string + } + name: string; + description?: string; +} + +export function SelectList({ name, value, list, onChange, label, description }: Props): JSX.Element { + return +} diff --git a/packages/taler-wallet-webextension/src/context/useDevContext.ts b/packages/taler-wallet-webextension/src/context/devContext.ts similarity index 100% rename from packages/taler-wallet-webextension/src/context/useDevContext.ts rename to packages/taler-wallet-webextension/src/context/devContext.ts diff --git a/packages/taler-wallet-webextension/src/context/translation.ts b/packages/taler-wallet-webextension/src/context/translation.ts new file mode 100644 index 000000000..5f57958de --- /dev/null +++ b/packages/taler-wallet-webextension/src/context/translation.ts @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU 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. + + GNU 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 + GNU Taler; see the file COPYING. If not, see + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { createContext, h, VNode } from 'preact' +import { useContext, useEffect } from 'preact/hooks' +import { useLang } from '../hooks/useLang' +//@ts-ignore: type declaration +import * as jedLib from "jed"; +import { strings } from "../i18n/strings"; +import { setupI18n } from '@gnu-taler/taler-util'; + +interface Type { + lang: string; + changeLanguage: (l: string) => void; +} +const initial = { + lang: 'en', + changeLanguage: () => { + // do not change anything + } +} +const Context = createContext(initial) + +interface Props { + initial?: string, + children: any, + forceLang?: string +} + +//we use forceLang when we don't want to use the saved state, but sone forced +//runtime lang predefined lang +export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => { + const [lang, changeLanguage] = useLang(initial) + useEffect(() => { + if (forceLang) { + changeLanguage(forceLang) + } + }) + useEffect(()=> { + setupI18n(lang, strings) + },[lang]) + if (forceLang) { + setupI18n(forceLang, strings) + } else { + setupI18n(lang, strings) + } + return h(Context.Provider, { value: { lang, changeLanguage }, children }); +} + +export const useTranslationContext = (): Type => useContext(Context); diff --git a/packages/taler-wallet-webextension/src/hooks/useLang.ts b/packages/taler-wallet-webextension/src/hooks/useLang.ts new file mode 100644 index 000000000..d9ad7cd55 --- /dev/null +++ b/packages/taler-wallet-webextension/src/hooks/useLang.ts @@ -0,0 +1,7 @@ +import { useNotNullLocalStorage } from './useLocalStorage'; + +export function useLang(initial?: string): [string, (s:string) => void] { + const browserLang: string | undefined = typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : undefined; + const defaultLang = (browserLang || initial || 'en').substring(0, 2) + return useNotNullLocalStorage('lang-preference', defaultLang) +} diff --git a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts index 30f681940..78a8b65d5 100644 --- a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts +++ b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts @@ -42,3 +42,24 @@ export function useLocalStorage(key: string, initialValue?: string): [string | u return [storedValue, setValue]; } + +//TODO: merge with the above function +export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater] { + const [storedValue, setStoredValue] = useState((): string => { + return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue; + }); + + const setValue = (value: string | ((val: string) => string)) => { + const valueToStore = value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + if (typeof window !== "undefined") { + if (!valueToStore) { + window.localStorage.removeItem(key) + } else { + window.localStorage.setItem(key, valueToStore); + } + } + }; + + return [storedValue, setValue]; +} diff --git a/packages/taler-wallet-webextension/src/i18n/strings.ts b/packages/taler-wallet-webextension/src/i18n/strings.ts index 748b9656a..5b1257830 100644 --- a/packages/taler-wallet-webextension/src/i18n/strings.ts +++ b/packages/taler-wallet-webextension/src/i18n/strings.ts @@ -159,6 +159,76 @@ strings["en-US"] = { }, }; +strings["es"] = { + domain: "messages", + locale_data: { + messages: { + "": { + domain: "messages", + plural_forms: "nplurals=2; plural=(n != 1);", + lang: "", + }, + "Invalid Wire": [""], + "Invalid Test Wire Detail": [""], + "Test Wire Acct #%1$s on %2$s": [""], + "Unknown Wire Detail": [""], + Operation: [""], + "time (ms/op)": [""], + "The merchant %1$s offers you to purchase:": [""], + "The total price is %1$s (plus %2$s fees).": [""], + "The total price is %1$s.": [""], + Retry: [""], + "Confirm payment": [""], + Balance: [""], + History: ["Historial"], + Debug: [""], + "You have no balance to show. Need some %1$s getting started?": [""], + "%1$s incoming": [""], + "%1$s being spent": [""], + "Error: could not retrieve balance information.": [""], + "Invalid ": [""], + "Fees ": [""], + "Refresh sessions has completed": [""], + "Order Refused": [""], + "Order redirected": [""], + "Payment aborted": [""], + "Payment Sent": [""], + "Backup": ["Resguardo"], + "Order accepted": [""], + "Reserve balance updated": [""], + "Payment refund": [""], + Withdrawn: [""], + "Tip Accepted": [""], + "Tip Declined": [""], + "%1$s": [""], + "Your wallet has no events recorded.": [""], + "Wire to bank account": [""], + Confirm: ["Confirmar"], + Cancel: ["Cancelar"], + "Could not get details for withdraw operation:": [""], + "Chose different exchange provider": [""], + "Please select an exchange. You can review the details before after your selection.": [ + "", + ], + "Select %1$s": [""], + "Select custom exchange": [""], + "You are about to withdraw %1$s from your bank account into your wallet.": [ + "", + ], + "Accept fees and withdraw": [""], + "Cancel withdraw operation": [""], + "Withdrawal fees:": [""], + "Rounding loss:": [""], + "Earliest expiration (for deposit): %1$s": [""], + "# Coins": [""], + Value: [""], + "Withdraw Fee": [""], + "Refresh Fee": [""], + "Deposit Fee": [""], + }, + }, +}; + strings["fr"] = { domain: "messages", locale_data: { diff --git a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx index 0b76d7560..30512d227 100644 --- a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx +++ b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx @@ -31,7 +31,7 @@ interface Props { export function ProviderDetailPage({ pid, onBack }: Props): VNode { const status = useProviderStatus(pid) if (!status) { - return
Loading...
+ return
Loading...
} if (!status.info) { onBack() @@ -67,26 +67,26 @@ export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewP

{daysSince(info?.lastSuccessfulBackupTimestamp)}

{descriptionByStatus(info.paymentStatus)}

{info.paymentStatus.type === ProviderPaymentType.TermsChanged &&
-

terms has changed, extending the service will imply accepting the new terms of service

+

terms has changed, extending the service will imply accepting the new terms of service

- + - + - + - + @@ -117,11 +117,11 @@ function daysSince(d?: Timestamp) { const str = formatDuration(duration, { delimiter: ', ', format: [ - duration?.years ? 'years' : ( - duration?.months ? 'months' : ( - duration?.days ? 'days' : ( - duration?.hours ? 'hours' : ( - duration?.minutes ? 'minutes' : 'seconds' + duration?.years ? i18n.str`years` : ( + duration?.months ? i18n.str`months` : ( + duration?.days ? i18n.str`days` : ( + duration?.hours ? i18n.str`hours` : ( + duration?.minutes ? i18n.str`minutes` : i18n.str`seconds` ) ) ) @@ -139,13 +139,13 @@ function Error({ info }: { info: ProviderInfo }) { switch (info.backupProblem.type) { case "backup-conflicting-device": return - There is conflict with another backup from {info.backupProblem.otherDeviceId} + There is conflict with another backup from {info.backupProblem.otherDeviceId} } /> case "backup-unreadable": return default: return - Unknown backup problem: {JSON.stringify(info.backupProblem)} + Unknown backup problem: {JSON.stringify(info.backupProblem)} } /> } } @@ -172,15 +172,15 @@ function colorByStatus(status: ProviderPaymentType) { function descriptionByStatus(status: ProviderPaymentStatus) { switch (status.type) { case ProviderPaymentType.InsufficientBalance: - return 'no enough balance to make the payment' + return i18n.str`no enough balance to make the payment` case ProviderPaymentType.Unpaid: - return 'not paid yet' + return i18n.str`not paid yet` case ProviderPaymentType.Paid: case ProviderPaymentType.TermsChanged: if (status.paidUntil.t_ms === 'never') { - return 'service paid.' + return i18n.str`service paid` } else { - return `service paid until ${format(status.paidUntil.t_ms, 'yyyy/MM/dd HH:mm:ss')}` + return i18n.str`service paid until ${format(status.paidUntil.t_ms, 'yyyy/MM/dd HH:mm:ss')}` } case ProviderPaymentType.Pending: return '' diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx b/packages/taler-wallet-webextension/src/popup/Settings.tsx index d8cd04380..9bb10a4e3 100644 --- a/packages/taler-wallet-webextension/src/popup/Settings.tsx +++ b/packages/taler-wallet-webextension/src/popup/Settings.tsx @@ -15,18 +15,23 @@ */ +import { i18n } from "@gnu-taler/taler-util"; import { VNode } from "preact"; import { Checkbox } from "../components/Checkbox"; import { EditableText } from "../components/EditableText"; -import { useDevContext } from "../context/useDevContext"; +import { SelectList } from "../components/SelectList"; +import { useDevContext } from "../context/devContext"; import { useBackupDeviceName } from "../hooks/useBackupDeviceName"; import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; +import { useLang } from "../hooks/useLang"; export function SettingsPage(): VNode { const [permissionsEnabled, togglePermissions] = useExtendedPermissions(); const { devMode, toggleDevMode } = useDevContext() const { name, update } = useBackupDeviceName() + const [lang, changeLang] = useLang() return void; deviceName: string; setDeviceName: (s: string) => Promise; permissionsEnabled: boolean; @@ -42,20 +49,43 @@ export interface ViewProps { toggleDeveloperMode: () => void; } -export function SettingsView({ deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode { +import { strings as messages } from '../i18n/strings' + +type LangsNames = { + [P in keyof typeof messages]: string +} + +const names: LangsNames = { + es: 'Español [es]', + en: 'English [en]', + fr: 'Français [fr]', + de: 'Deutsch [de]', + sv: 'Svenska [sv]', + it: 'Italiano [it]', +} + + +export function SettingsView({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode { return (
- -

Wallet

+

Wallet

+ -

Permissions

+

Permissions

oldold ->newnew
feefee {info.paymentStatus.oldTerms.annualFee} -> {info.paymentStatus.newTerms.annualFee}
storagestorage {info.paymentStatus.oldTerms.storageLimitInMegabytes} -> {info.paymentStatus.newTerms.storageLimitInMegabytes}