gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 02/06: using web-utils in anastasis-webui


From: gnunet
Subject: [taler-wallet-core] 02/06: using web-utils in anastasis-webui
Date: Tue, 06 Dec 2022 16:13:37 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit d3a6544bc5fd7b3d2d65494ba1c3155b024a436e
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Dec 6 11:21:12 2022 -0300

    using web-utils in anastasis-webui
---
 packages/anastasis-webui/build.mjs                 | 147 +++++++++
 packages/anastasis-webui/clean_and_build.sh        |  72 ----
 packages/anastasis-webui/dev.mjs                   |  91 +----
 packages/anastasis-webui/html/stories.html         |  72 ----
 packages/anastasis-webui/html/ui-dev.html          |  65 ----
 packages/anastasis-webui/package.json              |  12 +-
 .../src/components/menu/SideBar.tsx                |  33 +-
 .../src/components/picker/DurationPicker.tsx       |  12 +-
 .../anastasis-webui/src/context/translation.ts     |  41 ++-
 packages/anastasis-webui/src/hooks/index.ts        |  71 +---
 .../src/{scss/_mixins.scss => hooks/useLang.ts}    |  24 +-
 .../src/hooks/{index.ts => useLocalStorage.ts}     |  69 +---
 packages/anastasis-webui/src/i18n/index.tsx        | 211 ------------
 .../{html/ui.html => src/index.html}               |  32 +-
 .../src/{main.test.ts => index.test.ts}            |   0
 packages/anastasis-webui/src/index.ts              |  25 +-
 packages/anastasis-webui/src/main.ts               |  41 ---
 .../pages/home/AddingProviderScreen/stories.tsx    |   1 +
 .../src/pages/home/AddingProviderScreen/views.tsx  |   4 +-
 .../pages/home/AttributeEntryScreen.stories.tsx    |   1 +
 .../home/AuthenticationEditorScreen.stories.tsx    |   1 +
 .../pages/home/BackupFinishedScreen.stories.tsx    |   1 +
 .../pages/home/ChallengeOverviewScreen.stories.tsx |   1 +
 .../pages/home/ChallengePayingScreen.stories.tsx   |   1 +
 .../home/ContinentSelectionScreen.stories.tsx      |   1 +
 .../src/pages/home/EditPoliciesScreen.stories.tsx  |   1 +
 .../pages/home/PoliciesPayingScreen.stories.tsx    |   1 +
 .../pages/home/RecoveryFinishedScreen.stories.tsx  |   1 +
 .../pages/home/ReviewPoliciesScreen.stories.tsx    |   1 +
 .../src/pages/home/SecretEditorScreen.stories.tsx  |   1 +
 .../pages/home/SecretSelectionScreen.stories.tsx   |   1 +
 .../src/pages/home/SolveScreen.stories.tsx         |   1 +
 .../src/pages/home/StartScreen.stories.tsx         |   1 +
 .../src/pages/home/TruthsPayingScreen.stories.tsx  |   1 +
 .../authMethod/AuthMethodEmailSetup.stories.tsx    |   1 +
 .../authMethod/AuthMethodEmailSolve.stories.tsx    |   1 +
 .../pages/home/authMethod/AuthMethodEmailSolve.tsx |   6 +-
 .../authMethod/AuthMethodIbanSetup.stories.tsx     |   1 +
 .../authMethod/AuthMethodIbanSolve.stories.tsx     |   1 +
 .../authMethod/AuthMethodPostSetup.stories.tsx     |   1 +
 .../authMethod/AuthMethodPostSolve.stories.tsx     |   1 +
 .../pages/home/authMethod/AuthMethodPostSolve.tsx  |   6 +-
 .../authMethod/AuthMethodQuestionSetup.stories.tsx |   1 +
 .../authMethod/AuthMethodQuestionSolve.stories.tsx |   1 +
 .../home/authMethod/AuthMethodSmsSetup.stories.tsx |   1 +
 .../home/authMethod/AuthMethodSmsSolve.stories.tsx |   1 +
 .../pages/home/authMethod/AuthMethodSmsSolve.tsx   |   6 +-
 .../authMethod/AuthMethodTotpSetup.stories.tsx     |   1 +
 .../authMethod/AuthMethodTotpSolve.stories.tsx     |   1 +
 packages/anastasis-webui/src/scss/_mixins.scss     |   2 +-
 packages/anastasis-webui/src/stories.tsx           | 365 +--------------------
 packages/anastasis-webui/src/test-utils.ts         |  12 +-
 packages/anastasis-webui/src/utils/index.tsx       |  16 +-
 packages/anastasis-webui/watch/reply.sh            |  18 -
 packages/anastasis-webui/watch/send.sh             |  12 -
 packages/anastasis-webui/watch/send2.sh            |  14 -
 packages/anastasis-webui/watch/serve.sh            |   7 -
 .../watch/web_socket_client.request                |   6 -
 .../anastasis-webui/watch/web_socket_server.reply  |   5 -
 59 files changed, 355 insertions(+), 1170 deletions(-)

diff --git a/packages/anastasis-webui/build.mjs 
b/packages/anastasis-webui/build.mjs
new file mode 100755
index 000000000..ebe914541
--- /dev/null
+++ b/packages/anastasis-webui/build.mjs
@@ -0,0 +1,147 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it 
under the
+ terms of the GNU Affero General Public License as published by the Free 
Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more 
details.
+
+ You should have received a copy of the GNU Affero General Public License 
along with
+ GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
+ */
+/* eslint-disable no-undef */
+import esbuild from 'esbuild'
+import fs from 'fs';
+import path from "path"
+import sass from "sass";
+
+// eslint-disable-next-line no-undef
+const BASE = process.cwd();
+
+const preact = path.join(
+  BASE,
+  "node_modules",
+  "preact",
+  "compat",
+  "dist",
+  "compat.module.js",
+);
+
+const preactCompatPlugin = {
+  name: "preact-compat",
+  setup(build) {
+    build.onResolve({ filter: /^(react-dom|react)$/ }, (args) => {
+      return {
+        path: preact,
+      };
+    });
+  },
+};
+
+
+let GIT_ROOT = BASE
+while (!fs.existsSync(path.join(GIT_ROOT, '.git')) && GIT_ROOT !== '/') {
+  GIT_ROOT = path.join(GIT_ROOT, '../')
+}
+if (GIT_ROOT === '/') {
+  // eslint-disable-next-line no-undef
+  console.log("not found")
+  // eslint-disable-next-line no-undef
+  process.exit(1);
+}
+const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash()
+
+
+let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json')));
+
+function git_hash() {
+  const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 
'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0];
+  if (rev.indexOf('/') === -1) {
+    return rev;
+  } else {
+    return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim();
+  }
+}
+
+const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/
+
+const buildSassPlugin = {
+  name: "custom-build-sass",
+  setup(build) {
+
+    build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
+      const resolveDir = path.dirname(file)
+      const { css: contents } = sass.compile(file, { loadPaths: ["./"] })
+
+      return {
+        resolveDir,
+        loader: 'css',
+        contents
+      }
+    });
+
+  },
+};
+
+function copyFilesPlugin(options) {
+  if (!options.basedir) {
+    options.basedir = process.cwd()
+  }
+  return {
+    name: "copy-files",
+    setup(build) {
+      build.onEnd(() => {
+        for (const fop of options) {
+          fs.copyFileSync(path.join(options.basedir, fop.src), 
path.join(options.basedir, fop.dest));
+        }
+      });
+    },
+  };
+}
+
+export const buildConfig = {
+  entryPoints: ['src/index.ts', 'src/stories.tsx'],
+  bundle: true,
+  outdir: 'dist',
+  minify: false,
+  loader: {
+    '.svg': 'dataurl',
+    '.ttf': 'file',
+    '.woff': 'file',
+    '.woff2': 'file',
+    '.eot': 'file',
+  },
+  target: [
+    'es6'
+  ],
+  format: 'esm',
+  platform: 'browser',
+  sourcemap: true,
+  jsxFactory: 'h',
+  jsxFragment: 'Fragment',
+  define: {
+    '__VERSION__': `"${_package.version}"`,
+    '__GIT_HASH__': `"${GIT_HASH}"`,
+  },
+  plugins: [
+    preactCompatPlugin,
+    copyFilesPlugin([
+      {
+        src: "./src/index.html",
+        dest: "./dist/index.html",
+      },
+    ]),
+    buildSassPlugin
+  ],
+}
+
+await esbuild.build(buildConfig)
+
+
+
+
diff --git a/packages/anastasis-webui/clean_and_build.sh 
b/packages/anastasis-webui/clean_and_build.sh
deleted file mode 100755
index 9486848fe..000000000
--- a/packages/anastasis-webui/clean_and_build.sh
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env bash
-
-echo clean
-rm -rf dist
-mkdir -p dist/fonts
-cp \
-       src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf \
-       src/scss/fonts/materialdesignicons-webfont-4.9.95.ttf \
-       src/scss/fonts/materialdesignicons-webfont-4.9.95.woff \
-       src/scss/fonts/materialdesignicons-webfont-4.9.95.woff2 \
-       dist/fonts
-
-VERSION=$(jq -r .version package.json)
-GIT_HASH=$(git rev-parse --short HEAD)
-
-function build_css() {
-       pnpm exec sass -I . ./src/scss/main.scss dist/main.css
-}
-function build_js() {
-       pnpm exec esbuild --log-level=error 
--define:process.env.__VERSION__=\"${VERSION}\" 
--define:process.env.__GIT_HASH__=\"${GIT_HASH}\"  --bundle $1 --outdir=dist 
--target=es6 --loader:.svg=dataurl --format=iife --sourcemap --jsx-factory=h 
--jsx-fragment=Fragment --platform=browser --minify
-}
-
-function build_html() {
-       cat html/$1.html \
-         | sed -e '/ANASTASIS_SCRIPT_CONTENT/ {' -e 'r dist/main.js' -e 'd' -e 
'}' \
-         | sed -e '/ANASTASIS_STYLE_CONTENT/ {' -e 'r dist/main.css' -e 'd' -e 
'}' \
-         >dist/$1.html
-}
-
-function cleanup {
- trap - SIGHUP SIGINT SIGTERM SIGQUIT
- echo -n "Cleaning up... "
- wait
- kill -- -$$
- exit 1
-}
-trap cleanup SIGHUP SIGINT SIGTERM SIGQUIT
-
-set -e
-echo compile
-build_css &
-build_js src/main.ts &
-build_js src/stories.tsx &
-build_js src/main.test.ts &
-for file in $(find src/ -name test.ts); do build_js $file; done &
-wait -n
-wait -n
-wait -n
-wait -n
-wait -n
-pnpm run --silent test -- -R dot
-
-echo html
-build_html ui
-build_html ui-dev
-build_html stories
-
-if [ "WATCH" == "$1" ]; then
-
-  echo watch mode
-  echo Writing any file in the src directory will trigger a browser reload.
-  echo Be sure that the watcher server is running.
-  echo ./watch/serve.sh
-  inotifywait -e close_write -r src -q -m | while read line; do
-    echo $(date) $line
-    build_js src/main.ts
-    build_html ui-dev
-               build_js src/stories.tsx
-               build_html stories
-    ./watch/send.sh '{"type":"RELOAD"}'
-  done;
-fi
diff --git a/packages/anastasis-webui/dev.mjs b/packages/anastasis-webui/dev.mjs
index 3f4915ffc..0446603dc 100755
--- a/packages/anastasis-webui/dev.mjs
+++ b/packages/anastasis-webui/dev.mjs
@@ -14,87 +14,18 @@
  You should have received a copy of the GNU Affero General Public License 
