[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] 03/03: refactor i18n to support weblate
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] 03/03: refactor i18n to support weblate |
Date: |
Mon, 22 Feb 2021 23:09:15 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository merchant-backoffice.
commit ff30289ec79ae9ad1aa5fd20c1a496af8f5ea574
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Feb 22 19:08:56 2021 -0300
refactor i18n to support weblate
---
packages/frontend/.storybook/main.js | 23 +-
packages/frontend/.storybook/preview.js | 10 +-
packages/frontend/package.json | 5 +-
packages/frontend/preact.config.js | 22 +-
packages/frontend/src/components/auth/index.tsx | 6 +-
packages/frontend/src/components/modal/index.tsx | 8 +-
packages/frontend/src/components/navbar/index.tsx | 9 +-
.../notifications/Notifications.stories.tsx | 9 +-
.../src/components/notifications/index.tsx | 20 +-
packages/frontend/src/components/yup/YupField.tsx | 69 +++--
packages/frontend/src/custom.d.ts | 8 +
packages/frontend/src/declaration.d.ts | 16 +-
packages/frontend/src/hooks/backend.ts | 25 +-
packages/frontend/src/hooks/index.ts | 2 +-
packages/frontend/src/i18n/index.ts | 285 ---------------------
packages/frontend/src/index.tsx | 61 +++--
packages/frontend/src/messages/en.po | 155 +++++++++++
packages/frontend/src/messages/es.po | 22 ++
packages/frontend/src/messages/index.ts | 3 +
.../src/routes/instances/create/CreatePage.tsx | 12 +-
.../src/routes/instances/details/DetailPage.tsx | 12 +-
.../src/routes/instances/details/index.tsx | 8 +-
.../src/routes/instances/list/CardTable.tsx | 4 +-
.../src/routes/instances/list/EmptyTable.tsx | 5 +-
.../frontend/src/routes/instances/list/Table.tsx | 10 +-
.../frontend/src/routes/instances/list/View.tsx | 8 +-
.../frontend/src/routes/instances/list/index.tsx | 8 +-
.../src/routes/instances/update/UpdatePage.tsx | 12 +-
.../frontend/src/routes/instances/update/index.tsx | 4 +-
packages/frontend/src/schemas/index.ts | 32 +--
packages/frontend/tests/hooks/notification.test.ts | 2 +-
packages/preact-message/.gitignore | 2 +
packages/preact-message/CHANGELOG.md | 16 ++
packages/preact-message/LICENSE | 20 ++
packages/preact-message/README.md | 126 +++++++++
packages/preact-message/package.json | 39 +++
packages/preact-message/src/MessageProvider.ts | 183 +++++++++++++
packages/preact-message/src/declarations.d.ts | 3 +
packages/preact-message/src/get-message.ts | 87 +++++++
packages/preact-message/src/index.ts | 32 +++
packages/preact-message/src/message-context.ts | 72 ++++++
packages/preact-message/src/message-error.ts | 22 ++
packages/preact-message/src/message.ts | 89 +++++++
packages/preact-message/src/use-locales.ts | 44 ++++
packages/preact-message/src/use-message-getter.ts | 47 ++++
.../preact-message/src/use-message-template.ts | 31 +++
packages/preact-message/src/use-message.ts | 58 +++++
packages/preact-message/tsconfig.json | 60 +++++
pnpm-lock.yaml | 112 +++++---
49 files changed, 1418 insertions(+), 500 deletions(-)
diff --git a/packages/frontend/.storybook/main.js
b/packages/frontend/.storybook/main.js
index 4b3f58f..7dc5cc2 100644
--- a/packages/frontend/.storybook/main.js
+++ b/packages/frontend/.storybook/main.js
@@ -29,5 +29,26 @@ module.exports = {
"@storybook/preset-scss",
// "@storybook/addon-a11y",
"@storybook/addon-essentials" //docs, control, actions, viewpot, toolbar,
background
- ]
+ ],
+ webpackFinal: async (config, { configType }) => {
+ // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
+ // You can change the configuration based on that.
+ // 'PRODUCTION' is used when building the static version of storybook.
+
+ // Make whatever fine-grained changes you need
+ config.module.rules.push({
+ test: [/\.pot?$/, /\.mo$/],
+ loader: require.resolve('messageformat-po-loader'),
+ options: {
+ biDiSupport: false,
+ defaultCharset: null,
+ defaultLocale: 'en',
+ forceContext: false,
+ pluralFunction: null,
+ verbose: false
+ }
+ });
+ // Return the altered config
+ return config;
+ },
}
\ No newline at end of file
diff --git a/packages/frontend/.storybook/preview.js
b/packages/frontend/.storybook/preview.js
index 2c732ec..48c87c7 100644
--- a/packages/frontend/.storybook/preview.js
+++ b/packages/frontend/.storybook/preview.js
@@ -1,8 +1,8 @@
import "../src/scss/main.scss"
-import { IntlProvider } from 'preact-i18n';
-import { h } from "preact";
-import { translations } from '../src/i18n'
+import { MessageProvider } from "preact-messages";
import { ConfigContext } from '../src/context/backend'
+import * as messages from '../src/messages'
+import { h } from 'preact';
const mockConfig = {
backendURL: 'http://demo.taler.net',
@@ -31,9 +31,9 @@ export const globalTypes = {
export const decorators = [
(Story, { globals }) => {
- return <IntlProvider definition={translations[globals.locale]} mark>
+ return <MessageProvider locale={globals.locale} onError="warn"
messages={messages[globals.locale]} >
<Story />
- </IntlProvider>
+ </MessageProvider>
},
(Story) => <ConfigContext.Provider value={mockConfig}> <Story />
</ConfigContext.Provider>
];
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 78ed6ee..07a7191 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -31,9 +31,10 @@
"dependencies": {
"axios": "^0.21.1",
"date-fns": "^2.17.0",
+ "messageformat": "^2.3.0",
"preact": "^10.3.1",
- "preact-i18n": "2.3.1-preactx",
"preact-router": "^3.2.1",
+ "preact-messages": "workspace:*",
"swr": "^0.4.1",
"yup": "^0.32.8"
},
@@ -51,7 +52,6 @@
"@testing-library/preact-hooks": "^1.1.0",
"@types/enzyme": "^3.10.5",
"@types/jest": "^26.0.8",
- "@types/preact-i18n": "^2.3.0",
"@typescript-eslint/eslint-plugin": "^4.15.1",
"@typescript-eslint/parser": "^4.15.1",
"ava": "^3.15.0",
@@ -69,6 +69,7 @@
"eslint-config-preact": "^1.1.1",
"jest": "^26.2.2",
"jest-preset-preact": "^4.0.2",
+ "messageformat-po-loader": "^0.3.0",
"node-sass": "^5.0.0",
"preact-cli": "^3.0.5",
"preact-render-to-string": "^5.1.4",
diff --git a/packages/frontend/preact.config.js
b/packages/frontend/preact.config.js
index de2debe..5e42bc6 100644
--- a/packages/frontend/preact.config.js
+++ b/packages/frontend/preact.config.js
@@ -26,10 +26,22 @@
export default {
webpack(config, env, helpers, options) {
config.node.process = 'mock'
- // config.plugins.push(
- // new DefinePlugin({
- // // 'process.env.BACKEND_ENDPOINT':
JSON.stringify(parsed['BACKEND_ENDPOINT']),
- // }),
- // );
+ config.resolve.extensions.push('.po');
+ config.module.rules.push({
+ enforce: 'pre',
+ test: /\.po$/,
+ use: [{
+ loader: 'messageformat-po-loader',
+ options: {
+ biDiSupport: false,
+ defaultCharset: null,
+ defaultLocale: 'en',
+ forceContext: false,
+ pluralFunction: null,
+ verbose: false
+ }
+ }],
+ });
+
}
}
diff --git a/packages/frontend/src/components/auth/index.tsx
b/packages/frontend/src/components/auth/index.tsx
index 8fdccbf..2688586 100644
--- a/packages/frontend/src/components/auth/index.tsx
+++ b/packages/frontend/src/components/auth/index.tsx
@@ -20,7 +20,7 @@
*/
import { h, VNode } from "preact";
-import { Text } from "preact-i18n";
+import { Message } from "preact-messages";
import { useContext, useState } from "preact/hooks";
import { BackendContext } from "../../context/backend";
import { Notification } from "../../declaration";
@@ -44,8 +44,8 @@ export function LoginModal({ onConfirm, withMessage }:
Props): VNode {
<div class="columns is-vcentered">
<div class="column is-12">
<div>
- <p><Text id={`notification.${withMessage.messageId}.title`} />
</p>
- <Text id={`notification.${withMessage.messageId}.description`}
fields={withMessage.params} />
+ <p>{withMessage.message}</p>
+ {withMessage.description}
</div>
</div>
</div>
diff --git a/packages/frontend/src/components/modal/index.tsx
b/packages/frontend/src/components/modal/index.tsx
index e3c957c..6ba0101 100644
--- a/packages/frontend/src/components/modal/index.tsx
+++ b/packages/frontend/src/components/modal/index.tsx
@@ -20,7 +20,7 @@
*/
import { h, VNode } from "preact";
-import { Text } from "preact-i18n";
+import { Message } from "preact-messages";
interface Props {
active?: boolean;
@@ -36,15 +36,15 @@ export function ConfirmModal({ active, description,
onCancel, onConfirm, childre
<div class="modal-background " onClick={onCancel} />
<div class="modal-card">
<header class="modal-card-head">
- <p class="modal-card-title"> <Text id="confirm_modal.title" /> {
!description ? null : <Text id={`confirm_modal.${description}`} /> }</p>
+ <p class="modal-card-title"> <Message id="confirm_modal.title" /> {
!description ? null : <Message id={`confirm_modal.${description}`} /> }</p>
<button class="delete " aria-label="close" onClick={onCancel} />
</header>
<section class="modal-card-body">
{children}
</section>
<footer class="modal-card-foot">
- <button class="button " onClick={onCancel} ><Text id="text.cancel"
/></button>
- <button class={danger ? "button is-danger " : "button is-info "}
onClick={onConfirm} ><Text id="text.confirm" /></button>
+ <button class="button " onClick={onCancel} ><Message id="Cancel"
/></button>
+ <button class={danger ? "button is-danger " : "button is-info "}
onClick={onConfirm} ><Message id="Confirm" /></button>
</footer>
</div>
<button class="modal-close is-large " aria-label="close"
onClick={onCancel} />
diff --git a/packages/frontend/src/components/navbar/index.tsx
b/packages/frontend/src/components/navbar/index.tsx
index ec00c70..08ac6d0 100644
--- a/packages/frontend/src/components/navbar/index.tsx
+++ b/packages/frontend/src/components/navbar/index.tsx
@@ -20,9 +20,8 @@
*/
import { h, VNode } from 'preact';
-import { translations } from '../../i18n'
-// TODO: Fix compilation problem
-// import * as logo from '../../assets/logo.jpeg';
+import * as messages from '../../messages'
+import logo from '../../assets/logo.jpeg';
interface Props {
lang: string;
@@ -34,7 +33,7 @@ export function NavigationBar({ lang, setLang, onLogout }:
Props): VNode {
return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main
navigation">
<div class="navbar-brand">
<a class="navbar-item" href="https://taler.net">
- <img src="https://taler.net/static/images/logo-2020.jpg" style={{
height: 50, maxHeight: 50 }} />
+ <img src={logo} style={{ height: 50, maxHeight: 50 }} />
</a>
<a role="button" class="navbar-burger" aria-label="menu"
aria-expanded="false" data-target="navbarBasicExample">
@@ -51,7 +50,7 @@ export function NavigationBar({ lang, setLang, onLogout }:
Props): VNode {
<div class="control has-icons-left">
<div class="select">
<select onChange={(e): void => setLang(e.currentTarget.value)}>
- {Object.keys(translations).map(l => <option selected={lang ===
l} value={l}>{l}</option>)}
+ {Object.keys(messages).map(l => <option selected={lang === l}
value={l}>{l}</option>)}
</select>
</div>
<div class="icon is-small is-left">
diff --git
a/packages/frontend/src/components/notifications/Notifications.stories.tsx
b/packages/frontend/src/components/notifications/Notifications.stories.tsx
index 734644d..17043bf 100644
--- a/packages/frontend/src/components/notifications/Notifications.stories.tsx
+++ b/packages/frontend/src/components/notifications/Notifications.stories.tsx
@@ -34,21 +34,24 @@ export default {
export const Info = (a: any) => <Notifications {...a} />;
Info.args = {
notifications: [{
- messageId: 'unauthorized',
+ message: 'Title',
+ description: 'Some large description',
type: 'INFO',
}]
}
export const Warn = (a: any) => <Notifications {...a} />;
Warn.args = {
notifications: [{
- messageId: 'unauthorized',
+ message: 'Title',
+ description: 'Some large description',
type: 'WARN',
}]
}
export const Error = (a: any) => <Notifications {...a} />;
Error.args = {
notifications: [{
- messageId: 'unauthorized',
+ message: 'Title',
+ description: 'Some large description',
type: 'ERROR',
}]
}
diff --git a/packages/frontend/src/components/notifications/index.tsx
b/packages/frontend/src/components/notifications/index.tsx
index a5450fd..045b8e0 100644
--- a/packages/frontend/src/components/notifications/index.tsx
+++ b/packages/frontend/src/components/notifications/index.tsx
@@ -14,13 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
import { h, VNode } from "preact";
-import { Text } from "preact-i18n";
+import { Message } from "preact-messages";
import { MessageType, Notification } from "../../declaration";
interface Props {
@@ -42,12 +42,12 @@ export function Notifications({ notifications,
removeNotification }: Props): VNo
return <div class="toast">
{notifications.map(n => <article class={messageStyle(n.type)}>
<div class="message-header">
- <p><Text id={`notification.${n.messageId}.title`} /> </p>
- <button class="delete" onClick={()=> removeNotification &&
removeNotification(n)} />
- </div>
- <div class="message-body">
- <Text id={`notification.${n.messageId}.description`} fields={n.params}
/>
+ <p>{n.message}</p>
+ <button class="delete" onClick={() => removeNotification &&
removeNotification(n)} />
</div>
+ {n.description && <div class="message-body">
+ {n.description}
+ </div>}
</article>)}
</div>
}
\ No newline at end of file
diff --git a/packages/frontend/src/components/yup/YupField.tsx
b/packages/frontend/src/components/yup/YupField.tsx
index ea15bda..49a8e30 100644
--- a/packages/frontend/src/components/yup/YupField.tsx
+++ b/packages/frontend/src/components/yup/YupField.tsx
@@ -20,7 +20,7 @@
*/
import { h, VNode } from "preact";
-import { Text, useText } from "preact-i18n";
+import { Message, useMessage } from "preact-messages";
import { StateUpdater, useContext, useState } from "preact/hooks";
import { intervalToDuration, formatDuration } from 'date-fns'
import { BackendContext, ConfigContext } from '../../context/backend';
@@ -96,7 +96,7 @@ function YupObjectInput({ name, info, value, errors, onChange
}: PropsObject): V
return <div class="card">
<header class="card-header">
<p class="card-header-title">
- <Text id={`fields.instance.${name}.label`} />
+ <Message id={`fields.instance.${name}.label`} />
</p>
<button class="card-header-icon" aria-label="more options" onClick={():
void => setActive(!active)}>
<span class="icon">
@@ -118,16 +118,14 @@ function YupObjectInput({ name, info, value, errors,
onChange }: PropsObject): V
}
function YupInput({ name, readonly, value, errors, onChange }:
PropsInputInternal): VNode {
- const dict = useText({
- placeholder: `fields.instance.${name}.placeholder`,
- tooltip: `fields.instance.${name}.tooltip`,
- })
+ const placeholder = useMessage(`fields.instance.${name}.placeholder`)
+ const tooltip = useMessage(`fields.instance.${name}.tooltip`)
return <div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
- <Text id={`fields.instance.${name}.label`} />
- {dict.tooltip && <span class="icon" data-tooltip={dict.tooltip}>
+ <Message id={`fields.instance.${name}.label`} />
+ {tooltip && <span class="icon" data-tooltip={tooltip}>
<i class="mdi mdi-information" />
</span>}
</label>
@@ -136,13 +134,13 @@ function YupInput({ name, readonly, value, errors,
onChange }: PropsInputInterna
<div class="field">
<p class="control">
<input class={errors[name] ? "input is-danger" : "input"} type="text"
- placeholder={dict.placeholder} readonly={readonly}
+ placeholder={placeholder} readonly={readonly}
name={name} value={value}
onChange={(e): void => onChange(e.currentTarget.value)} />
- <Text id={`fields.instance.${name}.help`} />
+ <Message id={`fields.instance.${name}.help`} > </Message>
</p>
{errors[name] ? <p class="help is-danger">
- <Text id={`validation.${errors[name].type}`}
fields={errors[name].params}>{errors[name].message}</Text>
+ <Message id={`validation.${errors[name].type}`}
fields={errors[name].params}>{errors[name].message} </Message>
</p> : null}
</div>
</div>
@@ -150,18 +148,17 @@ function YupInput({ name, readonly, value, errors,
onChange }: PropsInputInterna
}
function YupInputArray({ name, readonly, value, errors, onChange }:
PropsInputInternal): VNode {
- const dict = useText({
- placeholder: `fields.instance.${name}.placeholder`,
- tooltip: `fields.instance.${name}.tooltip`,
- })
+ const placeholder = useMessage(`fields.instance.${name}.placeholder`)
+ const tooltip = useMessage(`fields.instance.${name}.tooltip`)
+
const array = value as unknown as string[] || []
const [currentValue, setCurrentValue] = useState('')
return <div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
- <Text id={`fields.instance.${name}.label`} />
- {dict.tooltip && <span class="icon" data-tooltip={dict.tooltip}>
+ <Message id={`fields.instance.${name}.label`} />
+ {tooltip && <span class="icon" data-tooltip={tooltip}>
<i class="mdi mdi-information" />
</span>}
</label>
@@ -177,14 +174,14 @@ function YupInputArray({ name, readonly, value, errors,
onChange }: PropsInputIn
</p>
<p class="control">
<input class={errors[name] ? "input is-danger" : "input"}
type="text"
- placeholder={dict.placeholder} readonly={readonly}
+ placeholder={placeholder} readonly={readonly}
name={name} value={currentValue}
onChange={(e): void => setCurrentValue(e.currentTarget.value)} />
- <Text id={`fields.instance.${name}.help`} />
+ <Message id={`fields.instance.${name}.help`} > </Message>
</p>
</div>
{errors[name] ? <p class="help is-danger">
- <Text id={`validation.${errors[name].type}`}
fields={errors[name].params}>{errors[name].message}</Text>
+ <Message id={`validation.${errors[name].type}`}
fields={errors[name].params}>{errors[name].message}</Message>
</p> : null}
{array.map(v => <div class="tags has-addons">
<span class="tag is-medium is-info">{v}</span>
@@ -201,16 +198,14 @@ function YupInputArray({ name, readonly, value, errors,
onChange }: PropsInputIn
}
function YupInputWithAddon({ name, readonly, value, errors, onChange, addon,
atTheEnd }: PropsInputInternal & { addon: string; atTheEnd?: boolean }): VNode {
- const dict = useText({
- placeholder: `fields.instance.${name}.placeholder`,
- tooltip: `fields.instance.${name}.tooltip`,
- })
+ const placeholder = useMessage(`fields.instance.${name}.placeholder`)
+ const tooltip = useMessage(`fields.instance.${name}.tooltip`)
return <div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
- <Text id={`fields.instance.${name}.label`} />
- {dict.tooltip && <span class="icon" data-tooltip={dict.tooltip}>
+ <Message id={`fields.instance.${name}.label`} />
+ {tooltip && <span class="icon" data-tooltip={tooltip}>
<i class="mdi mdi-information" />
</span>}
</label>
@@ -223,34 +218,32 @@ function YupInputWithAddon({ name, readonly, value,
errors, onChange, addon, atT
</div>}
<p class="control is-expanded">
<input class={errors[name] ? "input is-danger" : "input"}
type="text"
- placeholder={dict.placeholder} readonly={readonly}
+ placeholder={placeholder} readonly={readonly}
name={name} value={value}
onChange={(e): void => onChange(e.currentTarget.value)} />
- <Text id={`fields.instance.${name}.help`} />
+ <Message id={`fields.instance.${name}.help`} > </Message>
</p>
{atTheEnd && <div class="control">
<a class="button is-static">{addon}</a>
</div>}
</div>
- {errors[name] ? <p class="help is-danger"><Text
id={`validation.${errors[name].type}`}
fields={errors[name].params}>{errors[name].message}</Text></p> : null}
+ {errors[name] ? <p class="help is-danger"><Message
id={`validation.${errors[name].type}`}
fields={errors[name].params}>{errors[name].message}</Message></p> : null}
</div>
</div>
</div>
}
function YupInputSecured({ name, readonly, value, errors, onChange }:
PropsInputInternal): VNode {
- const dict = useText({
- placeholder: `fields.instance.${name}.placeholder`,
- tooltip: `fields.instance.${name}.tooltip`,
- })
+ const placeholder = useMessage(`fields.instance.${name}.placeholder`, {})
+ const tooltip = useMessage(`fields.instance.${name}.tooltip`, {})
const [active, setActive] = useState(false)
return <div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
- <Text id={`fields.instance.${name}.label`} />
- {dict.tooltip && <span class="icon" data-tooltip={dict.tooltip}>
+ <Message id={`fields.instance.${name}.label`} />
+ {tooltip && <span class="icon" data-tooltip={tooltip}>
<i class="mdi mdi-information" />
</span>}
</label>
@@ -264,14 +257,14 @@ function YupInputSecured({ name, readonly, value, errors,
onChange }: PropsInput
</label>
<p class="control">
<input class="input" type="text"
- placeholder={dict.placeholder} readonly={readonly || !active}
+ placeholder={placeholder} readonly={readonly || !active}
disabled={readonly || !active}
name={name} value={value}
onChange={(e): void => onChange(e.currentTarget.value)} />
- <Text id={`fields.instance.${name}.help`} />
+ <Message id={`fields.instance.${name}.help`}> </Message>
</p>
</div>
- {errors[name] ? <p class="help is-danger"><Text
id={`validation.${errors[name].type}`}
fields={errors[name].params}>{errors[name].message}</Text></p> : null}
+ {errors[name] ? <p class="help is-danger"><Message
id={`validation.${errors[name].type}`}
fields={errors[name].params}>{errors[name].message}</Message></p> : null}
</div>
</div>
</div>
diff --git a/packages/frontend/src/custom.d.ts
b/packages/frontend/src/custom.d.ts
new file mode 100644
index 0000000..bdf59ac
--- /dev/null
+++ b/packages/frontend/src/custom.d.ts
@@ -0,0 +1,8 @@
+declare module '*.po' {
+ const content: any;
+ export default content;
+}
+declare module "*.jpeg" {
+ const content: any;
+ export default content;
+}
diff --git a/packages/frontend/src/declaration.d.ts
b/packages/frontend/src/declaration.d.ts
index faa6cd2..a81260a 100644
--- a/packages/frontend/src/declaration.d.ts
+++ b/packages/frontend/src/declaration.d.ts
@@ -19,26 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-declare module "*.css" {
- const mapping: Record<string, string>;
- export default mapping;
-}
-declare module "*.jpeg" {
- const mapping: Record<string, string>;
- export default mapping;
-}
-
-declare module "*.scss" {
- const mapping: Record<string, string>;
- export default mapping;
-}
interface KeyValue {
[key: string]: string;
}
interface Notification {
- messageId: string;
+ message: string;
+ description?: string;
type: MessageType;
params?: any;
}
diff --git a/packages/frontend/src/hooks/backend.ts
b/packages/frontend/src/hooks/backend.ts
index 75246c8..e2b696b 100644
--- a/packages/frontend/src/hooks/backend.ts
+++ b/packages/frontend/src/hooks/backend.ts
@@ -24,17 +24,24 @@ import axios from 'axios'
import { MerchantBackend } from '../declaration';
import { useContext } from 'preact/hooks';
import { BackendContext, InstanceContext } from '../context/backend';
-import { useBackendInstanceToken } from '.';
-type HttpResponse<T> = HttpResponseOk<T> | HttpResponseError<T>;
+type HttpResponse<T> = HttpResponseOk<T> | HttpResponseError;
interface HttpResponseOk<T> {
data: T;
}
-interface HttpResponseError<T> {
+
+export interface SwrError {
+ info: any,
+ status: number,
+ message: string,
+ backend: string,
+ hasToken: boolean,
+}
+interface HttpResponseError {
data: undefined;
unauthorized: boolean;
- error: Error;
+ error?: SwrError;
}
@@ -46,6 +53,8 @@ interface RequestOptions {
data?: any;
}
+
+
async function request(url: string, options: RequestOptions = {}):
Promise<any> {
const headers = options.token ? { Authorization: `${options.token}` } :
undefined
@@ -61,7 +70,7 @@ async function request(url: string, options: RequestOptions =
{}): Promise<any>
} catch (e) {
const info = e.response?.data
const status = e.response?.status
- throw { info, status, error: e, backend: url, hasToken: !!options.token }
+ throw { info, status, message: e.message, backend: url, hasToken:
!!options.token }
}
}
@@ -123,7 +132,7 @@ export function useBackendInstanceMutateAPI():
BackendInstaceMutateAPI {
export function useBackendInstances():
HttpResponse<MerchantBackend.Instances.InstancesResponse> {
const { url, token } = useContext(BackendContext)
- const { data, error } =
useSWR<MerchantBackend.Instances.InstancesResponse>(['/private/instances',
token, url], fetcher)
+ const { data, error } = useSWR<MerchantBackend.Instances.InstancesResponse,
SwrError>(['/private/instances', token, url], fetcher)
return { data, unauthorized: error?.status === 401, error }
}
@@ -131,14 +140,14 @@ export function useBackendInstances():
HttpResponse<MerchantBackend.Instances.In
export function useBackendInstance():
HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
const { url } = useContext(BackendContext);
const { id, token } = useContext(InstanceContext);
- const { data, error } =
useSWR<MerchantBackend.Instances.QueryInstancesResponse>([`/private/instances/${id}`,
token, url], fetcher)
+ const { data, error } =
useSWR<MerchantBackend.Instances.QueryInstancesResponse,
SwrError>([`/private/instances/${id}`, token, url], fetcher)
return { data, unauthorized: error?.status === 401, error }
}
export function useBackendConfig():
HttpResponse<MerchantBackend.VersionResponse> {
const { url, token } = useContext(BackendContext)
- const { data, error } = useSWR<MerchantBackend.VersionResponse>(['/config',
token, url], fetcher, {
+ const { data, error } = useSWR<MerchantBackend.VersionResponse,
SwrError>(['/config', token, url], fetcher, {
shouldRetryOnError: false
})
diff --git a/packages/frontend/src/hooks/index.ts
b/packages/frontend/src/hooks/index.ts
index 12d65d4..69a5e8e 100644
--- a/packages/frontend/src/hooks/index.ts
+++ b/packages/frontend/src/hooks/index.ts
@@ -23,7 +23,7 @@ import { StateUpdater, useEffect, useState } from
"preact/hooks";
import { mutate } from 'swr';
export function useBackendURL(): [string, StateUpdater<string>] {
- return useNotNullLocalStorage('backend-url', window.location.origin)
+ return useNotNullLocalStorage('backend-url', typeof window !== 'undefined' ?
window.location.origin : '')
}
export function useBackendDefaultToken(): [string | undefined,
StateUpdater<string | undefined>] {
return useLocalStorage('backend-token')
diff --git a/packages/frontend/src/i18n/index.ts
b/packages/frontend/src/i18n/index.ts
deleted file mode 100644
index f497f81..0000000
--- a/packages/frontend/src/i18n/index.ts
+++ /dev/null
@@ -1,285 +0,0 @@
-/*
- 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)
- */
-
-export const translations = {
- es: {
- confirm_modal: {
- title: 'confirmar accion',
- create_instance: 'crear instancia',
- delete_instance: 'borrar instancia',
- update_instance: 'actualizar instancia',
- },
- notification: {
- unauthorized: {
- title: 'acceso no autorizado',
- description: 'el servidor a denegado el acceso'
- },
- create_error: {
- title: 'error creando',
- description: 'la creación no se efectuó correctamente. el servidor
dice: {{message}}'
- },
- create_success: {
- title: 'creación correcta',
- description: 'la creación se efectuó correctamente'
- },
- update_error: {
- title: 'error actualizando',
- description: 'la actualizacion no se efectuó correctamente. el
servidor dice: {{message}}'
- },
- update_success: {
- title: 'actualización correcta',
- description: 'la actualizacion se efectuó correctamente'
- },
- delete_error: {
- title: 'error eliminando',
- description: 'la eliminación no se efectuó correctamente. el servidor
dice: {{message}}'
- },
- delete_success: {
- title: 'eliminación correcta',
- description: 'la eliminación se efectuó correctamente'
- },
- },
- cancel: 'cancelar',
- confirm: 'confirmar',
- fields: {
- instance: {
- id: {
- label: 'Id',
- },
- merchant_pub: {
- label: 'Clave pública'
- },
- payment_targets: {
- label: 'Dirección de pago',
- },
- name: {
- label: 'Nombre',
- },
- payto_uris: {
- label: 'PaytTO URI',
- placeholder: 'valores separados por coma',
- help: 'example: payto://<authority>/<path>/<name>',
- },
- default_max_deposit_fee: {
- label: 'Máximo pago por depósito',
- },
- default_max_wire_fee: {
- label: 'Máximo pago por transferencia bancaria',
- },
- default_wire_fee_amortization: {
- label: 'Amortización de pago',
- },
- default_pay_delay: {
- label: 'Tiempo de espera de pago'
- },
- default_wire_transfer_delay: {
- label: 'Tiempo de espera de transferencia bancaria'
- },
- },
- },
- validation: {
- required: '{{label}} es obligatorio',
- typeError: '{{label}}',
- payto: 'la dirección de pago no es valida',
- },
- text: {
- instances: 'Instancias',
- merchant: 'Merchant',
- list_of_configured_instances: 'Lista de instancias configuradas',
- instance: {
- empty_list: 'No hay instancias configuradas, puede crear una usando el
boton + ',
- }
- }
- },
- en: {
- confirm_modal: {
- title: 'confirm action',
- create_instance: 'create instance',
- delete_instance: 'delete instance',
- update_instance: 'update instance',
- },
- notification: {
- unauthorized: {
- title: 'Could not access the backend',
- description: 'backend has denied access, try using another token'
- },
- error: {
- title: 'Error query the backend',
- description: 'Got message: "{{error.message}}" from: {{backend}}
(hasToken: {{hasToken}})'
- },
- no_server: {
- title: 'Could not access the backend',
- description: `There was a problem trying to reach the backend. \n Got
message: "{{error.message}}" from: {{backend}} (hasToken: {{hasToken}})`
- },
- create_error: {
- title: 'create error',
- description: 'the create process went wrong, server says:
{{info.hint}}'
- },
- create_success: {
- title: 'create success',
- description: 'the create process completed'
- },
- update_error: {
- title: 'update error',
- description: 'the update process went wrong, server says:
{{info.hint}}'
- },
- update_success: {
- title: 'update success',
- description: 'the update process completed'
- },
- delete_error: {
- title: 'delete error',
- description: 'the delete process went wrong, server says:
{{info.hint}}'
- },
- delete_success: {
- title: 'delete success',
- description: 'the delete process completed'
- },
- },
- fields: {
- instance: {
- id: {
- label: 'Id',
- },
- auth_token: {
- label: 'Auth Token',
- },
- merchant_pub: {
- label: 'Public Key'
- },
- payment_targets: {
- label: 'Payment targets',
- },
- name: {
- label: 'Business Name',
- tooltip: 'the name of the merchant instance'
- },
- payto_uris: {
- label: 'Bank accounts',
- tooltip: 'Bank account URI',
- help: 'payto://x-taler-bank/bank.taler:5882/blogger',
- },
- default_max_deposit_fee: {
- label: 'Max deposit fee',
- },
- default_max_wire_fee: {
- label: 'Max wire fee',
- },
- default_wire_fee_amortization: {
- label: 'Max fee amortization',
- },
- default_pay_delay: {
- label: 'Pay delay',
- tooltip: 'value expressed in seconds',
- },
- default_wire_transfer_delay: {
- label: 'Wire transfer delay',
- tooltip: 'value expressed in seconds',
- },
- address: {
- label: 'Address',
- country: {
- label: 'Country',
- },
- country_subdivision: {
- label: 'Country subdivision',
- },
- town: {
- label: 'Town',
- },
- district: {
- label: 'District',
- },
- town_location: {
- label: 'Town Location',
- },
- post_code: {
- label: 'Post code',
- },
- street: {
- label: 'Street',
- },
- building_name: {
- label: 'Building name',
- },
- building_number: {
- label: 'Building number',
- },
- address_lines: {
- label: 'Address lines',
- }
- },
- jurisdiction: {
- label: 'Jurisdiction',
- country: {
- label: 'Country',
- },
- country_subdivision: {
- label: 'Country subdivision',
- },
- town: {
- label: 'Town',
- },
- district: {
- label: 'District',
- },
- town_location: {
- label: 'Town Location',
- },
- post_code: {
- label: 'Post code',
- },
- street: {
- label: 'Street',
- },
- building_name: {
- label: 'Building name',
- },
- building_number: {
- label: 'Building number',
- },
- address_lines: {
- label: 'Address lines',
- }
- }
- }
- },
- validation: {
- required: '{{label}} is required',
- typeError: '{{label}}',
- payto: 'the pay address is not valid',
- },
- text: {
- instances: 'Instances',
- merchant: 'Merchant',
- list_of_configured_instances: 'List of configured instances',
- create_new_instance: 'Create new instance',
-
- cancel: 'cancel',
- confirm: 'confirm',
- delete: 'delete',
- update: 'update',
- instance: {
- empty_list: 'No instance configured yet, setup one pressing the +
button',
- }
- },
- },
-}
\ No newline at end of file
diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx
index 0b0b48c..ea2ef54 100644
--- a/packages/frontend/src/index.tsx
+++ b/packages/frontend/src/index.tsx
@@ -22,19 +22,19 @@
import "./scss/main.scss"
import { h, VNode } from 'preact';
-import { StateUpdater, useCallback, useContext, useEffect, useState } from
"preact/hooks";
+import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { Route, Router, route } from 'preact-router';
-import { IntlProvider } from 'preact-i18n';
+import { MessageError, MessageProvider, useMessageTemplate } from
'preact-messages';
import { Notification } from "./declaration";
import { Sidebar } from './components/sidebar';
import { NavigationBar } from './components/navbar';
import { Notifications } from './components/notifications';
-import { translations } from './i18n';
+import * as messages from './messages'
import { useBackendURL, useBackendDefaultToken, useLang,
useBackendInstanceToken } from './hooks';
import { useNotifications } from "./hooks/notifications";
import { BackendContext, ConfigContext, InstanceContext } from
'./context/backend';
-import { useBackendConfig } from "./hooks/backend";
+import { SwrError, useBackendConfig } from "./hooks/backend";
import NotFoundPage from './routes/notfound';
import Login from './routes/login';
@@ -68,12 +68,13 @@ function AppRouting(): VNode {
const { notifications, pushNotification, removeNotification } =
useNotifications()
const { lang, setLang, changeBackend, updateToken } =
useContext(BackendContext)
const backendConfig = useBackendConfig();
+ const i18n = useMessageTemplate('')
const LoginWithError = () => <Login
withMessage={{
- messageId: 'no_server',
+ message: i18n`Couldnt access the server`,
type: 'ERROR',
- params: !backendConfig.data ? backendConfig.error : {}
+ description: !backendConfig.data && backendConfig.error ? i18n`Got
message: ${backendConfig.error.message} from: ${backendConfig.error.backend}
(hasToken: ${backendConfig.error.hasToken})` : undefined,
}}
onConfirm={(url: string, token?: string) => {
changeBackend(url)
@@ -108,6 +109,7 @@ function AppReady({ pushNotification,addTokenCleaner }: {
pushNotification: (n:
changeBackend(url)
if (token) updateToken(token)
}
+ const i18n = useMessageTemplate('')
return <Router>
<Route path={RootPages.root} component={Redirect} to={RootPages.instances}
/>
@@ -124,14 +126,14 @@ function AppReady({ pushNotification,addTokenCleaner }: {
pushNotification: (n:
}}
onUnauthorized={() => <Login
- withMessage={{ messageId: 'unauthorized', type: 'ERROR', }}
+ withMessage={{ message: i18n`unauthorized`, type: 'ERROR', }}
onConfirm={updateLoginStatus}
/>}
- onError={(error: Error) => {
- pushNotification({ messageId: 'error', params: error, type: 'ERROR' })
+ onError={(error: SwrError) => {
+ pushNotification({ message: i18n`error`, params: error, type: 'ERROR'
})
return <div />
- }}
+ }}
/>
<Route path={RootPages.new}
@@ -139,12 +141,12 @@ function AppReady({ pushNotification,addTokenCleaner }: {
pushNotification: (n:
onBack={() => route(RootPages.instances)}
onConfirm={() => {
- pushNotification({ messageId: 'create_success', type: 'SUCCESS' })
+ pushNotification({ message: i18n`create_success`, type: 'SUCCESS' })
route(RootPages.instances)
}}
onError={(error: any) => {
- pushNotification({ messageId: 'create_error', type: 'ERROR', params:
error })
+ pushNotification({ message: i18n`create_error`, type: 'ERROR', params:
error })
}}
/>
@@ -155,6 +157,10 @@ function AppReady({ pushNotification,addTokenCleaner }: {
pushNotification: (n:
</Router>
}
+function hasKey<O>(obj: O, key: string | number | symbol): key is keyof O {
+ return key in obj
+}
+
function useBackendContextState() {
const [lang, setLang] = useLang()
const [url, changeBackend] = useBackendURL();
@@ -163,13 +169,19 @@ function useBackendContextState() {
return { url, token, changeBackend, updateToken, lang, setLang }
}
+function onTranslationError(error: MessageError) {
+ if (typeof window === "undefined") return;
+ (window as any)['missing_locale'] = ([] as string[]).concat((window as
any)['missing_locale']).concat(error.path.join())
+}
+
export default function Application(): VNode {
const state = useBackendContextState()
+
return (
<BackendContext.Provider value={state}>
- <IntlProvider definition={(translations as any)[state.lang] ||
translations.en}>
+ <MessageProvider locale={state.lang} onError={onTranslationError}
messages={hasKey(messages, state.lang) ? messages[state.lang] : messages.en}
pathSep={null as any} >
<AppRouting />
- </IntlProvider >
+ </MessageProvider >
</BackendContext.Provider>
);
}
@@ -179,12 +191,13 @@ interface SubPagesProps {
addTokenCleaner: any;
}
+
function SubPages({ id, pushNotification, addTokenCleaner }: SubPagesProps):
VNode {
const [token, updateToken] = useBackendInstanceToken(id);
const { changeBackend } = useContext(BackendContext)
-
const cleaner = useCallback(() =>{updateToken(undefined)},[id])
-
+ const i18n = useMessageTemplate('')
+
useEffect(() => {
addTokenCleaner(cleaner)
}, [addTokenCleaner, cleaner])
@@ -200,7 +213,7 @@ function SubPages({ id, pushNotification, addTokenCleaner
}: SubPagesProps): VNo
component={Details}
onUnauthorized={() => <Login
- withMessage={{ messageId: 'unauthorized', type: 'ERROR', }}
+ withMessage={{ message: i18n`unauthorized`, type: 'ERROR', }}
onConfirm={updateLoginStatus}
/>}
@@ -208,8 +221,8 @@ function SubPages({ id, pushNotification, addTokenCleaner
}: SubPagesProps): VNo
route(`/instance/${id}/update`)
}}
- onLoadError={(e: Error) => {
- pushNotification({ messageId: 'update_load_error', type: 'ERROR',
params: e })
+ onLoadError={(e: SwrError) => {
+ pushNotification({ message: i18n`update_load_error`, type: 'ERROR',
params: e })
route(`/instance/${id}/`)
return <div />
}}
@@ -218,11 +231,11 @@ function SubPages({ id, pushNotification, addTokenCleaner
}: SubPagesProps): VNo
<Route path={InstancePages.update}
component={Update}
onUnauthorized={() => <Login
- withMessage={{ messageId: 'unauthorized', type: 'ERROR', }}
+ withMessage={{ message: i18n`unauthorized`, type: 'ERROR', }}
onConfirm={updateLoginStatus}
/>}
- onLoadError={(e: Error) => {
- pushNotification({ messageId: 'update_load_error', type: 'ERROR',
params: e })
+ onLoadError={(e: SwrError) => {
+ pushNotification({ message: i18n`update_load_error`, type: 'ERROR',
params: e })
route(`/instance/${id}/`)
return <div />
}}
@@ -230,11 +243,11 @@ function SubPages({ id, pushNotification, addTokenCleaner
}: SubPagesProps): VNo
route(`/instance/${id}/`)
}}
onConfirm={() => {
- pushNotification({ messageId: 'create_success', type: 'SUCCESS' })
+ pushNotification({ message: i18n`create_success`, type: 'SUCCESS' })
route(`/instance/${id}/`)
}}
onUpdateError={(e: Error) => {
- pushNotification({ messageId: 'update_error', type: 'ERROR', params:
e })
+ pushNotification({ message: i18n`update_error`, type: 'ERROR',
params: e })
}}
/>
diff --git a/packages/frontend/src/messages/en.po
b/packages/frontend/src/messages/en.po
new file mode 100644
index 0000000..b0be0c0
--- /dev/null
+++ b/packages/frontend/src/messages/en.po
@@ -0,0 +1,155 @@
+# Examples from http://pology.nedohodnik.net/doc/user/en_US/ch-poformat.html
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: en\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10
|| n%100>=20) ? 1 : 2);\n"
+
+msgid "Time: %1 second"
+msgid_plural "Time: %1 seconds"
+msgstr[0] "Czas: %1 sekunda"
+msgstr[1] "Czas: %1 sekundy"
+msgstr[2] "Czas: %1 sekund"
+
+msgid "Hi"
+msgstr "Hello"
+
+msgid "List of configured instances"
+msgstr "List of configured instances"
+
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr "There is no instances yet, add more pressing the + sign"
+
+# msgctxt "fields.instance.name"
+# msgid "placeholder"
+# msgstr ""
+
+# |msgctxt "fields"
+# |msgctxt "instance"
+# msgctxt "fields.instance.id.label"
+
+msgid "fields.instance.id.label"
+msgstr "Id"
+
+msgid "fields.instance.name.label"
+msgstr "Name"
+
+msgid "fields.instance.merchant.pub.label"
+msgstr "Public key"
+
+msgid "fields.instance.payment.targets.label"
+msgstr "Payment targets"
+
+msgid "fields.instance.auth_token.label"
+msgstr "Auth token"
+
+msgid "fields.instance.auth_token.tooltip"
+msgstr "Use this token to secure an instance with a password"
+
+msgid "fields.instance.payto_uris.label"
+msgstr "Account address"
+
+msgid "fields.instance.payto_uris.help"
+msgstr "payto://x-taler-bank/bank.taler:5882/blogger"
+
+msgid "fields.instance.default_max_deposit_fee.label"
+msgstr "Max deposit fee label"
+
+msgid "fields.instance.default_max_wire_fee.label"
+msgstr "Max wire fee label"
+
+msgid "fields.instance.default_wire_fee_amortization.label"
+msgstr "Wire fee Amortization"
+
+msgid "fields.instance.address.label"
+msgstr "Address"
+
+msgid "fields.instance.address.country.label"
+msgstr "Country"
+
+msgid "fields.instance.address.country_subdivision.label"
+msgstr "Country Subdivision"
+
+msgid "fields.instance.address.district.label"
+msgstr "District"
+
+msgid "fields.instance.address.town.label"
+msgstr "Town"
+
+msgid "fields.instance.address.town_location.label"
+msgstr "Town Location"
+
+msgid "fields.instance.address.post_code.label"
+msgstr "Post code"
+
+msgid "fields.instance.address.street.label"
+msgstr "Street"
+
+msgid "fields.instance.address.building_name.label"
+msgstr "Building Name"
+
+msgid "fields.instance.address.building_number.label"
+msgstr "Building Number"
+
+msgid "fields.instance.address.address_lines.label"
+msgstr "Adress Line"
+
+msgid "fields.instance.jurisdiction.label"
+msgstr "Jurisdiction"
+
+msgid "fields.instance.jurisdiction.country.label"
+msgstr "Country"
+
+msgid "fields.instance.jurisdiction.country_subdivision.label"
+msgstr "Country Subdivision"
+
+msgid "fields.instance.jurisdiction.district.label"
+msgstr "District"
+
+msgid "fields.instance.jurisdiction.town.label"
+msgstr "Town"
+
+msgid "fields.instance.jurisdiction.town_location.label"
+msgstr "Town Location"
+
+msgid "fields.instance.jurisdiction.post_code.label"
+msgstr "Post code"
+
+msgid "fields.instance.jurisdiction.street.label"
+msgstr "Street"
+
+msgid "fields.instance.jurisdiction.building_name.label"
+msgstr "Building Name"
+
+msgid "fields.instance.jurisdiction.building_number.label"
+msgstr "Building Number"
+
+msgid "fields.instance.jurisdiction.address_lines.label"
+msgstr "Adress Line"
+
+msgid "fields.instance.default_pay_delay.label"
+msgstr "Pay delay"
+
+msgid "fields.instance.default_wire_transfer_delay.label"
+msgstr "Wire transfer delay"
+
+msgid "Couldnt access the server"
+msgstr "Couldnt access the server"
+
+msgid "Got message: %s from: %s (hasToken: %s)"
+msgstr "Recibimos el mensaje: %s desde: %s (con token: %s)"
+
+msgid "Merchant"
+msgstr "Merchant"
+
+msgid "Instances"
+msgstr "Instances"
+
+msgid "Update this instance"
+msgstr "Update this instance"
+
+msgid "Cancel"
+msgstr "Cancel"
+
+msgid "Confirm"
+msgstr "Confirm"
\ No newline at end of file
diff --git a/packages/frontend/src/messages/es.po
b/packages/frontend/src/messages/es.po
new file mode 100644
index 0000000..16bfcce
--- /dev/null
+++ b/packages/frontend/src/messages/es.po
@@ -0,0 +1,22 @@
+# Examples from http://pology.nedohodnik.net/doc/user/en_US/ch-poformat.html
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: pl\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10
|| n%100>=20) ? 1 : 2);\n"
+
+msgid "Time: %1 second"
+msgid_plural "Time: %1 seconds"
+msgstr[0] "Czas: %1 sekunda"
+msgstr[1] "Czas: %1 sekundy"
+msgstr[2] "Czas: %1 sekund"
+
+msgid "Hi"
+msgstr "Hola"
+
+msgid "List of configured instances"
+msgstr "Lista de instancias configuradas"
+
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr "No hay instancias todavía, agregá mas presionando el signo +"
+
diff --git a/packages/frontend/src/messages/index.ts
b/packages/frontend/src/messages/index.ts
new file mode 100644
index 0000000..3b965a4
--- /dev/null
+++ b/packages/frontend/src/messages/index.ts
@@ -0,0 +1,3 @@
+export * as en from './en.po'
+export * as es from './es.po'
+
diff --git a/packages/frontend/src/routes/instances/create/CreatePage.tsx
b/packages/frontend/src/routes/instances/create/CreatePage.tsx
index 44aa1cc..c779f94 100644
--- a/packages/frontend/src/routes/instances/create/CreatePage.tsx
+++ b/packages/frontend/src/routes/instances/create/CreatePage.tsx
@@ -25,7 +25,7 @@ import { MerchantBackend } from "../../../declaration";
import * as yup from 'yup';
import { YupField } from "../../../components/yup/YupField"
import { InstanceCreateSchema as schema } from '../../../schemas'
-import { Text } from "preact-i18n";
+import { Message } from "preact-messages";
interface Props {
onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) =>
void;
@@ -67,8 +67,8 @@ export function CreatePage({ onCreate, isLoading, onBack }:
Props): VNode {
<div class="level-left">
<div class="level-item">
<ul>
- <li><Text id="text.merchant" /></li>
- <li><Text id="text.instances" /></li>
+ <li><Message id="Merchant" /></li>
+ <li><Message id="Instances" /></li>
</ul>
</div>
</div>
@@ -81,7 +81,7 @@ export function CreatePage({ onCreate, isLoading, onBack }:
Props): VNode {
<div class="level-left">
<div class="level-item">
<h1 class="title">
- <Text id="text.create_new_instances" />
+ <Message id="Create new instances" />
</h1>
</div>
</div>
@@ -102,8 +102,8 @@ export function CreatePage({ onCreate, isLoading, onBack }:
Props): VNode {
valueHandler={valueHandler} info={schema.fields[f].describe()}
/>)}
<div class="buttons is-right">
- <button class="button" onClick={onBack} ><Text id="text.cancel"
/></button>
- <button class="button is-success" onClick={submit} ><Text
id="text.confirm" /></button>
+ <button class="button" onClick={onBack} ><Message id="Cancel"
/></button>
+ <button class="button is-success" onClick={submit} ><Message
id="Confirm" /></button>
</div>
</div>
<div class="column" />
diff --git a/packages/frontend/src/routes/instances/details/DetailPage.tsx
b/packages/frontend/src/routes/instances/details/DetailPage.tsx
index 38c89d2..17dab63 100644
--- a/packages/frontend/src/routes/instances/details/DetailPage.tsx
+++ b/packages/frontend/src/routes/instances/details/DetailPage.tsx
@@ -24,7 +24,7 @@ import { useState } from "preact/hooks";
import { MerchantBackend } from "../../../declaration";
import { YupField } from "../../../components/yup/YupField"
import { InstanceUpdateSchema as schema } from '../../../schemas'
-import { Text } from "preact-i18n";
+import { Message } from "preact-messages";
interface Props {
onUpdate: () => void;
@@ -59,8 +59,8 @@ export function DetailPage({ onUpdate, isLoading, selected,
onDelete }: Props):
<div class="level-left">
<div class="level-item">
<ul>
- <li><Text id="text.merchant" /></li>
- <li><Text id="text.instances" /></li>
+ <li><Message id="Merchant" /></li>
+ <li><Message id="Instances" /></li>
</ul>
</div>
</div>
@@ -73,7 +73,7 @@ export function DetailPage({ onUpdate, isLoading, selected,
onDelete }: Props):
<div class="level-left">
<div class="level-item">
<h1 class="title">
- <Text id="text.create_new_instances" />
+ <Message id="Instance details" />
</h1>
</div>
</div>
@@ -94,8 +94,8 @@ export function DetailPage({ onUpdate, isLoading, selected,
onDelete }: Props):
valueHandler={valueHandler} info={schema.fields[f].describe()}
/>)}
<div class="buttons is-right">
- <button class="button is-danger" onClick={() => onDelete()} ><Text
id="text.delete" /></button>
- <button class="button is-success" onClick={() => onUpdate()}
><Text id="text.update" /></button>
+ <button class="button is-danger" onClick={() => onDelete()}
><Message id="delete" /></button>
+ <button class="button is-success" onClick={() => onUpdate()}
><Message id="update" /></button>
</div>
</div>
<div class="column" />
diff --git a/packages/frontend/src/routes/instances/details/index.tsx
b/packages/frontend/src/routes/instances/details/index.tsx
index ab19342..ffd3be8 100644
--- a/packages/frontend/src/routes/instances/details/index.tsx
+++ b/packages/frontend/src/routes/instances/details/index.tsx
@@ -2,13 +2,13 @@ import { Fragment, h, VNode } from "preact";
import { useContext, useState } from "preact/hooks";
import { InstanceContext } from "../../../context/backend";
import { Notification } from "../../../declaration";
-import { useBackendInstance, useBackendInstanceMutateAPI } from
"../../../hooks/backend";
+import { useBackendInstance, useBackendInstanceMutateAPI, SwrError } from
"../../../hooks/backend";
import { DeleteModal } from "../list/DeleteModal";
import { DetailPage } from "./DetailPage";
interface Props {
onUnauthorized: () => VNode;
- onLoadError: (e: Error) => VNode;
+ onLoadError: (e: SwrError) => VNode;
onUpdate: () => void;
pushNotification: (n: Notification) => void;
}
@@ -41,9 +41,9 @@ export default function Detail({ onUpdate, onLoadError,
onUnauthorized, pushNoti
onConfirm={async (): Promise<void> => {
try {
await deleteInstance()
- pushNotification({ messageId: 'delete_success', type: 'SUCCESS' })
+ pushNotification({ message: 'delete_success', type: 'SUCCESS' })
} catch (error) {
- pushNotification({ messageId: 'delete_error', type: 'ERROR', params:
error })
+ pushNotification({ message: 'delete_error', type: 'ERROR', params:
error })
}
setDeleting(false)
}}
diff --git a/packages/frontend/src/routes/instances/list/CardTable.tsx
b/packages/frontend/src/routes/instances/list/CardTable.tsx
index 46c32ff..53a6f54 100644
--- a/packages/frontend/src/routes/instances/list/CardTable.tsx
+++ b/packages/frontend/src/routes/instances/list/CardTable.tsx
@@ -20,7 +20,7 @@
*/
import { h, VNode } from "preact";
-import { Text } from "preact-i18n";
+import { Message } from "preact-messages";
import { useEffect, useState } from "preact/hooks";
import { MerchantBackend } from "../../../declaration";
import { EmptyTable } from "./EmptyTable";
@@ -70,7 +70,7 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected }:
return <div class="card has-table">
<header class="card-header">
- <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Text id="text.instances" /></p>
+ <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Message id="Instances" /></p>
<div class="card-header-icon" aria-label="more options">
diff --git a/packages/frontend/src/routes/instances/list/EmptyTable.tsx
b/packages/frontend/src/routes/instances/list/EmptyTable.tsx
index 5a0af48..154e3a8 100644
--- a/packages/frontend/src/routes/instances/list/EmptyTable.tsx
+++ b/packages/frontend/src/routes/instances/list/EmptyTable.tsx
@@ -20,13 +20,14 @@
*/
import { h, VNode } from "preact";
-import { Text } from "preact-i18n";
+import { useMessageTemplate } from "preact-messages";
+import { Message } from "preact-messages";
export function EmptyTable(): VNode {
return <div class="content has-text-grey has-text-centered">
<p>
<span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px"
/></span>
</p>
- <p><Text id="text.instance.empty_list" /></p>
+ <p><Message id="There is no instances yet, add more pressing the + sign"
/></p>
</div>
}
diff --git a/packages/frontend/src/routes/instances/list/Table.tsx
b/packages/frontend/src/routes/instances/list/Table.tsx
index eb44447..a7298ef 100644
--- a/packages/frontend/src/routes/instances/list/Table.tsx
+++ b/packages/frontend/src/routes/instances/list/Table.tsx
@@ -20,7 +20,7 @@
*/
import { h, VNode } from "preact"
-import { Text } from "preact-i18n"
+import { Message } from "preact-messages"
import { StateUpdater } from "preact/hooks"
import { MerchantBackend } from "../../../declaration"
@@ -47,10 +47,10 @@ export function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate,
<span class="check" />
</label>
</th>
- <th><Text id="fields.instance.id.label" /></th>
- <th><Text id="fields.instance.name.label" /></th>
- <th><Text id="fields.instance.merchant_pub.label" /></th>
- <th><Text id="fields.instance.payment_targets.label" /></th>
+ <th><Message id="fields_instance_id_label" /></th>
+ <th><Message id="fields_instance_name_label" /></th>
+ <th><Message id="fields_instance_merchant_pub_label" /></th>
+ <th><Message id="fields_instance_payment_targets_label" /></th>
<th />
</tr>
</thead>
diff --git a/packages/frontend/src/routes/instances/list/View.tsx
b/packages/frontend/src/routes/instances/list/View.tsx
index 48b2a46..f54e64a 100644
--- a/packages/frontend/src/routes/instances/list/View.tsx
+++ b/packages/frontend/src/routes/instances/list/View.tsx
@@ -22,7 +22,7 @@
import { h, VNode } from "preact";
import { MerchantBackend } from "../../../declaration";
import { CardTable } from './CardTable';
-import { Text } from "preact-i18n";
+import { Message } from "preact-messages";
interface Props {
instances: MerchantBackend.Instances.Instance[];
@@ -42,8 +42,8 @@ export function View({ instances, isLoading, onCreate,
onDelete, onUpdate, selec
<div class="level-left">
<div class="level-item">
<ul>
- <li><Text id="text.merchant" /></li>
- <li><Text id="text.instances" /></li>
+ <li><Message id="Merchant" /></li>
+ <li><Message id="Instances" /></li>
</ul>
</div>
</div>
@@ -56,7 +56,7 @@ export function View({ instances, isLoading, onCreate,
onDelete, onUpdate, selec
<div class="level-left">
<div class="level-item">
<h1 class="title">
- <Text id="text.list_of_configured_instances" />
+ <Message id="List of configured instances" />
</h1>
</div>
</div>
diff --git a/packages/frontend/src/routes/instances/list/index.tsx
b/packages/frontend/src/routes/instances/list/index.tsx
index f63e05e..ba63b3e 100644
--- a/packages/frontend/src/routes/instances/list/index.tsx
+++ b/packages/frontend/src/routes/instances/list/index.tsx
@@ -21,14 +21,14 @@
import { Fragment, h, VNode } from 'preact';
import { View } from './View';
-import { useBackendInstances, useBackendInstanceMutateAPI } from
'../../../hooks/backend';
+import { useBackendInstances, useBackendInstanceMutateAPI, SwrError } from
'../../../hooks/backend';
import { useState } from 'preact/hooks';
import { MerchantBackend, Notification } from '../../../declaration';
import { DeleteModal } from './DeleteModal';
interface Props {
pushNotification: (n: Notification) => void;
onUnauthorized: () => VNode;
- onError: (e: Error) => VNode;
+ onError: (e: SwrError) => VNode;
onCreate: () => void;
onUpdate: (id: string) => void;
}
@@ -60,9 +60,9 @@ export default function Instances({ pushNotification,
onUnauthorized, onError, o
onConfirm={async (): Promise<void> => {
try {
await deleteInstance()
- pushNotification({ messageId: 'delete_success', type: 'SUCCESS' })
+ pushNotification({ message: 'delete_success', type: 'SUCCESS' })
} catch (e) {
- pushNotification({ messageId: 'delete_error', type: 'ERROR', params:
error })
+ pushNotification({ message: 'delete_error', type: 'ERROR', params:
error })
}
setDeleting(null)
}}
diff --git a/packages/frontend/src/routes/instances/update/UpdatePage.tsx
b/packages/frontend/src/routes/instances/update/UpdatePage.tsx
index 54c157f..8cbd2d6 100644
--- a/packages/frontend/src/routes/instances/update/UpdatePage.tsx
+++ b/packages/frontend/src/routes/instances/update/UpdatePage.tsx
@@ -25,7 +25,7 @@ import { MerchantBackend } from "../../../declaration";
import * as yup from 'yup';
import { YupField } from "../../../components/yup/YupField"
import { InstanceUpdateSchema as schema } from '../../../schemas'
-import { Text } from "preact-i18n";
+import { Message } from "preact-messages";
interface Props {
onUpdate: (d: MerchantBackend.Instances.InstanceReconfigurationMessage) =>
void;
@@ -72,8 +72,8 @@ export function UpdatePage({ onUpdate, isLoading, selected,
onBack }: Props): VN
<div class="level-left">
<div class="level-item">
<ul>
- <li><Text id="text.merchant" /></li>
- <li><Text id="text.instances" /></li>
+ <li><Message id="Merchant" /></li>
+ <li><Message id="Instances" /></li>
</ul>
</div>
</div>
@@ -86,7 +86,7 @@ export function UpdatePage({ onUpdate, isLoading, selected,
onBack }: Props): VN
<div class="level-left">
<div class="level-item">
<h1 class="title">
- <Text id="text.create_new_instances" />
+ <Message id="Update this instance" />
</h1>
</div>
</div>
@@ -107,8 +107,8 @@ export function UpdatePage({ onUpdate, isLoading, selected,
onBack }: Props): VN
valueHandler={valueHandler} info={schema.fields[f].describe()}
/>)}
<div class="buttons is-right">
- <button class="button" onClick={onBack} ><Text id="text.cancel"
/></button>
- <button class="button is-success" onClick={submit} ><Text
id="text.confirm" /></button>
+ <button class="button" onClick={onBack} ><Message id="Cancel"
/></button>
+ <button class="button is-success" onClick={submit} ><Message
id="Confirm" /></button>
</div>
</div>
<div class="column" />
diff --git a/packages/frontend/src/routes/instances/update/index.tsx
b/packages/frontend/src/routes/instances/update/index.tsx
index 3afe7c5..ecaad37 100644
--- a/packages/frontend/src/routes/instances/update/index.tsx
+++ b/packages/frontend/src/routes/instances/update/index.tsx
@@ -1,6 +1,6 @@
import { h, VNode } from "preact";
import { MerchantBackend } from "../../../declaration";
-import { useBackendInstance, useBackendInstanceMutateAPI } from
"../../../hooks/backend";
+import { SwrError, useBackendInstance, useBackendInstanceMutateAPI } from
"../../../hooks/backend";
import { UpdatePage } from "./UpdatePage";
interface Props {
@@ -9,7 +9,7 @@ interface Props {
pushNotification: (n: Notification) => void;
onUnauthorized: () => VNode;
- onLoadError: (e: Error) => VNode;
+ onLoadError: (e: SwrError) => VNode;
onUpdateError: (e: Error) => void;
}
diff --git a/packages/frontend/src/schemas/index.ts
b/packages/frontend/src/schemas/index.ts
index d7c6cbc..b047bc8 100644
--- a/packages/frontend/src/schemas/index.ts
+++ b/packages/frontend/src/schemas/index.ts
@@ -69,27 +69,27 @@ const InstanceSchema = yup.object().shape({
.required(),
address: yup.object().shape({
country: yup.string().optional(),
- country_subdivision: yup.string().optional(),
- district: yup.string().optional(),
- town: yup.string(),
- town_location: yup.string().optional(),
- post_code: yup.string().optional(),
- street: yup.string().optional(),
- building_name: yup.string().optional(),
- building_number: yup.string().optional(),
address_lines: yup.array().of(yup.string()).max(7).optional(),
+ building_number: yup.string().optional(),
+ building_name: yup.string().optional(),
+ street: yup.string().optional(),
+ post_code: yup.string().optional(),
+ town_location: yup.string().optional(),
+ town: yup.string(),
+ district: yup.string().optional(),
+ country_subdivision: yup.string().optional(),
}).meta({type:'group'}),
jurisdiction: yup.object().shape({
country: yup.string().optional(),
- country_subdivision: yup.string().optional(),
- district: yup.string().optional(),
- town: yup.string(),
- town_location: yup.string().optional(),
- post_code: yup.string().optional(),
- street: yup.string().optional(),
- building_name: yup.string().optional(),
- building_number: yup.string().optional(),
address_lines: yup.array().of(yup.string()).max(7).optional(),
+ building_number: yup.string().optional(),
+ building_name: yup.string().optional(),
+ street: yup.string().optional(),
+ post_code: yup.string().optional(),
+ town_location: yup.string().optional(),
+ town: yup.string(),
+ district: yup.string().optional(),
+ country_subdivision: yup.string().optional(),
}).meta({type:'group'}),
default_pay_delay: yup.object()
.shape({ d_ms: yup.number() })
diff --git a/packages/frontend/tests/hooks/notification.test.ts
b/packages/frontend/tests/hooks/notification.test.ts
index e70cdd2..6825a82 100644
--- a/packages/frontend/tests/hooks/notification.test.ts
+++ b/packages/frontend/tests/hooks/notification.test.ts
@@ -34,7 +34,7 @@ test('notification should disapear after timeout', () => {
act(() => {
result.current?.pushNotification({
- messageId: 'some_id',
+ message: 'some_id',
type: 'INFO'
});
});
diff --git a/packages/preact-message/.gitignore
b/packages/preact-message/.gitignore
new file mode 100644
index 0000000..ad09c5f
--- /dev/null
+++ b/packages/preact-message/.gitignore
@@ -0,0 +1,2 @@
+/example/dist/
+/lib/
diff --git a/packages/preact-message/CHANGELOG.md
b/packages/preact-message/CHANGELOG.md
new file mode 100644
index 0000000..4681441
--- /dev/null
+++ b/packages/preact-message/CHANGELOG.md
@@ -0,0 +1,16 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit
guidelines.
+
+# 1.0.0-beta.1 (2020-11-29)
+
+
+### Features
+
+* Add @messageformat/react (was react-message-context)
([#292](https://github.com/messageformat/messageformat/issues/292))
([9089f0a](https://github.com/messageformat/messageformat/commit/9089f0ad52f21f8ab6c356fd4f51bb140dc36855))
+
+
+# 0.6.2 and earlier
+
+For earlier changes, see
https://github.com/eemeli/react-message-context/releases
diff --git a/packages/preact-message/LICENSE b/packages/preact-message/LICENSE
new file mode 100644
index 0000000..78918d5
--- /dev/null
+++ b/packages/preact-message/LICENSE
@@ -0,0 +1,20 @@
+Copyright OpenJS Foundation and contributors, https://openjsf.org/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/preact-message/README.md
b/packages/preact-message/README.md
new file mode 100644
index 0000000..a52d5b3
--- /dev/null
+++ b/packages/preact-message/README.md
@@ -0,0 +1,126 @@
+# @messageformat/react
+
+An efficient React front-end for message formatting libraries.
+Designed in particular for use with [messageformat], but will work with any
messages.
+Provides the best possible API for a front-end developer, without making the
back end any more difficult than it needs to be either.
+Should add at most about 1kB to your compiled & minified bundle size.
+
+This package was previously named
[react-message-context](https://www.npmjs.com/package/react-message-context).
+
+[messageformat]: https://messageformat.github.io
+
+## Installation
+
+```
+npm install @messageformat/react
+```
+
+The library has React 16.8 or later as a peer dependency.
+It is published as an **ES module** only, which should work directly with
almost all tools and environments that support modern development targeting
browser environments.
+For tools such as Jest that define their own import methods, you may need to
add something like `transformIgnorePatterns:
['node_modules/(?!@messageformat/react)']` to your configuration.
+
+## [API Documentation]
+
+- [`<MessageProvider messages [locale] [onError]
[pathSep]>`](http://messageformat.github.io/messageformat/api/react.messageprovider/)
+- [`<Message id [locale] [props]
[...msgProps]>`](http://messageformat.github.io/messageformat/api/react.message/)
+-
[`useLocales()`](http://messageformat.github.io/messageformat/api/react.uselocales/)
+- [`useMessage(id, [params],
[locale])`](http://messageformat.github.io/messageformat/api/react.usemessage/)
+- [`useMessageGetter(rootId, [{ baseParams, locale
}])`](http://messageformat.github.io/messageformat/api/react.usemessagegetter/)
+
+## Usage Examples
+
+In addition to the examples included below and in the [API documentation], see
the [example] for a simple, but fully functional example of using this library
along with [@messageformat/core] and [@messageformat/loader] to handle
localized messages, with dynamic loading of non-default locales.
+
+[api documentation]: http://messageformat.github.io/messageformat/api/react/
+[example]:
https://github.com/messageformat/messageformat/tree/master/packages/react/example
+[@messageformat/core]: https://www.npmjs.com/package/@messageformat/core
+[@messageformat/loader]: https://www.npmjs.com/package/@messageformat/loader
+
+---
+
+Within a `MessageProvider`, access to the messages is possible using either
the `Message` component, or via custom hooks such as `useMessageGetter`:
+
+```js
+import React from 'preact';
+import {
+ Message,
+ MessageProvider,
+ useMessageGetter
+} from '@messageformat/react';
+
+const messages = {
+ message: 'Your message is important',
+ answers: {
+ sixByNine: ({ base }) => (6 * 9).toString(base),
+ universe: 42
+ }
+};
+
+function Equality() {
+ const getAnswer = useMessageGetter('answers');
+ const foo = getAnswer('sixByNine', { base: 13 });
+ const bar = getAnswer('universe');
+ return `${foo} and ${bar} are equal`;
+}
+
+export const Example = () => (
+ <MessageProvider messages={messages}>
+ <ul>
+ <li>
+ <Message id="message" />
+ </li>
+ <li>
+ <Equality />
+ </li>
+ </ul>
+ </MessageProvider>
+);
+
+// Will render as:
+// - Your message is important
+// - 42 and 42 are equal
+```
+
+---
+
+Using MessageProviders within each other allows for multiple locales and
namespaces:
+
+```jsx
+import React from 'preact';
+import { Message, MessageProvider } from '@messageformat/react';
+
+export const Example = () => (
+ <MessageProvider locale="en" messages={{ foo: 'FOO', qux: 'QUX' }}>
+ <MessageProvider locale="fi" messages={{ foo: 'FÖÖ', bar: 'BÄR' }}>
+ <ul>
+ <li>
+ <Message id="foo" />
+ </li>
+ <li>
+ <Message id="foo" locale="en" />
+ </li>
+ <li>
+ <Message id="bar" />
+ </li>
+ <li>
+ <Message id="bar" locale="en" />
+ </li>
+ <li>
+ <Message id="qux" />
+ </li>
+ <li>
+ <Message id="quux">xyzzy</Message>
+ </li>
+ </ul>
+ </MessageProvider>
+ </MessageProvider>
+);
+
+// Will render as:
+// - FÖÖ
+// - FOO
+// - BÄR
+// - bar (uses fallback to key)
+// - QUX (uses fallback to "en" locale)
+// - xyzzy (uses fallback to child node)
+```
diff --git a/packages/preact-message/package.json
b/packages/preact-message/package.json
new file mode 100644
index 0000000..47bc250
--- /dev/null
+++ b/packages/preact-message/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "preact-messages",
+ "version": "1.0.0-beta.1",
+ "description": "PReact hooks and other bindings for messages",
+ "keywords": [
+ "i18n",
+ "preact",
+ "context",
+ "messages",
+ "messageformat",
+ "provider"
+ ],
+ "contributors": [
+ "Eemeli Aro <eemeli@gmail.com>"
+ ],
+ "license": "MIT",
+ "homepage": "http://messageformat.github.io/messageformat/api/react/",
+ "main": "lib/index.js",
+ "types": "lib/index.d.ts",
+ "exports": {
+ ".": "./lib/index.js",
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "lib/"
+ ],
+ "sideEffects": false,
+ "scripts": {
+ "build": "tsc",
+ "extract-api": "api-extractor run --local --verbose"
+ },
+ "peerDependencies": {
+ "preact": ">=10.3.0"
+ },
+ "dependencies": {
+ "preact": "^10.5.12",
+ "typescript": "^4.1.5"
+ }
+}
diff --git a/packages/preact-message/src/MessageProvider.ts
b/packages/preact-message/src/MessageProvider.ts
new file mode 100644
index 0000000..cb72622
--- /dev/null
+++ b/packages/preact-message/src/MessageProvider.ts
@@ -0,0 +1,183 @@
+import { createElement } from 'preact';
+import { useContext, useMemo } from 'preact/hooks';
+import { MessageContext, MessageObject, defaultValue } from
'./message-context';
+// import { MessageProviderProps, getPathSep, getLocales, getMessages,
getOnError } from './message-provider';
+import { MessageError, ErrorCode, errorMessages } from './message-error';
+
+/**
+ * `<MessageProvider messages [locale] [merge] [onError] [pathSep]>`
+ *
+ * Makes the messages available for its descendants via a React Context.
+ * To support multiple locales and/or namespaces, MessageProviders may be used
within each other, merging each provider's messages with those of its parents.
+ * The locale preference order is also set similarly, from nearest to furthest.
+ *
+ * @public
+ *
+ * @example
+ * ```js
+ * import React from 'preact'
+ * import { Message, MessageProvider } from '@messageformat/react'
+ *
+ * const messages = { example: { key: 'Your message here' } }
+ * const extended = { other: { key: 'Another message' } }
+ *
+ * const Example = () => (
+ * <span>
+ * <Message id={['example', 'key']} />
+ * {' | '}
+ * <Message id="other/key" />
+ * </span>
+ * ) // 'Your message here | Another message'
+ *
+ * export const App = () => (
+ * <MessageProvider messages={messages} pathSep="/">
+ * <MessageProvider messages={extended}>
+ * <Example />
+ * </MessageProvider>
+ * </MessageProvider>
+ * )
+ * ```
+ */
+
+export function MessageProvider(props: MessageProviderProps) {
+ const {
+ children,
+ context: propContext,
+ debug,
+ locale = '',
+ merge,
+ messages,
+ onError,
+ pathSep
+ } = props;
+ let parent = useContext(MessageContext);
+ if (propContext)
+ parent = propContext;
+ else if (propContext === null)
+ parent = defaultValue;
+ const value: MessageContext = useMemo(() => {
+ const ps = getPathSep(parent, pathSep);
+ return {
+ locales: getLocales(parent, locale),
+ merge: merge || parent.merge,
+ messages: getMessages(parent, locale, messages),
+ onError: getOnError(parent, ps, onError, debug),
+ pathSep: ps
+ };
+ }, [parent, locale, merge, messages, pathSep]);
+ return createElement(MessageContext.Provider, { value } as any, children);
+}
+
+
+
+/** @public */
+export interface MessageProviderProps {
+ children: any;
+
+ /**
+ * A hierarchical object containing the messages as boolean, number, string
or function values.
+ */
+ messages: MessageObject;
+ context?: MessageContext;
+
+ /** @deprecated Use onError instead */
+ debug?: 'error' | 'warn' | ((msg: string) => any);
+
+ /**
+ * A key for the locale of the given messages.
+ * If uset, will inherit the locale from the parent context, or ultimately
use en empty string.
+ */
+ locale?: string;
+
+ /**
+ * By default, top-level namespaces defined in a child `MessageProvider`
overwrite those defined in a parent.
+ * Set this to {@link https://lodash.com/docs/#merge | _.merge} or some
other function with the same arguments as
+ * {@link
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
| Object.assign} to allow for deep merges.
+ */
+ merge?: MessageContext['merge'];
+
+ /**
+ * What to do on errors; most often called if a message is not found.
+ *
+ * - `"silent"`: Ignore the error; use the message's id as the replacement
message.
+ *
+ * - `"error"`: Throw the error.
+ *
+ * - `"warn"` (default): Print a warning in the console and use the
message's id as the replacement message.
+ *
+ * - `(error) => any`: A custom function that is called with an `Error`
object with `code: string` and `path: string[]` fields set.
+ * The return falue is used as the replacement message.
+ */
+ onError?: 'error' | 'silent' | 'warn' | ((error: MessageError) => any);
+
+ /**
+ * By default, `.` in a `<Message id>` splits the path into parts, such that
e.g. `'a.b'` is equivalent to `['a', 'b']`.
+ * Use this option to customize or disable this behaviour (by setting it to
`null`).
+ */
+ pathSep?: string;
+}
+
+export function getOnError(
+ parent: MessageContext,
+ pathSep: string | null,
+ onError: MessageProviderProps['onError'],
+ debug: MessageProviderProps['debug']
+) {
+ const asId = (path: string[]) => path.join(pathSep || ',');
+ function msgError(path: string[], code: ErrorCode) {
+ throw new MessageError(path, code, asId);
+ }
+ function msgWarning(path: string[], code: ErrorCode) {
+ console.warn(errorMessages[code], path);
+ return asId(path);
+ }
+
+ if (onError === undefined) {
+ // debug is deprecated, will be removed later
+ if (typeof debug === 'function')
+ return (path: string[], code: ErrorCode) =>
+ debug(`${errorMessages[code]}: ${asId(path)}`);
+ onError = debug;
+ }
+
+ switch (onError) {
+ case 'silent':
+ return asId;
+ case 'error':
+ return msgError;
+ case 'warn':
+ return msgWarning;
+ default:
+ if (typeof onError === 'function') {
+ const _onError = onError;
+ return (path: string[], code: ErrorCode) =>
+ _onError(new MessageError(path, code, asId));
+ }
+ return parent.onError || msgWarning;
+ }
+}
+
+export function getLocales({ locales }: MessageContext, locale: string) {
+ const fallback = locales.filter(fb => fb !== locale);
+ return [locale].concat(fallback);
+}
+
+export function getMessages(
+ { merge, messages }: MessageContext,
+ locale: string,
+ lcMessages: MessageObject
+) {
+ const res = Object.assign({}, messages);
+ const prev = res[locale];
+ res[locale] =
+ prev && typeof prev === 'object' ? merge({}, prev, lcMessages) :
lcMessages;
+ return res;
+}
+
+export function getPathSep(context: MessageContext, pathSep?: string | null) {
+ return pathSep === null || typeof pathSep === 'string'
+ ? pathSep
+ : context.pathSep;
+}
+
+
diff --git a/packages/preact-message/src/declarations.d.ts
b/packages/preact-message/src/declarations.d.ts
new file mode 100644
index 0000000..acb8034
--- /dev/null
+++ b/packages/preact-message/src/declarations.d.ts
@@ -0,0 +1,3 @@
+export module "" {
+
+}
\ No newline at end of file
diff --git a/packages/preact-message/src/get-message.ts
b/packages/preact-message/src/get-message.ts
new file mode 100644
index 0000000..e04d076
--- /dev/null
+++ b/packages/preact-message/src/get-message.ts
@@ -0,0 +1,87 @@
+import {
+ MessageContext,
+ MessageObject,
+ MessageValue
+} from './message-context.js';
+
+function getIn(messages: MessageValue | MessageObject, path: string[]) {
+ if (messages) {
+ for (let i = 0; i < path.length; ++i) {
+ if (typeof messages !== 'object') return undefined;
+ messages = messages[path[i]];
+ if (messages === undefined) return undefined;
+ }
+ }
+ return messages;
+}
+
+export function getPath(id?: string | string[], pathSep?: string | null) {
+ if (!id) return [];
+ if (Array.isArray(id)) return id;
+ return pathSep ? id.split(pathSep) : [id];
+}
+
+/**
+ * Given a `MessageContext` instance, fetches an entry from the messages
object of the current or given locale.
+ * The returned value will be `undefined` if not found, or otherwise exactly
as set in the `MessageProvider` props.
+ *
+ * @public
+ * @param id - The key or key path of the message or message object.
+ * If empty or `[]`, matches the root of the messages object
+ * @param locale - If set, overrides the current locale precedence as set by
parent MessageProviders.
+ */
+export function getMessage(
+ context: MessageContext,
+ id?: string | string[],
+ locale?: string | string[]
+) {
+ const { locales, messages, onError, pathSep } = context;
+ const lca =
+ locale == null ? locales : Array.isArray(locale) ? locale : [locale];
+ const path = getPath(id, pathSep);
+ for (let i = 0; i < lca.length; ++i) {
+ const lc = lca[i];
+ const msg = getIn(messages[lc], path);
+ if (msg !== undefined) return msg;
+ }
+ return onError ? onError(path, 'ENOMSG') : undefined;
+}
+
+/**
+ * @param id - Message identifier; extends the path set by `rootId`
+ * @param params - Parameters for a function message
+ */
+export interface MessageGetterOptions {
+ baseParams?: any;
+ locale?: string | string[];
+}
+
+/**
+ * Given a `MessageContext` instance, returns a message getter function, which
may have a preset root id path, locale, and/or base parameters for message
functions.
+ *
+ * The returned function takes two parameters `(msgId, msgParams)`, which will
extend any values set by the hook's arguments.
+ *
+ * @public
+ * @param context - The `MessageContext` instance
+ * @param rootId - The key or key path of the message or message object.
+ * If empty or `[]`, matches the root of the messages object
+ * @param options - If `baseParams` is set, message function parameters will
be assumed to always be an object, with these values initially set.
+ * `locale` overrides the current locale precedence as set by parent
MessageProviders.
+ */
+export function getMessageGetter(
+ context: MessageContext,
+ rootId?: string | string[],
+ { baseParams, locale }: MessageGetterOptions = {}
+) {
+ const { pathSep } = context;
+ const pathPrefix = getPath(rootId, pathSep);
+ return function message(id?: string | string[], params?: any) {
+ const path = pathPrefix.concat(getPath(id, pathSep));
+ const msg = getMessage(context, path, locale);
+ if (typeof msg !== 'function') return msg;
+ const msgParams = baseParams
+ ? Object.assign({}, baseParams, params)
+ : params;
+ return msg(msgParams);
+ };
+}
diff --git a/packages/preact-message/src/index.ts
b/packages/preact-message/src/index.ts
new file mode 100644
index 0000000..d8dcd69
--- /dev/null
+++ b/packages/preact-message/src/index.ts
@@ -0,0 +1,32 @@
+/**
+ * An efficient React front-end for message formatting
+ *
+ * @packageDocumentation
+ * @remarks
+ * Designed in particular for use with {@link https://messageformat.github.io
| messageformat}, but will work with any messages.
+ * Provides the best possible API for a front-end developer, without making
the back end any more difficult than it needs to be either.
+ * Should add at most about 1kB to your compiled & minified bundle size.
+ *
+ * @example
+ * ```js
+ * import {
+ * MessageContext,
+ * MessageProvider,
+ * Message,
+ * getMessage,
+ * getMessageGetter,
+ * useLocales,
+ * useMessage,
+ * useMessageGetter
+ * } from '@messageformat/react'
+ * ```
+ */
+export { getMessage, getMessageGetter } from './get-message';
+export { Message, MessageProps } from './message';
+export { MessageContext, MessageObject, MessageValue } from
'./message-context';
+export { MessageError } from './message-error';
+export { MessageProvider } from './MessageProvider';
+export { useLocales } from './use-locales';
+export { useMessage } from './use-message';
+export { useMessageGetter } from './use-message-getter';
+export { useMessageTemplate } from './use-message-template';
diff --git a/packages/preact-message/src/message-context.ts
b/packages/preact-message/src/message-context.ts
new file mode 100644
index 0000000..c344a11
--- /dev/null
+++ b/packages/preact-message/src/message-context.ts
@@ -0,0 +1,72 @@
+// @ts-ignore - https://github.com/microsoft/rushstack/issues/1050
+import { createContext } from 'preact';
+import { ErrorCode } from './message-error';
+
+/** @internal */
+export type MessageValue = string | number | boolean | ((props: any) => any);
+
+/** @internal */
+export interface MessageObject {
+ [key: string]: MessageValue | MessageObject;
+}
+
+/** @public */
+export interface MessageContext {
+ locales: string[];
+ merge: (target: MessageObject, ...sources: MessageObject[]) => MessageObject;
+ messages: MessageObject;
+
+ /** Always defined in MessageProvider children */
+ onError?: (path: string[], code: ErrorCode) => any;
+ pathSep: string | null;
+}
+
+export const defaultValue: MessageContext = {
+ locales: [],
+ merge: Object.assign,
+ messages: {},
+ pathSep: '.'
+};
+
+/**
+ * The context object used internally by the library.
+ * Probably only useful with `Class.contextType` or for building custom hooks.
+ *
+ * @public
+ *
+ * @example
+ * ```js
+ * import React, { Component } from 'preact'
+ * import {
+ * getMessage,
+ * getMessageGetter,
+ * MessageContext,
+ * MessageProvider
+ * } from '@messageformat/react'
+ *
+ * const messages = {
+ * example: { key: 'Your message here' },
+ * other: { key: 'Another message' }
+ * }
+ *
+ * class Example extends Component {
+ * render() {
+ * const message = getMessage(this.context, 'example.key')
+ * const otherMsg = getMessageGetter(this.context, 'other')
+ * return (
+ * <span>
+ * {message} | {otherMsg('key')}
+ * </span>
+ * ) // 'Your message here | Another message'
+ * }
+ * }
+ * Example.contextType = MessageContext
+ *
+ * export const App = () => (
+ * <MessageProvider messages={messages}>
+ * <Example />
+ * </MessageProvider>
+ * )
+ * ```
+ */
+export const MessageContext = createContext(defaultValue);
diff --git a/packages/preact-message/src/message-error.ts
b/packages/preact-message/src/message-error.ts
new file mode 100644
index 0000000..f641544
--- /dev/null
+++ b/packages/preact-message/src/message-error.ts
@@ -0,0 +1,22 @@
+export const errorMessages = {
+ EBADMSG: 'Message with unexpected object value',
+ ENOMSG: 'Message not found'
+};
+
+export type ErrorCode = keyof typeof errorMessages;
+
+/** @internal */
+export class MessageError extends Error {
+ code: ErrorCode;
+ path: string[];
+
+ constructor(
+ path: string[],
+ code: ErrorCode,
+ asId: (path: string[]) => string
+ ) {
+ super(`${errorMessages[code]}: ${asId(path)}`);
+ this.code = code;
+ this.path = path;
+ }
+}
diff --git a/packages/preact-message/src/message.ts
b/packages/preact-message/src/message.ts
new file mode 100644
index 0000000..e64481b
--- /dev/null
+++ b/packages/preact-message/src/message.ts
@@ -0,0 +1,89 @@
+import { useContext } from 'preact/hooks';
+import { getMessage, getPath } from './get-message';
+import { MessageContext } from './message-context';
+
+/** @public */
+export interface MessageProps {
+ /**
+ * If a function, will be called with the found message.
+ * In this case, `params` will be ignored and `id` is optional.
+ * If some other type of non-empty renderable node, it will be used as a
fallback value if the message is not found.
+ */
+ children?: any;
+
+ /** The key or key path of the message. */
+ id?: string | string[];
+
+ /** If set, overrides the `locale` of the nearest MessageProvider. */
+ locale?: string | string[];
+
+ /**
+ * Parameters to pass to function messages as their first and only argument.
+ * `params` will override `msgParams`, to allow for data keys such as `key`
and `locale`.
+ */
+ params?: any;
+
+ /**
+ * Parameters to pass to function messages as their first and only argument.
+ * Overriden by `params`, to allow for data keys such as `key` and `locale`.
+ */
+ [msgParamKey: string]: any;
+}
+
+// Just using { foo, ...bar } adds a polyfill with a boilerplate copyright
+// statement that would add 50% to the minified size of the whole library.
+function rest(props: { [key: string]: any }, exclude: string[]) {
+ const t: typeof props = {};
+ for (const k of Object.keys(props)) if (!exclude.includes(k)) t[k] =
props[k];
+ return t;
+}
+
+/**
+ * `<Message id [locale] [params] [...msgParams]>`
+ *
+ * The value of a message.
+ * May also be used with a render prop: `<Message id={id}>{msg =>
{...}}</Message>`.
+ *
+ * @public
+ *
+ * @example
+ * ```js
+ * import React from 'preact'
+ * import { Message, MessageProvider } from '@messageformat/react'
+ *
+ * const messages = { example: { key: ({ thing }) => `Your ${thing} here` } }
+ *
+ * const Example = () => (
+ * <span>
+ * <Message id="example.key" thing="message" />
+ * </span>
+ * ) // 'Your message here'
+ *
+ * export const App = () => (
+ * <MessageProvider messages={messages}>
+ * <Example />
+ * </MessageProvider>
+ * )
+ * ```
+ */
+export function Message(props: MessageProps) {
+ const { children, id, locale, params } = props;
+ const msgParams = rest(props, ['children', 'id', 'locale', 'params']);
+ let context = useContext(MessageContext);
+ let fallback = false;
+ if (children && typeof children !== 'function')
+ context = Object.assign({}, context, { onError: () => (fallback = true) });
+ const msg = getMessage(context, id, locale);
+ if (fallback) return children;
+ if (typeof children === 'function') return children(msg);
+ switch (typeof msg) {
+ case 'function':
+ return msg(Object.assign(msgParams, params));
+ case 'boolean':
+ return String(msg);
+ case 'object':
+ if (msg && !Array.isArray(msg))
+ return context.onError ? context.onError(getPath(id), 'EBADMSG') :
null;
+ }
+ return msg || null;
+}
diff --git a/packages/preact-message/src/use-locales.ts
b/packages/preact-message/src/use-locales.ts
new file mode 100644
index 0000000..6addd8a
--- /dev/null
+++ b/packages/preact-message/src/use-locales.ts
@@ -0,0 +1,44 @@
+import { useContext } from 'preact/hooks';
+import { MessageContext } from './message-context';
+
+/**
+ * A custom React hook providing the current locales as an array of string
identifiers, with earlier entries taking precedence over latter ones.
+ * Undefined locales are identified by an empty string `''`.
+ *
+ * @public
+ *
+ * @example
+ * ```js
+ * import React from 'preact'
+ * import { MessageProvider, useLocales } from '@messageformat/react'
+ *
+ * <MessageProvider locale="en" messages={ { foo: 'FOO' } }>
+ * {() => useLocales().join(',') // 'en'
+ * }
+ * <MessageProvider locale="fi" messages={ { foo: 'FÖÖ' } }>
+ * {() => useLocales().join(',') // 'fi,en'
+ * }
+ * </MessageProvider>
+ * </MessageProvider>
+ * ```
+ *
+ * @example
+ * ```js
+ * import React, { Component } from 'preact'
+ * import { MessageContext, MessageProvider, useLocales } from
'@messageformat/react'
+ *
+ * // Within a class component, locales are available via the context object
+ * class Foo extends Component {
+ * static contextType = MessageContext
+ * declare context: React.ContextType<typeof MessageContext> // TS
+ * render() {
+ * const { locales } = this.context
+ * return locales.join(',')
+ * }
+ * }
+ * ```
+ */
+export function useLocales() {
+ const { locales } = useContext(MessageContext);
+ return locales.slice();
+}
diff --git a/packages/preact-message/src/use-message-getter.ts
b/packages/preact-message/src/use-message-getter.ts
new file mode 100644
index 0000000..a4b08fb
--- /dev/null
+++ b/packages/preact-message/src/use-message-getter.ts
@@ -0,0 +1,47 @@
+import { useContext } from 'preact/hooks';
+import { getMessageGetter, MessageGetterOptions } from './get-message';
+import { MessageContext } from './message-context';
+
+/**
+ * A custom [React hook] providing a message getter function, which may have a
preset root id path, locale, and/or base parameters for message functions.
+ *
+ * The returned function takes two parameters `(msgId, msgParams)`, which will
extend any values set by the hook's arguments.
+ *
+ * @public
+ * @param rootId - The key or key path of the message or message object.
+ * If empty or `[]`, matches the root of the messages object
+ * @param options - If `baseParams` is set, message function parameters will
be assumed to always be an object, with these values initially set.
+ * `locale` overrides the current locale precedence as set by parent
MessageProviders.
+ *
+ * @example
+ * ```js
+ * import React from 'preact'
+ * import { MessageProvider, useMessageGetter } from '@messageformat/react'
+ *
+ * const messages = {
+ * example: {
+ * funMsg: ({ thing }) => `Your ${thing} here`,
+ * thing: 'message'
+ * }
+ * }
+ *
+ * function Example() {
+ * const getMsg = useMessageGetter('example')
+ * const thing = getMsg('thing') // 'message'
+ * return getMsg('funMsg', { thing }) // 'Your message here'
+ * }
+ *
+ * export const App = () => (
+ * <MessageProvider messages={messages}>
+ * <Example />
+ * </MessageProvider>
+ * )
+ * ```
+ */
+export function useMessageGetter(
+ rootId: string | string[],
+ opt?: MessageGetterOptions
+) {
+ const context = useContext(MessageContext);
+ return getMessageGetter(context, rootId, opt);
+}
diff --git a/packages/preact-message/src/use-message-template.ts
b/packages/preact-message/src/use-message-template.ts
new file mode 100644
index 0000000..e4de07a
--- /dev/null
+++ b/packages/preact-message/src/use-message-template.ts
@@ -0,0 +1,31 @@
+import { useContext } from 'preact/hooks';
+import { MessageGetterOptions, getPath, getMessage } from './get-message';
+import { MessageContext } from './message-context';
+
+
+export function useMessageTemplate(
+ rootId?: string | string[],
+ opt?: MessageGetterOptions
+) {
+ const context = useContext(MessageContext);
+ return getMessageGetter(context, rootId, opt);
+}
+
+export function getMessageGetter(
+ context: MessageContext,
+ rootId?: string | string[],
+ { baseParams, locale }: MessageGetterOptions = {}
+) {
+ const { pathSep } = context;
+ const pathPrefix = getPath(rootId, pathSep);
+ return function message(id?: TemplateStringsArray, ...params: any) {
+ const path = pathPrefix.concat(getPath(id?.join('%s'), pathSep));
+ const msg = getMessage(context, path, locale);
+ if (typeof msg !== 'function') return msg;
+ const msgParams = baseParams
+ ? Object.assign({}, baseParams, params)
+ : params;
+ return msg(msgParams);
+ };
+}
+
diff --git a/packages/preact-message/src/use-message.ts
b/packages/preact-message/src/use-message.ts
new file mode 100644
index 0000000..c44ec9f
--- /dev/null
+++ b/packages/preact-message/src/use-message.ts
@@ -0,0 +1,58 @@
+import { useContext } from 'preact/hooks';
+import { getMessage } from './get-message';
+import { MessageContext } from './message-context';
+
+/**
+ * A custom React hook providing an entry from the messages object of the
current or given locale.
+ * The returned value will be `undefined` if not found.
+ *
+ * If the identified message value is a function, the returned value will be
the result of calling it with a single argument `params`, or `{}` if empty.
+ * Otherwise the value set in the `MessageProvider` props will be returned
directly.
+ *
+ * @public
+ * @param id - The key or key path of the message or message object.
+ * If empty or `[]`, matches the root of the messages object
+ * @param params - Argument to use if the identified message is a function
+ * @param locale - If set, overrides the current locale precedence as set by
parent MessageProviders.
+ *
+ * @example
+ * ```js
+ * import React from 'preact'
+ * import { MessageProvider, useLocales, useMessage } from
'@messageformat/react'
+ *
+ * const en = { example: { key: 'Your message here' } }
+ * const fi = { example: { key: 'Lisää viestisi tähän' } }
+ *
+ * // Intl.ListFormat may require a polyfill, such as intl-list-format
+ * function Example() {
+ * const locales = useLocales() // ['fi', 'en']
+ * const lfOpt = { style: 'long', type: 'conjunction' }
+ * const lf = new Intl.ListFormat(locales, lfOpt)
+ * const lcMsg = lf.format(locales.map(lc => JSON.stringify(lc))) // '"fi"
ja "en"'
+ * const keyMsg = useMessage('example.key') // 'Lisää viestisi tähän'
+ * return (
+ * <article>
+ * <h1>{lcMsg}</h1>
+ * <p>{keyMsg}</p>
+ * </article>
+ * )
+ * }
+ *
+ * export const App = () => (
+ * <MessageProvider locale="en" messages={en}>
+ * <MessageProvider locale="fi" messages={fi}>
+ * <Example />
+ * </MessageProvider>
+ * </MessageProvider>
+ * )
+ * ```
+ */
+export function useMessage(
+ id: string | string[],
+ params?: any,
+ locale?: string | string[]
+) {
+ const context = useContext(MessageContext);
+ const msg = getMessage(context, id, locale);
+ return typeof msg === 'function' ? msg(params == null ? {} : params) : msg;
+}
diff --git a/packages/preact-message/tsconfig.json
b/packages/preact-message/tsconfig.json
new file mode 100644
index 0000000..2ae553e
--- /dev/null
+++ b/packages/preact-message/tsconfig.json
@@ -0,0 +1,60 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES5", /* Specify ECMAScript target
version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
+ "module": "ESNext", /* Specify module code
generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+ // "lib": [], /* Specify library files to be
included in the compilation: */
+ "allowJs": true, /* Allow javascript files to
be compiled. */
+ // "checkJs": true, /* Report errors in .js files.
*/
+ "jsx": "react", /* Specify JSX code
generation: 'preserve', 'react-native', or 'react'. */
+ "jsxFactory": "h", /* Specify the JSX factory
function to use when targeting react JSX emit, e.g. React.createElement or h. */
+ "declaration": true, /* Generates corresponding
'.d.ts' file. */
+ // "sourceMap": true, /* Generates corresponding
'.map' file. */
+ // "outFile": "./", /* Concatenate and emit output
to single file. */
+ "outDir": "./lib/", /* Redirect output structure
to the directory. */
+ // "rootDir": "./", /* Specify the root directory
of input files. Use to control the output directory structure with --outDir. */
+ // "removeComments": true, /* Do not emit comments to
output. */
+ "noEmit": false, /* Do not emit outputs. */
+ // "importHelpers": true, /* Import emit helpers from
'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for
iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'.
*/
+ // "isolatedModules": true, /* Transpile each file as a
separate module (similar to 'ts.transpileModule'). */
+
+ /* Strict Type-Checking Options */
+ "strict": true, /* Enable all strict
type-checking options. */
+ // "noImplicitAny": true, /* Raise error on expressions
and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* Enable strict null checks.
*/
+ // "noImplicitThis": true, /* Raise error on 'this'
expressions with an implied 'any' type. */
+ // "alwaysStrict": true, /* Parse in strict mode and
emit "use strict" for each source file. */
+
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused
locals. */
+ // "noUnusedParameters": true, /* Report errors on unused
parameters. */
+ // "noImplicitReturns": true, /* Report error when not all
code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for
fallthrough cases in switch statement. */
+
+ /* Module Resolution Options */
+ "moduleResolution": "node", /* Specify module resolution
strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+ "esModuleInterop": true, /* */
+ // "baseUrl": "./", /* Base directory to resolve
non-absolute module names. */
+ // "paths": {}, /* A series of entries which
re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose
combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include
type definitions from. */
+ // "types": [], /* Type declaration files to
be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from
modules with no default export. This does not affect code emit, just
typechecking. */
+ // "preserveSymlinks": true, /* Do not resolve the real
path of symlinks. */
+
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where
debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where
debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with
source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside
the sourcemaps within a single file; requires '--inlineSourceMap' or
'--sourceMap' to be set. */
+
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental
support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental
support for emitting type metadata for decorators. */
+
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of
declaration files. */
+ },
+ "include": ["src/**/*", "tests/**/*"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 242438d..934ddbf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,8 +5,9 @@ importers:
dependencies:
axios: 0.21.1
date-fns: 2.17.0
+ messageformat: 2.3.0
preact: 10.5.12
- preact-i18n: 2.3.1-preactx_preact@10.5.12
+ preact-messages: link:../preact-message
preact-router: 3.2.1_preact@10.5.12
swr: 0.4.2
yup: 0.32.9
@@ -24,7 +25,6 @@ importers:
'@testing-library/preact-hooks': 1.1.0_368c9f1500877413beac8052be555e33
'@types/enzyme': 3.10.8
'@types/jest': 26.0.20
- '@types/preact-i18n': 2.3.0
'@typescript-eslint/eslint-plugin':
4.15.1_dd080f2a8fb4d0ac76cfb4c7062ee728
'@typescript-eslint/parser': 4.15.1_eslint@7.20.0+typescript@4.1.5
ava: 3.15.0
@@ -42,6 +42,7 @@ importers:
eslint-config-preact: 1.1.3_eslint@7.20.0+typescript@4.1.5
jest: 26.6.3
jest-preset-preact: 4.0.2_120c6743da4bd73ebdbf5629f89f97bc
+ messageformat-po-loader: 0.3.0_messageformat@2.3.0
node-sass: 5.0.0
preact-cli: 3.0.5_2abf32adaded329872bb8e69d10f8425
preact-render-to-string: 5.1.12_preact@10.5.12
@@ -64,7 +65,6 @@ importers:
'@testing-library/preact-hooks': ^1.1.0
'@types/enzyme': ^3.10.5
'@types/jest': ^26.0.8
- '@types/preact-i18n': ^2.3.0
'@typescript-eslint/eslint-plugin': ^4.15.1
'@typescript-eslint/parser': ^4.15.1
ava: ^3.15.0
@@ -84,10 +84,12 @@ importers:
eslint-config-preact: ^1.1.1
jest: ^26.2.2
jest-preset-preact: ^4.0.2
+ messageformat: ^2.3.0
+ messageformat-po-loader: ^0.3.0
node-sass: ^5.0.0
preact: ^10.3.1
preact-cli: ^3.0.5
- preact-i18n: 2.3.1-preactx
+ preact-messages: workspace:*
preact-render-to-string: ^5.1.4
preact-router: ^3.2.1
rimraf: ^3.0.2
@@ -97,6 +99,13 @@ importers:
typedoc: ^0.20.25
typescript: ^4.1.3
yup: ^0.32.8
+ packages/preact-message:
+ dependencies:
+ preact: 10.5.12
+ typescript: 4.1.5
+ specifiers:
+ preact: ^10.5.12
+ typescript: ^4.1.5
lockfileVersion: 5.2
packages:
/@babel/code-frame/7.12.11:
@@ -3121,12 +3130,6 @@ packages:
dev: true
resolution:
integrity:
sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
- /@types/preact-i18n/2.3.0:
- dependencies:
- preact: 10.5.12
- dev: true
- resolution:
- integrity:
sha512-qDgb5QbPnWJ141y+fca5R3MBQis5h7ITnSB9WQiHj5WH41Q5g9Wc4rCnqYERfqSBSC0ac4cE1JAlFisiAUIiLw==
/@types/prettier/2.2.1:
dev: true
resolution:
@@ -6524,10 +6527,6 @@ packages:
dev: true
resolution:
integrity: sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
- /dlv/1.1.3:
- dev: false
- resolution:
- integrity:
sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
/dns-equal/1.0.0:
dev: true
resolution:
@@ -6857,6 +6856,12 @@ packages:
node: '>= 0.8'
resolution:
integrity: sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+ /encoding/0.1.13:
+ dependencies:
+ iconv-lite: 0.6.2
+ dev: true
+ resolution:
+ integrity:
sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
/end-of-stream/1.4.4:
dependencies:
once: 1.4.0
@@ -8075,6 +8080,21 @@ packages:
dev: true
resolution:
integrity: sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ /gettext-parser/1.4.0:
+ dependencies:
+ encoding: 0.1.13
+ safe-buffer: 5.2.1
+ dev: true
+ resolution:
+ integrity:
sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==
+ /gettext-to-messageformat/0.3.1:
+ dependencies:
+ gettext-parser: 1.4.0
+ dev: true
+ engines:
+ node: '>=6.0'
+ resolution:
+ integrity:
sha512-UyqIL3Ul4NryU95Wome/qtlcuVIqgEWVIFw0zi7Lv14ACLXfaVDCbrjZ7o+3BZ7u+4NS1mP/2O1eXZoHCoas8g==
/github-slugger/1.3.0:
dependencies:
emoji-regex: 6.1.1
@@ -8827,6 +8847,14 @@ packages:
node: '>=0.10.0'
resolution:
integrity:
sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ /iconv-lite/0.6.2:
+ dependencies:
+ safer-buffer: 2.1.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity:
sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
/icss-utils/4.1.1:
dependencies:
postcss: 7.0.35
@@ -10811,6 +10839,13 @@ packages:
node: '>=8'
resolution:
integrity:
sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
+ /make-plural/4.3.0:
+ dev: false
+ hasBin: true
+ optionalDependencies:
+ minimist: 1.2.5
+ resolution:
+ integrity:
sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==
/makeerror/1.0.11:
dependencies:
tmpl: 1.0.4
@@ -11026,6 +11061,34 @@ packages:
node: '>= 8'
resolution:
integrity:
sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+ /messageformat-formatters/2.0.1:
+ dev: false
+ resolution:
+ integrity:
sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg==
+ /messageformat-parser/4.1.3:
+ dev: false
+ resolution:
+ integrity:
sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg==
+ /messageformat-po-loader/0.3.0_messageformat@2.3.0:
+ dependencies:
+ gettext-to-messageformat: 0.3.1
+ loader-utils: 1.4.0
+ messageformat: 2.3.0
+ dev: true
+ engines:
+ node: '>=6.0'
+ peerDependencies:
+ messageformat: 1.x | 2.x
+ resolution:
+ integrity:
sha512-thu/A7hNl/iBHsRXUdmiy/nEFJZku3bsBMXL53HgHm+I0JaVU9lSpwuQAe7huCO4INGxgZtDoPAEpeb1ZeI5lg==
+ /messageformat/2.3.0:
+ dependencies:
+ make-plural: 4.3.0
+ messageformat-formatters: 2.0.1
+ messageformat-parser: 4.1.3
+ dev: false
+ resolution:
+ integrity:
sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==
/methods/1.1.2:
dev: true
engines:
@@ -11154,7 +11217,6 @@ packages:
resolution:
integrity:
sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
/minimist/1.2.5:
- dev: true
resolution:
integrity:
sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
/minipass-collect/1.0.2:
@@ -12823,24 +12885,6 @@ packages:
preact-render-to-string: '*'
resolution:
integrity:
sha512-Oc9HOjwX/3Zk1eXkmP7TMmtqbaROl7F0RWZ2Ni5Q/grmx3yBLJmarkUcOSKabkI/Usw2dU3RVju32Q3Pvy5qIw==
- /preact-i18n/2.3.1-preactx_preact@10.5.12:
- dependencies:
- dlv: 1.1.3
- preact: 10.5.12
- preact-markup: 2.1.1_preact@10.5.12
- dev: false
- peerDependencies:
- preact: '>=10'
- resolution:
- integrity:
sha512-i/QGG3BQOWh4nFPXTnhazHGOq2STYMa9/0h6oiUkV+p/c5IDd0luPhRlXkAnEgGRZX3PjAEgx/tzWPQne61wuQ==
- /preact-markup/2.1.1_preact@10.5.12:
- dependencies:
- preact: 10.5.12
- dev: false
- peerDependencies:
- preact: '>=10'
- resolution:
- integrity:
sha512-8JL2p36mzK8XkspOyhBxUSPjYwMxDM0L5BWBZWxsZMVW8WsGQrYQDgVuDKkRspt2hwrle+Cxr/053hpc9BJwfw==
/preact-render-to-string/5.1.12_preact@10.5.12:
dependencies:
preact: 10.5.12
@@ -12859,6 +12903,7 @@ packages:
resolution:
integrity:
sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg==
/preact/10.5.12:
+ dev: false
resolution:
integrity:
sha512-r6siDkuD36oszwlCkcqDJCAKBQxGoeEGytw2DGMD5A/GGdu5Tymw+N2OBXwvOLxg6d1FeY8MgMV3cc5aVQo4Cg==
/prelude-ls/1.1.2:
@@ -15755,7 +15800,6 @@ packages:
resolution:
integrity:
sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==
/typescript/4.1.5:
- dev: true
engines:
node: '>=4.2.0'
hasBin: true
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.