This commit is contained in:
Sebastian 2021-07-26 11:27:56 -03:00
parent e70e664da9
commit 44551245da
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
13 changed files with 272 additions and 41 deletions

View File

@ -45,7 +45,7 @@ function toI18nString(stringSeq: ReadonlyArray<string>): string {
/** /**
* Internationalize a string template with arbitrary serialized values. * 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 s = toI18nString(stringSeq);
const tr = jed const tr = jed
.translate(s) .translate(s)
@ -141,7 +141,9 @@ function stringifyArray(children: Array<any>): string {
} }
export const i18n = { export const i18n = {
str, str: singular,
singular,
Translate, Translate,
translate, translate,
}; };

View File

@ -14,15 +14,9 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { setupI18n } from "@gnu-taler/taler-util"
import { Fragment } from "preact" import { Fragment } from "preact"
import { strings } from '../src/i18n/strings.ts'
import { NavBar } from '../src/popup/popup' import { NavBar } from '../src/popup/popup'
import { TranslationProvider } from '../src/context/translation'
const mockConfig = {
backendURL: 'http://demo.taler.net',
currency: 'KUDOS'
}
export const parameters = { export const parameters = {
controls: { expanded: true }, controls: { expanded: true },
@ -38,7 +32,7 @@ export const globalTypes = {
icon: 'globe', icon: 'globe',
items: [ items: [
{ value: 'en', right: '🇺🇸', title: 'English' }, { value: 'en', right: '🇺🇸', title: 'English' },
{ value: 'es', right: '🇪🇸', title: 'Spanish' }, { value: 'de', right: '🇪🇸', title: 'German' },
], ],
}, },
}, },
@ -58,7 +52,7 @@ export const decorators = [
<Story /> <Story />
</div> </div>
} else { } else {
const path = !isTestingHeader ? /popup(\/.*)\/.*/.exec(kind)[1] : '' const path = !isTestingHeader ? /popup(\/.*).*/.exec(kind)[1] : ''
// add a fake header so it looks similar // add a fake header so it looks similar
return <Fragment> return <Fragment>
<NavBar path={path} devMode={path === '/dev'} /> <NavBar path={path} devMode={path === '/dev'} />
@ -113,9 +107,7 @@ export const decorators = [
<Story /> <Story />
</div> </div>
}, },
(Story, { globals }) => { (Story, { globals }) => <TranslationProvider initial='en' forceLang={globals.locale}>
setupI18n(globals.locale, strings); <Story />
return <Story /> </TranslationProvider>,
},
// (Story) => <ConfigContextProvider value={mockConfig}> <Story /> </ConfigContextProvider>
]; ];

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# This file is in the public domain. # This file is in the public domain.
[ "also-wallet" == "$1" ] && { pnpm -C ../taler-wallet-core/ compile || exit 1; } [ "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) pnpm clean && pnpm compile && rm -rf extension/ && ./pack.sh && (cd extension/ && unzip taler*.zip)

View File

@ -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 <http://www.gnu.org/licenses/>
*/
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 <select name={name} id="slct">
<option selected disabled>Choose an option</option>
{Object.keys(list)
.filter((l) => l !== value)
.map(key => <option value={key} key={key}>{list[key]}</option> )
}
</select>
}

View File