along with
  GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
-/* eslint-disable no-undef */
-import esbuild from 'esbuild'
-import fs from 'fs';
-import WebSocket from "ws";
-import chokidar from "chokidar";
 
-const devServerBroadcastDelay = 500
-const devServerPort = 8002
-const wss = new WebSocket.Server({ port: devServerPort });
-const toWatch = ["./src"]
-
-function broadcast(file, event) {
-  setTimeout(() => {
-    wss.clients.forEach((client) => {
-      if (client.readyState === WebSocket.OPEN) {
-        console.log(new Date(), file)
-        client.send(JSON.stringify(event));
-      }
-    });
-  }, devServerBroadcastDelay);
-}
-
-const watcher = chokidar
-  .watch(toWatch, {
-    persistent: true,
-    ignoreInitial: true,
-    awaitWriteFinish: {
-      stabilityThreshold: 100,
-      pollInterval: 100,
-    },
-  })
-  .on("error", (error) => console.error(error))
-  .on("change", async (file) => {
-    broadcast(file, { type: "RELOAD" });
-  })
-  .on("add", async (file) => {
-    broadcast(file, { type: "RELOAD" });
-  })
-  .on("unlink", async (file) => {
-    broadcast(file, { type: "RELOAD" });
-  });
-
-/**
- * Just bundling UI Stories.
- * FIXME: add linaria CSS after implementing Material so CSS will be bundled
- */
-fs.writeFileSync("dist/index.html", fs.readFileSync("html/stories.html"))
-fs.writeFileSync("dist/mocha.css", 
fs.readFileSync("node_modules/mocha/mocha.css"))
-fs.writeFileSync("dist/mocha.js", 
fs.readFileSync("node_modules/mocha/mocha.js"))
-fs.writeFileSync("dist/mocha.js.map", 
fs.readFileSync("node_modules/mocha/mocha.js.map"))
-
-export const buildConfig = {
-  entryPoints: ['src/main.ts', 'src/stories.tsx'],
-  bundle: true,
-  outdir: 'dist',
-  minify: false,
-  loader: {
-    '.svg': 'dataurl',
-  },
-  target: [
-    'es6'
-  ],
-  format: 'iife',
-  platform: 'browser',
-  sourcemap: true,
-  jsxFactory: 'h',
-  jsxFragment: 'Fragment',
-}
-
-const server = await esbuild
-  .serve({ servedir: 'dist' }, {
-    ...buildConfig, outdir: 'dist'
-  })
-  .catch((e) => {
-    console.log(e)
-    process.exit(1)
-  });
-
-console.log(`Dev server is ready at http://localhost:${server.port}/.
-The server is running a using websocket at ${devServerPort} to notify code 
change and live reload.
-`);
+import { serve } from "@gnu-taler/web-util/lib/index.node";
+import esbuild from 'esbuild';
+import { buildConfig } from "./build.mjs";
 
+buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
 
+serve({
+  folder: './dist',
+  port: 8080,
+  source: './src',
+  development: true,
+  onUpdate: async () => esbuild.build(buildConfig)
+})
 
diff --git a/packages/anastasis-webui/html/stories.html 
b/packages/anastasis-webui/html/stories.html
deleted file mode 100644
index 9f41fdeaf..000000000
--- a/packages/anastasis-webui/html/stories.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <title>Stories</title>
-    <style>
-      /* page css */
-      div.page {
-        margin: 0px;
-        padding: 0px;
-        font-size: 100%;
-        font-family: Arial, Helvetica, sans-serif;
-      }
-      div.page p:not([class]) {
-        margin-bottom: 1em;
-        margin-top: 1em;
-      }
-      div.page {
-        width: 100%;
-        display: flex;
-        flex-direction: row;
-      }
-      /* sidebar css */
-      div.sidebar {
-        min-width: 200px;
-        height: calc(100vh - 20px);
-        overflow-y: visible;
-        overflow-x: hidden;
-        scroll-behavior: smooth;
-      }
-      div.sidebar > ol {
-        padding: 4px;
-      }
-      div.sidebar div:first-child {
-        background-color: lightcoral;
-        cursor: pointer;
-      }
-      div.sidebar div[data-hide="true"] {
-        display: none;
-      }
-      div.sidebar dd {
-        margin-left: 1em;
-        padding: 4px;
-        cursor: pointer;
-        border-radius: 4px;
-        margin-bottom: 4px;
-      }
-      div.sidebar dd:nth-child(even) {
-        background-color: lightgray;
-      }
-      div.sidebar dd:nth-child(odd) {
-        background-color: lightblue;
-      }
-      div.sidebar a {
-        color: black;
-      }
-      div.sidebar dd[data-selected] {
-        background-color: green;
-      }
-
-      /* content css */
-      div.content {
-        width: 100%;
-        padding: 20px;
-      }
-    </style>
-    <script src="./stories.js"></script>
-    <link rel="stylesheet" href="./main.css" />
-  </head>
-  <body>
-    <taler-stories id="container"></taler-stories>
-  </body>
-</html>
diff --git a/packages/anastasis-webui/html/ui-dev.html 
b/packages/anastasis-webui/html/ui-dev.html
deleted file mode 100644
index 2790d5678..000000000
--- a/packages/anastasis-webui/html/ui-dev.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<!DOCTYPE html>
-<html
-  lang="en"
-  class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top 
has-aside-expanded"
->
-  <head>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width,initial-scale=1" />
-    <meta name="mobile-web-app-capable" content="yes" />
-    <meta name="apple-mobile-web-app-capable" content="yes" />
-
-    <link
-      rel="icon"
-      
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/
 [...]
