gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]