@ -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 <http://www.gnu.org/licenses/>
*/
/**
*
* @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<Type>(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);

View File

@ -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)
}

View File

@ -42,3 +42,24 @@ export function useLocalStorage(key: string, initialValue?: string): [string | u
return [storedValue, setValue]; return [storedValue, setValue];
} }
//TODO: merge with the above function
export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
const [storedValue, setStoredValue] = useState<string>((): 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];
}

View File

@ -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"] = { strings["fr"] = {
domain: "messages", domain: "messages",
locale_data: { locale_data: {

View File

@ -31,7 +31,7 @@ interface Props {
export function ProviderDetailPage({ pid, onBack }: Props): VNode { export function ProviderDetailPage({ pid, onBack }: Props): VNode {
const status = useProviderStatus(pid) const status = useProviderStatus(pid)
if (!status) { if (!status) {
return <div>Loading...</div> return <div><i18n.Translate>Loading...</i18n.Translate></div>
} }
if (!status.info) { if (!status.info) {
onBack() onBack()
@ -67,26 +67,26 @@ export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewP
<p>{daysSince(info?.lastSuccessfulBackupTimestamp)} </p> <p>{daysSince(info?.lastSuccessfulBackupTimestamp)} </p>
<p>{descriptionByStatus(info.paymentStatus)}</p> <p>{descriptionByStatus(info.paymentStatus)}</p>
{info.paymentStatus.type === ProviderPaymentType.TermsChanged && <div> {info.paymentStatus.type === ProviderPaymentType.TermsChanged && <div>
<p>terms has changed, extending the service will imply accepting the new terms of service</p> <p><i18n.Translate>terms has changed, extending the service will imply accepting the new terms of service</i18n.Translate></p>
<table> <table>
<thead> <thead>
<tr> <tr>
<td></td> <td></td>
<td>old</td> <td><i18n.Translate>old</i18n.Translate></td>
<td> -&gt;</td> <td> -&gt;</td>
<td>new</td> <td><i18n.Translate>new</i18n.Translate></td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>fee</td> <td><i18n.Translate>fee</i18n.Translate></td>
<td>{info.paymentStatus.oldTerms.annualFee}</td> <td>{info.paymentStatus.oldTerms.annualFee}</td>
<td>-&gt;</td> <td>-&gt;</td>
<td>{info.paymentStatus.newTerms.annualFee}</td> <td>{info.paymentStatus.newTerms.annualFee}</td>
</tr> </tr>
<tr> <tr>
<td>storage</td> <td><i18n.Translate>storage</i18n.Translate></td>
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td> <td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
<td>-&gt;</td> <td>-&gt;</td>
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td> <td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
@ -117,11 +117,11 @@ function daysSince(d?: Timestamp) {
const str = formatDuration(duration, { const str = formatDuration(duration, {
delimiter: ', ', delimiter: ', ',
format: [ format: [
duration?.years ? 'years' : ( duration?.years ? i18n.str`years` : (
duration?.months ? 'months' : ( duration?.months ? i18n.str`months` : (
duration?.days ? 'days' : ( duration?.days ? i18n.str`days` : (
duration?.hours ? 'hours' : ( duration?.hours ? i18n.str`hours` : (
duration?.minutes ? 'minutes' : 'seconds' duration?.minutes ? i18n.str`minutes` : i18n.str`seconds`
) )
) )
) )
@ -139,13 +139,13 @@ function Error({ info }: { info: ProviderInfo }) {
switch (info.backupProblem.type) { switch (info.backupProblem.type) {
case "backup-conflicting-device": case "backup-conflicting-device":
return <ErrorMessage title={<Fragment> return <ErrorMessage title={<Fragment>
There is conflict with another backup from <b>{info.backupProblem.otherDeviceId}</b> <i18n.Translate>There is conflict with another backup from <b>{info.backupProblem.otherDeviceId}</b></i18n.Translate>
</Fragment>} /> </Fragment>} />
case "backup-unreadable": case "backup-unreadable":
return <ErrorMessage title="Backup is not readable" /> return <ErrorMessage title="Backup is not readable" />
default: default:
return <ErrorMessage title={<Fragment> return <ErrorMessage title={<Fragment>
Unknown backup problem: {JSON.stringify(info.backupProblem)} <i18n.Translate>Unknown backup problem: {JSON.stringify(info.backupProblem)}</i18n.Translate>
</Fragment>} /> </Fragment>} />
} }
} }
@ -172,15 +172,15 @@ function colorByStatus(status: ProviderPaymentType) {
function descriptionByStatus(status: ProviderPaymentStatus) { function descriptionByStatus(status: ProviderPaymentStatus) {
switch (status.type) { switch (status.type) {
case ProviderPaymentType.InsufficientBalance: case ProviderPaymentType.InsufficientBalance:
return 'no enough balance to make the payment' return i18n.str`no enough balance to make the payment`
case ProviderPaymentType.Unpaid: case ProviderPaymentType.Unpaid:
return 'not paid yet' return i18n.str`not paid yet`
case ProviderPaymentType.Paid: case ProviderPaymentType.Paid:
case ProviderPaymentType.TermsChanged: case ProviderPaymentType.TermsChanged:
if (status.paidUntil.t_ms === 'never') { if (status.paidUntil.t_ms === 'never') {
return 'service paid.' return i18n.str`service paid`
} else { } 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: case ProviderPaymentType.Pending:
return '' return ''

View File

@ -15,18 +15,23 @@
*/ */
import { i18n } from "@gnu-taler/taler-util";
import { VNode } from "preact"; import { VNode } from "preact";
import { Checkbox } from "../components/Checkbox"; import { Checkbox } from "../components/Checkbox";
import { EditableText } from "../components/EditableText"; 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 { useBackupDeviceName } from "../hooks/useBackupDeviceName";
import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
import { useLang } from "../hooks/useLang";
export function SettingsPage(): VNode { export function SettingsPage(): VNode {
const [permissionsEnabled, togglePermissions] = useExtendedPermissions(); const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
const { devMode, toggleDevMode } = useDevContext() const { devMode, toggleDevMode } = useDevContext()
const { name, update } = useBackupDeviceName() const { name, update } = useBackupDeviceName()
const [lang, changeLang] = useLang()
return <SettingsView return <SettingsView
lang={lang} changeLang={changeLang}
deviceName={name} setDeviceName={update} deviceName={name} setDeviceName={update}
permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions} permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
developerMode={devMode} toggleDeveloperMode={toggleDevMode} developerMode={devMode} toggleDeveloperMode={toggleDevMode}
@ -34,6 +39,8 @@ export function SettingsPage(): VNode {
} }
export interface ViewProps { export interface ViewProps {
lang: string;
changeLang: (s: string) => void;
deviceName: string; deviceName: string;
setDeviceName: (s: string) => Promise<void>; setDeviceName: (s: string) => Promise<void>;
permissionsEnabled: boolean; permissionsEnabled: boolean;
@ -42,20 +49,43 @@ export interface ViewProps {
toggleDeveloperMode: () => void; 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 ( return (
<div> <div>
<section style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}> <section style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}>
<h2><i18n.Translate>Wallet</i18n.Translate></h2>
<h2>Wallet</h2> <SelectList
value={lang}
onChange={changeLang}
name="lang"
list={names}
label={i18n.str`Lang`}
description="(Choose your preferred lang)"
/>
<EditableText <EditableText
value={deviceName} value={deviceName}
onChange={setDeviceName} onChange={setDeviceName}
name="device-id" name="device-id"
label="Device name" label={i18n.str`Device name`}
description="(This is how you will recognize the wallet in the backup provider)" description="(This is how you will recognize the wallet in the backup provider)"
/> />
<h2>Permissions</h2> <h2><i18n.Translate>Permissions</i18n.Translate></h2>
<Checkbox label="Automatically open wallet based on page content" <Checkbox label="Automatically open wallet based on page content"
name="perm" name="perm"
description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)" description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)"

View File

@ -27,7 +27,7 @@
import { i18n } from "@gnu-taler/taler-util"; import { i18n } from "@gnu-taler/taler-util";
import { ComponentChildren, JSX } from "preact"; import { ComponentChildren, JSX } from "preact";
import Match from "preact-router/match"; import Match from "preact-router/match";
import { useDevContext } from "../context/useDevContext"; import { useDevContext } from "../context/devContext";
import { PopupNavigation } from '../components/styled' import { PopupNavigation } from '../components/styled'
export enum Pages { export enum Pages {

View File

@ -25,7 +25,7 @@ import { createHashHistory } from "history";
import { render } from "preact"; import { render } from "preact";
import Router, { route, Route } from "preact-router"; import Router, { route, Route } from "preact-router";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";
import { DevContextProvider } from "./context/useDevContext"; import { DevContextProvider } from "./context/devContext";
import { useTalerActionURL } from "./hooks/useTalerActionURL"; import { useTalerActionURL } from "./hooks/useTalerActionURL";
import { strings } from "./i18n/strings"; import { strings } from "./i18n/strings";
import { BackupPage } from "./popup/BackupPage"; import { BackupPage } from "./popup/BackupPage";