-    />
-    <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
-    <style id="style-id" type="text/css">
-      /* <![CDATA[ */
-      ANASTASIS_STYLE_CONTENT
-      /* <![CDATA[ */
-    </style>
-  </head>
-
-  <body>
-    <div id="container" class="anastasis-container"></div>
-    <script id="code" type="application/javascript">
-      ANASTASIS_SCRIPT_CONTENT;
-    </script>
-    <script type="application/javascript">
-      function setupLiveReload() {
-        const socketPath = `ws://localhost:8003/socket`;
-        console.log("connecting to ", socketPath);
-        const ws = new WebSocket(socketPath);
-        ws.onmessage = (message) => {
-          const event = JSON.parse(message.data);
-          if (event.type === "LOG") {
-            console.log(event.message);
-          }
-          if (event.type === "RELOAD") {
-            window.location.reload();
-          }
-          if (event.type === "UPDATE") {
-            document.body.removeChild(document.getElementById("container"));
-            const d = document.createElement("div");
-            d.setAttribute("id", "container");
-            d.setAttribute("class", "anastasis-container");
-            document.body.appendChild(d);
-            const s = document.createElement("script");
-            s.setAttribute("id", "code");
-            s.setAttribute("type", "application/javascript");
-            s.textContent = atob(event.content);
-            document.body.appendChild(s);
-          }
-        };
-        ws.onerror = (error) => {
-          console.error(error);
-        };
-        ws.onclose = (e) => {
-          setTimeout(setupLiveReload, 500);
-        };
-      }
-      setupLiveReload();
-    </script>
-  </body>
-</html>
diff --git a/packages/anastasis-webui/package.json 
b/packages/anastasis-webui/package.json
index de70d05fc..c01856243 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -15,11 +15,12 @@
   "dependencies": {
     "@gnu-taler/anastasis-core": "workspace:*",
     "@gnu-taler/taler-util": "workspace:*",
+    "@gnu-taler/web-util": "workspace:*",
     "@types/chai": "^4.3.0",
     "chai": "^4.3.6",
     "date-fns": "2.29.2",
     "jed": "1.1.1",
-    "preact": "^10.5.15",
+    "preact": "10.11.3",
     "preact-render-to-string": "^5.1.19",
     "preact-router": "^3.2.1",
     "qrcode-generator": "^1.4.4"
@@ -41,12 +42,9 @@
     "bulma": "^0.9.3",
     "bulma-checkbox": "^1.1.1",
     "bulma-radio": "^1.1.1",
-    "chokidar": "^3.5.3",
-    "eslint-plugin-header": "^3.1.1",
     "jssha": "^3.2.0",
     "mocha": "^9.2.0",
-    "sass": "1.32.13",
-    "typescript": "^4.8.4",
-    "ws": "7.4.5"
+    "sass": "1.56.1",
+    "typescript": "^4.8.4"
   }
-}
+}
\ No newline at end of file
diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx 
b/packages/anastasis-webui/src/components/menu/SideBar.tsx
index 51e854944..3dac73e04 100644
--- a/packages/anastasis-webui/src/components/menu/SideBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -22,7 +22,7 @@
 import { BackupStates, RecoveryStates } from "@gnu-taler/anastasis-core";
 import { Fragment, h, VNode } from "preact";
 import { useAnastasisContext } from "../../context/anastasis.js";
-import { Translate } from "../../i18n/index.js";
+import { useTranslationContext } from "../../context/translation.js";
 
 interface Props {
   mobile?: boolean;
@@ -34,6 +34,7 @@ const VERSION_WITH_HASH = GIT_HASH ? `${VERSION}-${GIT_HASH}` 
: VERSION;
 
 export function Sidebar({ mobile }: Props): VNode {
   const reducer = useAnastasisContext()!;
+  const { i18n } = useTranslationContext();
 
   function saveSession(): void {
     const state = reducer.exportState();
@@ -64,7 +65,7 @@ export function Sidebar({ mobile }: Props): VNode {
       <div class="menu is-menu-main">
         {!reducer.currentReducerState && (
           <p class="menu-label">
-            <Translate>Backup or Recorver</Translate>
+            <i18n.Translate>Backup or Recorver</i18n.Translate>
           </p>
         )}
         <ul class="menu-list">
@@ -72,7 +73,7 @@ export function Sidebar({ mobile }: Props): VNode {
             <li>
               <div class="ml-4">
                 <span class="menu-item-label">
-                  <Translate>Select one option</Translate>
+                  <i18n.Translate>Select one option</i18n.Translate>
                 </span>
               </div>
             </li>
@@ -91,7 +92,7 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Location</Translate>
+                    <i18n.Translate>Location</i18n.Translate>
                   </span>
                 </div>
               </li>
@@ -105,7 +106,7 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Personal information</Translate>
+                    <i18n.Translate>Personal information</i18n.Translate>
                   </span>
                 </div>
               </li>
@@ -119,7 +120,7 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Authorization methods</Translate>
+                    <i18n.Translate>Authorization methods</i18n.Translate>
                   </span>
                 </div>
               </li>
@@ -133,7 +134,7 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Policies</Translate>
+                    <i18n.Translate>Policies</i18n.Translate>
                   </span>
                 </div>
               </li>
@@ -147,14 +148,14 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Secret input</Translate>
+                    <i18n.Translate>Secret input</i18n.Translate>
                   </span>
                 </div>
               </li>
               {/* <li class={reducer.currentReducerState.backup_state === 
BackupStates.PoliciesPaying ? 'is-active' : ''}>
               <div class="ml-4">
 
-                <span class="menu-item-label"><Translate>Payment 
(optional)</Translate></span>
+                <span class="menu-item-label"><i18n.Translate>Payment 
(optional)</i18n.Translate></span>
               </div>
             </li> */}
               <li
@@ -167,14 +168,14 @@ export function Sidebar({ mobile }: Props): VNode {
               >
                 <div class="ml-4">
                   <span class="menu-item-label">
-                    <Translate>Backup completed</Translate>
+                    <i18n.Translate>Backup completed</i18n.Translate>
                   </span>
                 </div>
               </li>
               {/* <li class={reducer.currentReducerState.backup_state === 
BackupStates.TruthsPaying ? 'is-active' : ''}>
               <div class="ml-4">
 
-                <span class="menu-item-label"><Translate>Truth 
Paying</Translate></span>
+                <span class="menu-item-label"><i18n.Translate>Truth 
Paying</i18n.Translate></span>
               </div>
             </li> */}
               {reducer.currentReducerState.backup_state !==
@@ -219,7 +220,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Location</Translate>
+                      <i18n.Translate>Location</i18n.Translate>
                     </span>
                   </div>
                 </li>
@@ -233,7 +234,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Personal information</Translate>
+                      <i18n.Translate>Personal information</i18n.Translate>
                     </span>
                   </div>
                 </li>
@@ -247,7 +248,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Secret selection</Translate>
+                      <i18n.Translate>Secret selection</i18n.Translate>
                     </span>
                   </div>
                 </li>
@@ -263,7 +264,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Solve Challenges</Translate>
+                      <i18n.Translate>Solve Challenges</i18n.Translate>
                     </span>
                   </div>
                 </li>
@@ -277,7 +278,7 @@ export function Sidebar({ mobile }: Props): VNode {
                 >
                   <div class="ml-4">
                     <span class="menu-item-label">
-                      <Translate>Secret recovered</Translate>
+                      <i18n.Translate>Secret recovered</i18n.Translate>
                     </span>
                   </div>
                 </li>
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx 
b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
index 12ed158dd..c4caaec9f 100644
--- a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
+++ b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
@@ -21,7 +21,7 @@
 
 import { h, VNode } from "preact";
 import { useState } from "preact/hooks";
-import { useTranslator } from "../../i18n/index.js";
+import { useTranslationContext } from "../../context/translation.js";
 import "../../scss/DurationPicker.scss";
 
 export interface Props {
@@ -46,13 +46,13 @@ export function DurationPicker({
   const ms = ss * 60;
   const hs = ms * 60;
   const ds = hs * 24;
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   return (
     <div class="rdp-picker">
       {days && (
         <DurationColumn
-          unit={i18n`days`}
+          unit={i18n.str`days`}
           max={99}
           value={Math.floor(value / ds)}
           onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
@@ -62,7 +62,7 @@ export function DurationPicker({
       )}
       {hours && (
         <DurationColumn
-          unit={i18n`hours`}
+          unit={i18n.str`hours`}
           max={23}
           min={1}
           value={Math.floor(value / hs) % 24}
@@ -73,7 +73,7 @@ export function DurationPicker({
       )}
       {minutes && (
         <DurationColumn
-          unit={i18n`minutes`}
+          unit={i18n.str`minutes`}
           max={59}
           min={1}
           value={Math.floor(value / ms) % 60}
@@ -84,7 +84,7 @@ export function DurationPicker({
       )}
       {seconds && (
         <DurationColumn
-          unit={i18n`seconds`}
+          unit={i18n.str`seconds`}
           max={59}
           value={Math.floor(value / ss) % 60}
           onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
diff --git a/packages/anastasis-webui/src/context/translation.ts 
b/packages/anastasis-webui/src/context/translation.ts
index 87704a13f..44faaa456 100644
--- a/packages/anastasis-webui/src/context/translation.ts
+++ b/packages/anastasis-webui/src/context/translation.ts
@@ -19,23 +19,42 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
+import { i18n, setupI18n } from "@gnu-taler/taler-util";
 import { createContext, h, VNode } from "preact";
 import { useContext, useEffect } from "preact/hooks";
-import { useLang } from "../hooks/index.js";
-import * as jedLib from "jed";
+import { useLang } from "../hooks/useLang.js";
 import { strings } from "../i18n/strings.js";
 
 interface Type {
   lang: string;
-  handler: any;
+  supportedLang: { [id in keyof typeof supportedLang]: string };
   changeLanguage: (l: string) => void;
+  i18n: typeof i18n;
+  isSaved: boolean;
 }
+
+const supportedLang = {
+  es: "Español [es]",
+  ja: "日本語 [ja]",
+  en: "English [en]",
+  fr: "Français [fr]",
+  de: "Deutsch [de]",
+  sv: "Svenska [sv]",
+  it: "Italiano [it]",
+  // ko: "한국어 [ko]",
+  // ru: "Ру́сский язы́к [ru]",
+  tr: "Türk [tr]",
+  navigator: "Defined by navigator",
+};
+
 const initial = {
   lang: "en",
-  handler: null,
+  supportedLang,
   changeLanguage: () => {
     // do not change anything
   },
+  i18n,
+  isSaved: false,
 };
 const Context = createContext<Type>(initial);
 
@@ -50,15 +69,23 @@ export const TranslationProvider = ({
   children,
   forceLang,
 }: Props): VNode => {
-  const [lang, changeLanguage] = useLang(initial);
+  const [lang, changeLanguage, isSaved] = useLang(initial);
   useEffect(() => {
     if (forceLang) {
       changeLanguage(forceLang);
     }
   });
-  const handler = new jedLib.Jed(strings[lang] || strings["en"]);
+  useEffect(() => {
+    setupI18n(lang, strings);
+  }, [lang]);
+  if (forceLang) {
+    setupI18n(forceLang, strings);
+  } else {
+    setupI18n(lang, strings);
+  }
+
   return h(Context.Provider, {
-    value: { lang, handler, changeLanguage },
+    value: { lang, changeLanguage, supportedLang, i18n, isSaved },
     children,
   });
 };
diff --git a/packages/anastasis-webui/src/hooks/index.ts 
b/packages/anastasis-webui/src/hooks/index.ts
index c03e834d7..2dbf4fa5c 100644
--- a/packages/anastasis-webui/src/hooks/index.ts
+++ b/packages/anastasis-webui/src/hooks/index.ts
@@ -19,7 +19,9 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { StateUpdater, useState } from "preact/hooks";
+import { StateUpdater } from "preact/hooks";
+import { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js";
+
 export type ValueOrFunction<T> = T | ((p: T) => T);
 
 const calculateRootPath = () => {
@@ -69,69 +71,4 @@ export function useBackendInstanceToken(
   }
 
   return [token, setToken];
-}
-
-export function useLang(initial?: string): [string, StateUpdater<string>] {
-  const browserLang =
-    typeof window !== "undefined"
-      ? navigator.language || (navigator as any).userLanguage
-      : undefined;
-  const defaultLang = (browserLang || initial || "en").substring(0, 2);
-  return useNotNullLocalStorage("lang-preference", defaultLang);
-}
-
-export function useLocalStorage(
-  key: string,
-  initialValue?: string,
-): [string | undefined, StateUpdater<string | undefined>] {
-  const [storedValue, setStoredValue] = useState<string | undefined>(
-    (): string | undefined => {
-      return typeof window !== "undefined"
-        ? window.localStorage.getItem(key) || initialValue
-        : initialValue;
-    },
-  );
-
-  const setValue = (
-    value?: string | ((val?: string) => string | undefined),
-  ) => {
-    setStoredValue((p) => {
-      const toStore = value instanceof Function ? value(p) : value;
-      if (typeof window !== "undefined") {
-        if (!toStore) {
-          window.localStorage.removeItem(key);
-        } else {
-          window.localStorage.setItem(key, toStore);
-        }
-      }
-      return toStore;
-    });
-  };
-
-  return [storedValue, setValue];
-}
-
-export function useNotNullLocalStorage(
-  key: string,
-  initialValue: string,
-): [string, StateUpdater<string>] {
-  const [storedValue, setStoredValue] = useState<string>((): string => {
-    return typeof window !== "undefined"
-      ? window.localStorage.getItem(key) || initialValue
-      : initialValue;
-  });
-
-  const setValue = (value: string | ((val: string) => string)) => {
-    const valueToStore = value instanceof Function ? value(storedValue) : 
value;
-    setStoredValue(valueToStore);
-    if (typeof window !== "undefined") {
-      if (!valueToStore) {
-        window.localStorage.removeItem(key);
-      } else {
-        window.localStorage.setItem(key, valueToStore);
-      }
-    }
-  };
-
-  return [storedValue, setValue];
-}
+}
\ No newline at end of file
diff --git a/packages/anastasis-webui/src/scss/_mixins.scss 
b/packages/anastasis-webui/src/hooks/useLang.ts
similarity index 59%
copy from packages/anastasis-webui/src/scss/_mixins.scss
copy to packages/anastasis-webui/src/hooks/useLang.ts
index 64315785b..5b02c5255 100644
--- a/packages/anastasis-webui/src/scss/_mixins.scss
+++ b/packages/anastasis-webui/src/hooks/useLang.ts
@@ -14,21 +14,17 @@
  GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
 
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+import { useNotNullLocalStorage } from "./useLocalStorage.js";
 
-@mixin transition($t) {
-  transition: $t 250ms ease-in-out 50ms;
+function getBrowserLang(): string | undefined {
+  if (window.navigator.languages) return window.navigator.languages[0];
+  if (window.navigator.language) return window.navigator.language;
+  return undefined;
 }
 
-@mixin icon-with-update-mark($icon-base-width) {
-  .icon {
-    width: $icon-base-width;
-
-    &.has-update-mark:after {
-      right: ($icon-base-width / 2) - 0.85;
-    }
-  }
+export function useLang(
+  initial?: string,
+): [string, (s: string) => void, boolean] {
+  const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
+  return useNotNullLocalStorage("lang-preference", defaultLang);
 }
diff --git a/packages/anastasis-webui/src/hooks/index.ts 
b/packages/anastasis-webui/src/hooks/useLocalStorage.ts
similarity index 54%
copy from packages/anastasis-webui/src/hooks/index.ts
copy to packages/anastasis-webui/src/hooks/useLocalStorage.ts
index c03e834d7..ed5b491f2 100644
--- a/packages/anastasis-webui/src/hooks/index.ts
+++ b/packages/anastasis-webui/src/hooks/useLocalStorage.ts
@@ -20,65 +20,6 @@
  */
 
 import { StateUpdater, useState } from "preact/hooks";
-export type ValueOrFunction<T> = T | ((p: T) => T);
-
-const calculateRootPath = () => {
-  const rootPath =
-    typeof window !== undefined
-      ? window.location.origin + window.location.pathname
-      : "/";
-  return rootPath;
-};
-
-export function useBackendURL(
-  url?: string,
-): [string, boolean, StateUpdater<string>, () => void] {
-  const [value, setter] = useNotNullLocalStorage(
-    "backend-url",
-    url || calculateRootPath(),
-  );
-  const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
-
-  const checkedSetter = (v: ValueOrFunction<string>) => {
-    setTriedToLog("yes");
-    return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, 
""));
-  };
-
-  const resetBackend = () => {
-    setTriedToLog(undefined);
-  };
-  return [value, !!triedToLog, checkedSetter, resetBackend];
-}
-
-export function useBackendDefaultToken(): [
-  string | undefined,
-  StateUpdater<string | undefined>,
-] {
-  return useLocalStorage("backend-token");
-}
-
-export function useBackendInstanceToken(
-  id: string,
-): [string | undefined, StateUpdater<string | undefined>] {
-  const [token, setToken] = useLocalStorage(`backend-token-${id}`);
-  const [defaultToken, defaultSetToken] = useBackendDefaultToken();
-
-  // instance named 'default' use the default token
-  if (id === "default") {
-    return [defaultToken, defaultSetToken];
-  }
-
-  return [token, setToken];
-}
-
-export function useLang(initial?: string): [string, StateUpdater<string>] {
-  const browserLang =
-    typeof window !== "undefined"
-      ? navigator.language || (navigator as any).userLanguage
-      : undefined;
-  const defaultLang = (browserLang || initial || "en").substring(0, 2);
-  return useNotNullLocalStorage("lang-preference", defaultLang);
-}
 
 export function useLocalStorage(
   key: string,
@@ -94,7 +35,7 @@ export function useLocalStorage(
 
   const setValue = (
     value?: string | ((val?: string) => string | undefined),
-  ) => {
+  ): void => {
     setStoredValue((p) => {
       const toStore = value instanceof Function ? value(p) : value;
       if (typeof window !== "undefined") {
@@ -111,17 +52,18 @@ export function useLocalStorage(
   return [storedValue, setValue];
 }
 
+//TODO: merge with the above function
 export function useNotNullLocalStorage(
   key: string,
   initialValue: string,
-): [string, StateUpdater<string>] {
+): [string, StateUpdater<string>, boolean] {
   const [storedValue, setStoredValue] = useState<string>((): string => {
     return typeof window !== "undefined"
       ? window.localStorage.getItem(key) || initialValue
       : initialValue;
   });
 
-  const setValue = (value: string | ((val: string) => string)) => {
+  const setValue = (value: string | ((val: string) => string)): void => {
     const valueToStore = value instanceof Function ? value(storedValue) : 
value;
     setStoredValue(valueToStore);
     if (typeof window !== "undefined") {
@@ -133,5 +75,6 @@ export function useNotNullLocalStorage(
     }
   };
 
-  return [storedValue, setValue];
+  const isSaved = window.localStorage.getItem(key) !== null;
+  return [storedValue, setValue, isSaved];
 }
diff --git a/packages/anastasis-webui/src/i18n/index.tsx 
b/packages/anastasis-webui/src/i18n/index.tsx
deleted file mode 100644
index 01e3cdd3a..000000000
--- a/packages/anastasis-webui/src/i18n/index.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- This file is part of GNU Anastasis
- (C) 2021-2022 Anastasis SARL
-
- GNU Anastasis is free software; you can redistribute it and/or modify it 
under the
- terms of the GNU Affero General Public License as published by the Free 
Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Anastasis 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 Affero General Public License for more 
details.
-
- You should have received a copy of the GNU Affero General Public License 
along with
- GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
- */
-
-/**
- * Translation helpers for React components and template literals.
- */
-
-/**
- * Imports
- */
-import { ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact";
-
-import { useTranslationContext } from "../context/translation.js";
-
-export function useTranslator() {
-  const ctx = useTranslationContext();
-  const jed = ctx.handler;
-  return function str(
-    stringSeq: TemplateStringsArray,
-    ...values: any[]
-  ): string {
-    const s = toI18nString(stringSeq);
-    if (!s) return s;
-    const tr = jed
-      .translate(s)
-      .ifPlural(1, s)
-      .fetch(...values);
-    return tr;
-  };
-}
-
-/**
- * Convert template strings to a msgid
- */
-function toI18nString(stringSeq: ReadonlyArray<string>): string {
-  let s = "";
-  for (let i = 0; i < stringSeq.length; i++) {
-    s += stringSeq[i];
-    if (i < stringSeq.length - 1) {
-      s += `%${i + 1}$s`;
-    }
-  }
-  return s;
-}
-
-interface TranslateSwitchProps {
-  target: number;
-  children: ComponentChildren;
-}
-
-function stringifyChildren(children: ComponentChildren): string {
-  let n = 1;
-  const ss = (children instanceof Array ? children : [children]).map((c) => {
-    if (typeof c === "string") {
-      return c;
-    }
-    return `%${n++}$s`;
-  });
-  const s = ss.join("").replace(/ +/g, " ").trim();
-  return s;
-}
-
-interface TranslateProps {
-  children: ComponentChildren;
-  /**
-   * Component that the translated element should be wrapped in.
-   * Defaults to "div".
-   */
-  wrap?: any;
-
-  /**
-   * Props to give to the wrapped component.
-   */
-  wrapProps?: any;
-}
-
-function getTranslatedChildren(
-  translation: string,
-  children: ComponentChildren,
-): ComponentChild[] {
-  const tr = translation.split(/%(\d+)\$s/);
-  const childArray = children instanceof Array ? children : [children];
-  // Merge consecutive string children.
-  const placeholderChildren = Array<ComponentChild>();
-  for (let i = 0; i < childArray.length; i++) {
-    const x = childArray[i];
-    if (x === undefined) {
-      continue;
-    } else if (typeof x === "string") {
-      continue;
-    } else {
-      placeholderChildren.push(x);
-    }
-  }
-  const result = Array<ComponentChild>();
-  for (let i = 0; i < tr.length; i++) {
-    if (i % 2 == 0) {
-      // Text
-      result.push(tr[i]);
-    } else {
-      const childIdx = Number.parseInt(tr[i], 10) - 1;
-      result.push(placeholderChildren[childIdx]);
-    }
-  }
-  return result;
-}
-
-/**
- * Translate text node children of this component.
- * If a child component might produce a text node, it must be wrapped
- * in a another non-text element.
- *
- * Example:
- * ```
- * <Translate>
- * Hello.  Your score is <span><PlayerScore player={player} /></span>
- * </Translate>
- * ```
- */
-export function Translate({ children }: TranslateProps): VNode {
-  const s = stringifyChildren(children);
-  const ctx = useTranslationContext();
-  const translation: string = ctx.handler.ngettext(s, s, 1);
-  const result = getTranslatedChildren(translation, children);
-  return <Fragment>{result}</Fragment>;
-}
-
-/**
- * Switch translation based on singular or plural based on the target prop.
- * Should only contain TranslateSingular and TransplatePlural as children.
- *
- * Example:
- * ```
- * <TranslateSwitch target={n}>
- *  <TranslateSingular>I have {n} apple.</TranslateSingular>
- *  <TranslatePlural>I have {n} apples.</TranslatePlural>
- * </TranslateSwitch>
- * ```
- */
-export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
-  let singular: VNode<TranslationPluralProps> | undefined;
-  let plural: VNode<TranslationPluralProps> | undefined;
-  // const children = this.props.children;
-  if (children) {
-    (children instanceof Array ? children : [children]).forEach(
-      (child: any) => {
-        if (child.type === TranslatePlural) {
-          plural = child;
-        }
-        if (child.type === TranslateSingular) {
-          singular = child;
-        }
-      },
-    );
-  }
-  if (!singular || !plural) {
-    console.error("translation not found");
-    return h("span", {}, ["translation not found"]);
-  }
-  singular.props.target = target;
-  plural.props.target = target;
-  // We're looking up the translation based on the
-  // singular, even if we must use the plural form.
-  return singular;
-}
-
-interface TranslationPluralProps {
-  children: ComponentChildren;
-  target: number;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslatePlural({
-  children,
-  target,
-}: TranslationPluralProps): VNode {
-  const s = stringifyChildren(children);
-  const ctx = useTranslationContext();
-  const translation = ctx.handler.ngettext(s, s, 1);
-  const result = getTranslatedChildren(translation, children);
-  return <Fragment>{result}</Fragment>;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslateSingular({
-  children,
-  target,
-}: TranslationPluralProps): VNode {
-  const s = stringifyChildren(children);
-  const ctx = useTranslationContext();
-  const translation = ctx.handler.ngettext(s, s, target);
-  const result = getTranslatedChildren(translation, children);
-  return <Fragment>{result}</Fragment>;
-}
diff --git a/packages/anastasis-webui/html/ui.html 
b/packages/anastasis-webui/src/index.html
similarity index 67%
rename from packages/anastasis-webui/html/ui.html
rename to packages/anastasis-webui/src/index.html
index 6672eba6a..90a795ae3 100644
--- a/packages/anastasis-webui/html/ui.html
+++ b/packages/anastasis-webui/src/index.html
@@ -1,28 +1,42 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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
+-->
 <!DOCTYPE html>
 <html
   lang="en"
   class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top 
has-aside-expanded"
 >
   <head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width,initial-scale=1" />
     <meta name="mobile-web-app-capable" content="yes" />
     <meta name="apple-mobile-web-app-capable" content="yes" />
-
     <link
       rel="icon"
       
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/
 [...]
     />
     <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
-    <style id="style-id">
-      ANASTASIS_STYLE_CONTENT
-    </style>
+    <title>Anastasis</title>
+    <!-- Entry point for the demobank SPA. -->
+    <script type="module" src="index.js"></script>
+    <link rel="stylesheet" href="index.css" />
   </head>
-
   <body>
-    <div id="container" class="anastasis-container"></div>
-    <script>
-      ANASTASIS_SCRIPT_CONTENT;
-    </script>
+    <div id="container"></div>
   </body>
 </html>
diff --git a/packages/anastasis-webui/src/main.test.ts 
b/packages/anastasis-webui/src/index.test.ts
similarity index 100%
rename from packages/anastasis-webui/src/main.test.ts
rename to packages/anastasis-webui/src/index.test.ts
diff --git a/packages/anastasis-webui/src/index.ts 
b/packages/anastasis-webui/src/index.ts
index e04c44a31..d7b2164ab 100644
--- a/packages/anastasis-webui/src/index.ts
+++ b/packages/anastasis-webui/src/index.ts
@@ -13,7 +13,30 @@
  You should have received a copy of the GNU Affero General Public License 
along with
  GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
+import { setupI18n } from "@gnu-taler/taler-util";
+import { h, render } from "preact";
 import App from "./components/app.js";
 import "./scss/main.scss";
 
-export default App;
+function main(): void {
+  try {
+    const container = document.getElementById("container");
+    if (!container) {
+      throw Error("container not found, can't mount page contents");
+    }
+    render(h(App, {}), container);
+  } catch (e) {
+    console.error("got error", e);
+    if (e instanceof Error) {
+      document.body.innerText = `Fatal error: "${e.message}".  Please report 
this bug at https://bugs.gnunet.org/.`;
+    }
+  }
+}
+
+// setupI18n("en", strings);
+
+if (document.readyState === "loading") {
+  document.addEventListener("DOMContentLoaded", main);
+} else {
+  main();
+}
diff --git a/packages/anastasis-webui/src/main.ts 
b/packages/anastasis-webui/src/main.ts
deleted file mode 100644
index 72ab257eb..000000000
--- a/packages/anastasis-webui/src/main.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- This file is part of GNU Anastasis
- (C) 2021-2022 Anastasis SARL
-
- GNU Anastasis is free software; you can redistribute it and/or modify it 
under the
- terms of the GNU Affero General Public License as published by the Free 
Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Anastasis 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 Affero General Public License for more 
details.
-
- You should have received a copy of the GNU Affero General Public License 
along with
- GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
- */
-import { setupI18n } from "@gnu-taler/taler-util";
-import { h, render } from "preact";
-import App from "./components/app.js";
-
-function main(): void {
-  try {
-    const container = document.getElementById("container");
-    if (!container) {
-      throw Error("container not found, can't mount page contents");
-    }
-    render(h(App, {}), container);
-  } catch (e) {
-    console.error("got error", e);
-    if (e instanceof Error) {
-      document.body.innerText = `Fatal error: "${e.message}".  Please report 
this bug at https://bugs.gnunet.org/.`;
-    }
-  }
-}
-
-// setupI18n("en", strings);
-
-if (document.readyState === "loading") {
-  document.addEventListener("DOMContentLoaded", main);
-} else {
-  main();
-}
diff --git 
a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx 
b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
index dc41d9c1a..268189ed8 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
@@ -24,6 +24,7 @@ import { createExampleWithoutAnastasis } from 
"../../../utils/index.jsx";
 import { WithoutProviderType, WithProviderType } from "./views.jsx";
 
 export default {
+  title: "Adding Provider Screen",
   args: {
     order: 1,
   },
diff --git 
a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx 
b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
index e397e0b65..19557a12f 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
@@ -23,8 +23,10 @@ import { TextInput } from 
"../../../components/fields/TextInput.js";
 import { Notifications } from "../../../components/Notifications.js";
 import { AnastasisClientFrame } from "../index.js";
 import { testProvider, WithoutType, WithType } from "./index.js";
+import { useTranslationContext } from "../../../context/translation.js";
 
 export function WithProviderType(props: WithType): VNode {
+  const { i18n } = useTranslationContext();
   return (
     <AnastasisClientFrame
       hideNav
@@ -33,7 +35,7 @@ export function WithProviderType(props: WithType): VNode {
     >
       <div>
         <Notifications notifications={props.notifications} />
-        <p>Add a provider url for a {props.providerLabel} service</p>
+        <p>{i18n.str`Add a provider url for a ${props.providerLabel} 
service`}</p>
         <div class="container">
           <TextInput
             label="Provider URL"
diff --git 
a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
index b1569f184..38fc1b56b 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { AttributeEntryScreen as TestedComponent } from 
"./AttributeEntryScreen.js";
 
 export default {
+  title: "Attribute Entry Screen",
   component: TestedComponent,
   args: {
     order: 3,
diff --git 
a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
index c4901085d..ba48e2d5c 100644
--- 
a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { AuthenticationEditorScreen as TestedComponent } from 
"./AuthenticationEditorScreen.js";
 
 export default {
+  title: "Authentication Editor Screen",
   component: TestedComponent,
   args: {
     order: 4,
diff --git 
a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
index f50a72f8a..8aeaec25c 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { BackupFinishedScreen as TestedComponent } from 
"./BackupFinishedScreen.js";
 
 export default {
+  title: "Backup finish",
   component: TestedComponent,
   args: {
     order: 8,
diff --git 
a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
index 552cb069f..d2471755a 100644
--- 
a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
@@ -28,6 +28,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { ChallengeOverviewScreen as TestedComponent } from 
"./ChallengeOverviewScreen.js";
 
 export default {
+  title: "Challenge overview",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
index 0d4895a0b..cd41fe03a 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { ChallengePayingScreen as TestedComponent } from 
"./ChallengePayingScreen.js";
 
 export default {
+  title: "Challenge paying",
   component: TestedComponent,
   args: {
     order: 10,
diff --git 
a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
index 3994b7377..12a79c56c 100644
--- 
a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { ContinentSelectionScreen as TestedComponent } from 
"./ContinentSelectionScreen.js";
 
 export default {
+  title: "Continent selection",
   component: TestedComponent,
   args: {
     order: 2,
diff --git 
a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
index 75619ba05..1e3650300 100644
--- a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { EditPoliciesScreen as TestedComponent } from 
"./EditPoliciesScreen.js";
 
 export default {
+  title: "Edit policies",
   args: {
     order: 6,
   },
diff --git 
a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
index 54833234d..56c224d34 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { PoliciesPayingScreen as TestedComponent } from 
"./PoliciesPayingScreen.js";
 
 export default {
+  title: "Policies paying",
   component: TestedComponent,
   args: {
     order: 9,
diff --git 
a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
index eda8968b2..1eb2ae50c 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
@@ -25,6 +25,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { RecoveryFinishedScreen as TestedComponent } from 
"./RecoveryFinishedScreen.js";
 
 export default {
+  title: "Recovery Finished",
   args: {
     order: 7,
   },
diff --git 
a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
index 036455bce..c5003d6a0 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { ReviewPoliciesScreen as TestedComponent } from 
"./ReviewPoliciesScreen.js";
 
 export default {
+  title: "Reviewing Policies",
   args: {
     order: 6,
   },
diff --git 
a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
index 7a03116e7..dbf8bf128 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { SecretEditorScreen as TestedComponent } from 
"./SecretEditorScreen.js";
 
 export default {
+  title: "Secret editor",
   component: TestedComponent,
   args: {
     order: 7,
diff --git 
a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
index b457937f8..7669668ee 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
@@ -27,6 +27,7 @@ import {
 } from "./SecretSelectionScreen.js";
 
 export default {
+  title: "Secret selection",
   component: SecretSelectionScreen,
   args: {
     order: 4,
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
index 5b3a70dd0..1058ae126 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { SolveScreen as TestedComponent } from "./SolveScreen.js";
 
 export default {
+  title: "Solve Screen",
   component: TestedComponent,
   args: {
     order: 6,
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
index 3d54a9fd6..960426098 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { StartScreen as TestedComponent } from "./StartScreen.js";
 
 export default {
+  title: "Start screen",
   component: TestedComponent,
   args: {
     order: 1,
diff --git 
a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx 
b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
index 81bab4868..40ed5117c 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../utils/index.js";
 import { TruthsPayingScreen as TestedComponent } from 
"./TruthsPayingScreen.js";
 
 export default {
+  title: "Truths Paying",
   component: TestedComponent,
   args: {
     order: 10,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
index 38391d10d..4a2d76ca3 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Email setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
index db9abc86c..cc378d8f6 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
@@ -27,6 +27,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Email solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
index d4e034a37..6a9595a83 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
@@ -23,7 +23,7 @@ import { useState } from "preact/hooks";
 import { AsyncButton } from "../../../components/AsyncButton.js";
 import { TextInput } from "../../../components/fields/TextInput.js";
 import { useAnastasisContext } from "../../../context/anastasis.js";
-import { useTranslator } from "../../../i18n/index.js";
+import { useTranslationContext } from "../../../context/translation.js";
 import { AnastasisClientFrame } from "../index.js";
 import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
 import { shouldHideConfirm } from "./helpers.js";
@@ -53,7 +53,7 @@ export function AuthMethodEmailSolve({ id }: 
AuthMethodSolveProps): VNode {
     _setAnswer(result);
   }
   const [expanded, setExpanded] = useState(false);
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   const reducer = useAnastasisContext();
   if (!reducer) {
@@ -124,7 +124,7 @@ export function AuthMethodEmailSolve({ id }: 
AuthMethodSolveProps): VNode {
 
   const error =
     answer.length > 21
-      ? i18n`The answer should not be greater than 21 characters.`
+      ? i18n.str`The answer should not be greater than 21 characters.`
       : undefined;
 
   return (
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
index 5f3de47ff..dfe3850f1 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: IBAN setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
index c06611127..8a9a3f7a0 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: IBAN Solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
index 892de6023..8a32c45c1 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Post setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
index 8f7dc5ff9..702ba2810 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Post solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
index 725382c58..8204ab1cf 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
@@ -19,7 +19,7 @@ import { useState } from "preact/hooks";
 import { AsyncButton } from "../../../components/AsyncButton.js";
 import { TextInput } from "../../../components/fields/TextInput.js";
 import { useAnastasisContext } from "../../../context/anastasis.js";
-import { useTranslator } from "../../../i18n/index.js";
+import { useTranslationContext } from "../../../context/translation.js";
 import { AnastasisClientFrame } from "../index.js";
 import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
 import { shouldHideConfirm } from "./helpers.js";
@@ -48,7 +48,7 @@ export function AuthMethodPostSolve({ id }: 
AuthMethodSolveProps): VNode {
 
     _setAnswer(result);
   }
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   const reducer = useAnastasisContext();
   if (!reducer) {
@@ -119,7 +119,7 @@ export function AuthMethodPostSolve({ id }: 
AuthMethodSolveProps): VNode {
 
   const error =
     answer.length > 21
-      ? i18n`The answer should not be greater than 21 characters.`
+      ? i18n.str`The answer should not be greater than 21 characters.`
       : undefined;
 
   return (
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
index 736e7bfa8..2e108b4e6 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Question setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
index 182538775..f7116bf6f 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
@@ -27,6 +27,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Question solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
index 0d58dbdcf..b2c6cb61d 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: SMS setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
index f1717eff0..2064f12ff 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: SMS solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
index 965efbe60..58bb53c4f 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
@@ -19,7 +19,7 @@ import { useState } from "preact/hooks";
 import { AsyncButton } from "../../../components/AsyncButton.js";
 import { TextInput } from "../../../components/fields/TextInput.js";
 import { useAnastasisContext } from "../../../context/anastasis.js";
-import { useTranslator } from "../../../i18n/index.js";
+import { useTranslationContext } from "../../../context/translation.js";
 import { AnastasisClientFrame } from "../index.js";
 import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
 import { shouldHideConfirm } from "./helpers.js";
@@ -48,7 +48,7 @@ export function AuthMethodSmsSolve({ id }: 
AuthMethodSolveProps): VNode {
 
     _setAnswer(result);
   }
-  const i18n = useTranslator();
+  const { i18n } = useTranslationContext();
 
   const [expanded, setExpanded] = useState(false);
   const reducer = useAnastasisContext();
@@ -120,7 +120,7 @@ export function AuthMethodSmsSolve({ id }: 
AuthMethodSolveProps): VNode {
 
   const error =
     answer.length > 21
-      ? i18n`The answer should not be greater than 21 characters.`
+      ? i18n.str`The answer should not be greater than 21 characters.`
       : undefined;
 
   return (
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
index e22053b96..5582590f7 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
@@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Totp setup",
   component: TestedComponent,
   args: {
     order: 5,
diff --git 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
index 354516d80..20cd7e3c9 100644
--- 
a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
+++ 
b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
@@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from 
"../../../utils/index.js";
 import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
 
 export default {
+  title: "Auth method: Totp solve",
   component: TestedComponent,
   args: {
     order: 5,
diff --git a/packages/anastasis-webui/src/scss/_mixins.scss 
b/packages/anastasis-webui/src/scss/_mixins.scss
index 64315785b..a0fe6e93e 100644
--- a/packages/anastasis-webui/src/scss/_mixins.scss
+++ b/packages/anastasis-webui/src/scss/_mixins.scss
@@ -28,7 +28,7 @@
     width: $icon-base-width;
 
     &.has-update-mark:after {
-      right: ($icon-base-width / 2) - 0.85;
+      right: calc($icon-base-width / 2) - 0.85;
     }
   }
 }
diff --git a/packages/anastasis-webui/src/stories.tsx 
b/packages/anastasis-webui/src/stories.tsx
index 7d22deece..f345f082d 100644
--- a/packages/anastasis-webui/src/stories.tsx
+++ b/packages/anastasis-webui/src/stories.tsx
@@ -18,302 +18,24 @@
  *
  * @author Sebastian Javier Marchano (sebasjm)
  */
-import { setupI18n } from "@gnu-taler/taler-util";
-import { ComponentChild, Fragment, h, render, VNode } from "preact";
-import { useEffect, useErrorBoundary, useState } from "preact/hooks";
 import { strings } from "./i18n/strings.js";
-import * as pages from "./pages/home/index.storiesNo.js";
 
-const url = new URL(window.location.href);
-const lang = url.searchParams.get("lang") || "en";
+import * as pages from "./pages/home/index.storiesNo.js";
 
-setupI18n(lang, strings);
+import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
 
-const Page = ({ children }: any) => <div class="page">{children}</div>;
-const SideBar = ({ children }: any) => <div class="sidebar">{children}</div>;
-const Content = ({ children }: any) => <div class="content">{children}</div>;
-
-function parseExampleImport(
-  group: string,
-  im: any,
-  name?: string,
-): ComponentItem {
-  const component = name || im.default.title;
-  const order: number = im.default.args?.order || 0;
-  return {
-    name: component,
-    order,
-    examples: Object.entries(im)
-      .filter(([k]) => k !== "default")
-      .map(
-        ([name, render]) =>
-          ({
-            group,
-            component,
-            name,
-            render,
-          } as ExampleItem),
-      ),
-  };
-}
+import "./scss/main.scss";
 
 function SortStories(a: any, b: any): number {
   return (a?.order ?? 0) - (b?.order ?? 0);
 }
 
-const allExamples = Object.entries({ pages }).map(([title, value]) => {
-  return {
-    title,
-    list: Object.entries(value)
-      .filter(([name]) => name != "default")
-      .map(([name, value]) => parseExampleImport(title, value, name))
-      .sort(SortStories),
-  };
-});
-
-interface ComponentItem {
-  name: string;
-  order: number;
-  examples: ExampleItem[];
-}
-
-interface ExampleItem {
-  group: string;
-  component: string;
-  name: string;
-  render: {
-    (args: any): VNode;
-    args: any;
-  };
-}
-
-function findByGroupComponentName(
-  group: string,
-  component: string,
-  name: string,
-): ExampleItem | undefined {
-  const gl = allExamples.filter((e) => e.title === group);
-  if (gl.length === 0) {
-    return undefined;
-  }
-  const cl = gl[0].list.filter((l) => l.name === component);
-  if (cl.length === 0) {
-    return undefined;
-  }
-  const el = cl[0].examples.filter((c) => c.name === name);
-  if (el.length === 0) {
-    return undefined;
-  }
-  return el[0];
-}
-
-function getContentForExample(item: ExampleItem | undefined): () => VNode {
-  if (!item)
-    return function SelectExampleMessage() {
-      return <div>select example from the list on the left</div>;
-    };
-  const example = findByGroupComponentName(
-    item.group,
-    item.component,
-    item.name,
-  );
-  if (!example)
-    return function ExampleNotFoundMessage() {
-      return <div>example not found</div>;
-    };
-  return () => example.render(example.render.args);
-}
-
-function ExampleList({
-  name,
-  list,
-  selected,
-  onSelectStory,
-}: {
-  name: string;
-  list: {
-    name: string;
-    examples: ExampleItem[];
-  }[];
-  selected: ExampleItem | undefined;
-  onSelectStory: (i: ExampleItem, id: string) => void;
-}): VNode {
-  const [isOpen, setOpen] = useState(selected && selected.group === name);
-  return (
-    <ol>
-      <div onClick={() => setOpen(!isOpen)}>{name}</div>
-      <div data-hide={!isOpen}>
-        {list.map((k) => (
-          <li key={k.name}>
-            <dl>
-              <dt>{k.name}</dt>
-              {k.examples.map((r) => {
-                const e = encodeURIComponent;
-                const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
-                function doSelection(e: any): void {
-                  e.preventDefault();
-                  location.hash = `#${eId}`;
-                  onSelectStory(r, eId);
-                }
-                const isSelected =
-                  selected &&
-                  selected.component === r.component &&
-                  selected.group === r.group &&
-                  selected.name === r.name;
-                return (
-                  <dd
-                    id={eId}
-                    key={r.name}
-                    data-selected={isSelected}
-                    onClick={doSelection}
-                  >
-                    <a href={`#${eId}`} onClick={doSelection}>
-                      {r.name}
-                    </a>
-                  </dd>
-                );
-              })}
-            </dl>
-          </li>
-        ))}
-      </div>
-    </ol>
-  );
-}
-
-// function getWrapperForGroup(group: string): FunctionComponent {
-//   switch (group) {
-//     case "popup":
-//       return function PopupWrapper({ children }: any) {
-//         return (
-//           <Fragment>
-//             <PopupNavBar />
-//             <PopupBox>{children}</PopupBox>
-//           </Fragment>
-//         );
-//       };
-//     case "wallet":
-//       return function WalletWrapper({ children }: any) {
-//         return (
-//           <Fragment>
-//             <LogoHeader />
-//             <WalletNavBar />
-//             <WalletBox>{children}</WalletBox>
-//           </Fragment>
-//         );
-//       };
-//     case "cta":
-//       return function WalletWrapper({ children }: any) {
-//         return (
-//           <Fragment>
-//             <WalletBox>{children}</WalletBox>
-//           </Fragment>
-//         );
-//       };
-//     default:
-//       return Fragment;
-//   }
-// }
-
-function ErrorReport({
-  children,
-  selected,
-}: {
-  children: ComponentChild;
-  selected: ExampleItem | undefined;
-}): VNode {
-  const [error] = useErrorBoundary();
-  if (error) {
-    return (
-      <div class="error_report">
-        <p>Error was thrown trying to render</p>
-        {selected && (
-          <ul>
-            <li>
-              <b>group</b>: {selected.group}
-            </li>
-            <li>
-              <b>component</b>: {selected.component}
-            </li>
-            <li>
-              <b>example</b>: {selected.name}
-            </li>
-            <li>
-              <b>args</b>:{" "}
-              <pre>{JSON.stringify(selected.render.args, undefined, 2)}</pre>
-            </li>
-          </ul>
-        )}
-        <p>{error.message}</p>
-        <pre>{error.stack}</pre>
-      </div>
-    );
-  }
-  return <Fragment>{children}</Fragment>;
-}
-
-function getSelectionFromLocationHash(hash: string): ExampleItem | undefined {
-  if (!hash) return undefined;
-  const parts = hash.substring(1).split("-");
-  if (parts.length < 3) return undefined;
-  return findByGroupComponentName(
-    decodeURIComponent(parts[0]),
-    decodeURIComponent(parts[1]),
-    decodeURIComponent(parts[2]),
-  );
-}
-
-function Application(): VNode {
-  const initialSelection = getSelectionFromLocationHash(location.hash);
-  const [selected, updateSelected] = useState<ExampleItem | undefined>(
-    initialSelection,
-  );
-  useEffect(() => {
-    if (location.hash) {
-      const hash = location.hash.substring(1);
-      const found = document.getElementById(hash);
-      if (found) {
-        setTimeout(() => {
-          found.scrollIntoView({
-            block: "center",
-          });
-        }, 10);
-      }
-    }
-  }, []);
-
-  const ExampleContent = getContentForExample(selected);
-
-  // const GroupWrapper = getWrapperForGroup(selected?.group || "default");
-
-  return (
-    <Page>
-      <LiveReload />
-      <SideBar>
-        {allExamples.map((e) => (
-          <ExampleList
-            key={e.title}
-            name={e.title}
-            list={e.list}
-            selected={selected}
-            onSelectStory={(item, htmlId) => {
-              document.getElementById(htmlId)?.scrollIntoView({
-                block: "center",
-              });
-              updateSelected(item);
-            }}
-          />
-        ))}
-        <hr />
-      </SideBar>
-      <Content>
-        <ErrorReport selected={selected}>
-          {/* <GroupWrapper> */}
-          <ExampleContent />
-          {/* </GroupWrapper> */}
-        </ErrorReport>
-      </Content>
-    </Page>
+function main(): void {
+  renderStories(
+    { pages },
+    {
+      strings,
+    },
   );
 }
 
@@ -322,72 +44,3 @@ if (document.readyState === "loading") {
 } else {
   main();
 }
-function main(): void {
-  try {
-    const container = document.getElementById("container");
-    if (!container) {
-      throw Error("container not found, can't mount page contents");
-    }
-    render(<Application />, container);
-  } catch (e) {
-    console.error("got error", e);
-    if (e instanceof Error) {
-      document.body.innerText = `Fatal error: "${e.message}".  Please report 
this bug at https://bugs.gnunet.org/.`;
-    }
-  }
-}
-
-let liveReloadMounted = false;
-function LiveReload({ port = 8002 }: { port?: number }): VNode {
-  const [isReloading, setIsReloading] = useState(false);
-  useEffect(() => {
-    if (!liveReloadMounted) {
-      setupLiveReload(port, () => {
-        setIsReloading(true);
-        window.location.reload();
-      });
-      liveReloadMounted = true;
-    }
-  });
-
-  if (isReloading) {
-    return (
-      <div
-        style={{
-          position: "absolute",
-          width: "100%",
-          height: "100%",
-          backgroundColor: "rgba(0,0,0,0.5)",
-          color: "white",
-          display: "flex",
-          justifyContent: "center",
-        }}
-      >
-        <h1 style={{ margin: "auto" }}>reloading...</h1>
-      </div>
-    );
-  }
-  return <Fragment />;
-}
-
-function setupLiveReload(port: number, onReload: () => void): void {
-  const socketPath = `ws://localhost:8003/socket`;
-  // const socketPath = `${protocol}//${host}:${port}/socket`;
-
-  const ws = new WebSocket(socketPath);
-  ws.onmessage = (message) => {
-    const event = JSON.parse(message.data);
-    if (event.type === "LOG") {
-      console.log(event.message);
-    }
-    if (event.type === "RELOAD") {
-      onReload();
-    }
-  };
-  ws.onerror = (error) => {
-    console.error(error);
-  };
-  ws.onclose = (e) => {
-    console.log("disconnected", e);
-  };
-}
diff --git a/packages/anastasis-webui/src/test-utils.ts 
b/packages/anastasis-webui/src/test-utils.ts
index 1fcc753ee..f220540f1 100644
--- a/packages/anastasis-webui/src/test-utils.ts
+++ b/packages/anastasis-webui/src/test-utils.ts
@@ -41,8 +41,10 @@ export function createExample<Props>(
   // check how we can build evaluatedProps in render time
   const evaluatedProps = typeof props === "function" ? props() : props;
   const Render = (args: any): VNode => create(Component, args);
-  Render.args = evaluatedProps;
-  return Render;
+  return {
+    component: Render,
+    props: evaluatedProps
+  };
 }
 
 export function createExampleWithCustomContext<Props, ContextProps>(
@@ -58,8 +60,10 @@ export function createExampleWithCustomContext<Props, 
ContextProps>(
       ...contextProps,
       children: [Render(args)],
     } as any);
-  WithContext.args = evaluatedProps;
-  return WithContext;
+  return {
+    component: WithContext,
+    props: evaluatedProps
+  };
 }
 
 export function NullLink({
diff --git a/packages/anastasis-webui/src/utils/index.tsx 
b/packages/anastasis-webui/src/utils/index.tsx
index 78973e38f..4cf839473 100644
--- a/packages/anastasis-webui/src/utils/index.tsx
+++ b/packages/anastasis-webui/src/utils/index.tsx
@@ -37,16 +37,18 @@ export function createExampleWithoutAnastasis<Props>(
   // check how we can build evaluatedProps in render time
   const evaluatedProps = typeof props === "function" ? props() : props;
   const Render = (args: any): VNode => h(Component, args);
-  Render.args = evaluatedProps;
-  return Render;
+  return {
+    component: Render,
+    props: evaluatedProps,
+  };
 }
 
 export function createExample<Props>(
   Component: FunctionalComponent<Props>,
   currentReducerState?: ReducerState,
   props?: Partial<Props>,
-): { (args: Props): VNode } {
-  const r = (args: Props): VNode => {
+): ComponentChildren {
+  const Render = (args: Props): VNode => {
     return (
       <AnastasisProvider
         value={{
@@ -74,8 +76,10 @@ export function createExample<Props>(
       </AnastasisProvider>
     );
   };
-  r.args = props;
-  return r;
+  return {
+    component: Render,
+    props: props,
+  };
 }
 
 const base = {
diff --git a/packages/anastasis-webui/watch/reply.sh 
b/packages/anastasis-webui/watch/reply.sh
deleted file mode 100755
index 20cbff37e..000000000
--- a/packages/anastasis-webui/watch/reply.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash
-SERVER_KEY=258EAFA5-E914-47DA-95CA-C5AB0DC85B11
-
-while read line; do
-  LINE=$(echo $line | tr -d '\r')
-  case $LINE in 
-    Sec-WebSocket-Key:*)
-      CLIENT_KEY="${LINE:19}"
-      export WS_ACCEPT=$( echo -n $CLIENT_KEY$SERVER_KEY | sha1sum | xxd -r -p 
| base64 )
-      ;;
-     "") break ;;
-  esac
-done
-
-cat watch/web_socket_server.reply | sed 's/$'"/`echo \\\r`/" | envsubst 
'$WS_ACCEPT'
-
-tail -n 0 -F /tmp/send_signal 2> /dev/null
-
diff --git a/packages/anastasis-webui/watch/send.sh 
b/packages/anastasis-webui/watch/send.sh
deleted file mode 100755
index 184cd2491..000000000
--- a/packages/anastasis-webui/watch/send.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-
-#https://datatracker.ietf.org/doc/html/rfc6455#page-65
-
-COMMAND=$1
-LEN=$(printf '%x\n' ${#COMMAND})
-
-#text command
-OPCODE=81
-
-cat <(echo -n $OPCODE$LEN | xxd -r -p) <(echo -n $COMMAND) >> /tmp/send_signal
-
diff --git a/packages/anastasis-webui/watch/send2.sh 
b/packages/anastasis-webui/watch/send2.sh
deleted file mode 100755
index 6a2881c19..000000000
--- a/packages/anastasis-webui/watch/send2.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-
-#https://datatracker.ietf.org/doc/html/rfc6455#page-65
-
-CONTENT=$( cat $1 | base64 -w 0 )
-COMMAND='{"type":"UPDATE","'$CONTENT'"}'
-LEN=$(printf '%0*x\n' 4 ${#COMMAND})
-echo $LEN
-LEN=00000138
-#text command
-OPCODE=81
-
-cat <(echo -n $OPCODE$LEN | xxd -r -p) <(echo -n $COMMAND) >> /tmp/send_signal
-
diff --git a/packages/anastasis-webui/watch/serve.sh 
b/packages/anastasis-webui/watch/serve.sh
deleted file mode 100755
index f4e9595d5..000000000
--- a/packages/anastasis-webui/watch/serve.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-#clean up
-rm /tmp/send_signal
-
-socat TCP-LISTEN:8003,fork,reuseaddr,keepalive EXEC:"./watch/reply.sh"
-
diff --git a/packages/anastasis-webui/watch/web_socket_client.request 
b/packages/anastasis-webui/watch/web_socket_client.request
deleted file mode 100644
index e7077b0cb..000000000
--- a/packages/anastasis-webui/watch/web_socket_client.request
+++ /dev/null
@@ -1,6 +0,0 @@
-GET /socket HTTP/1.1
-Connection: Upgrade
-Upgrade: websocket
-Sec-WebSocket-Version: 13
-Sec-WebSocket-Key: aaaaaaaaaaaaaaaaaaaaaa==
-
diff --git a/packages/anastasis-webui/watch/web_socket_server.reply 
b/packages/anastasis-webui/watch/web_socket_server.reply
deleted file mode 100644
index b4e0db001..000000000
--- a/packages/anastasis-webui/watch/web_socket_server.reply
+++ /dev/null
@@ -1,5 +0,0 @@
-HTTP/1.1 101 Switching Protocols
-Upgrade: websocket
-Connection: Upgrade
-Sec-WebSocket-Accept: $WS_ACCEPT
-

-- 
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]