Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ OPENAI_API_KEY=sk-your-openai-api-key
PUBLIC_EVOTING_BASE_URL="http://localhost:7777"
PUBLIC_EVOTING_URL="http://localhost:3001"

# Translation corrections served to the wallet at runtime, so wording fixes
# ship without an app store release. Leave empty to use only the strings
# compiled into the build.
PUBLIC_TRANSLATIONS_URL=""

PUBLIC_APP_STORE_EID_WALLET=""
PUBLIC_PLAY_STORE_EID_WALLET=""
NOTIFICATION_SHARED_SECRET=your-notification-secret-key
Expand Down
1,613 changes: 1,613 additions & 0 deletions docs/static/translations.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion infrastructure/eid-wallet/biome.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["../../biome.json"],
"files": {
"ignore": ["src/lib/paraglide/**"]
},
"organizeImports": {
"include": ["src/**/*.ts", "src/**/*.svelte"]
"include": ["src/**/*.ts", "src/**/*.svelte"],
"ignore": ["src/lib/paraglide/**"]
}
}
689 changes: 689 additions & 0 deletions infrastructure/eid-wallet/messages/en.json

Large diffs are not rendered by default.

709 changes: 709 additions & 0 deletions infrastructure/eid-wallet/messages/ru.json

Large diffs are not rendered by default.

709 changes: 709 additions & 0 deletions infrastructure/eid-wallet/messages/uk.json

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions infrastructure/eid-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && npx @biomejs/biome check ./src",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"check": "npm run paraglide:compile && node scripts/build-translations-catalog.mjs --check && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && npx @biomejs/biome check ./src",
"check:watch": "npm run paraglide:compile && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "npx @biomejs/biome format --write ./src",
"check-format": "npx @biomejs/biome format ./src",
"lint": "npx @biomejs/biome lint --write ./src",
"check-lint": "npx @biomejs/biome lint ./src",
"tauri": "tauri",
"test": "vitest run",
"storybook": "svelte-kit sync && storybook dev -p 6006",
"build-storybook": "storybook build",
"test": "npm run paraglide:compile && vitest run",
"storybook": "npm run paraglide:compile && svelte-kit sync && storybook dev -p 6006",
"build-storybook": "npm run paraglide:compile && storybook build",
"build:apk": "npm run tauri android build -- --apk --target aarch64 --target armv7",
"build:aab": "npm run tauri android build -- --aab --target aarch64 --target armv7"
"build:aab": "npm run tauri android build -- --aab --target aarch64 --target armv7",
"translations:build": "node scripts/build-translations-catalog.mjs",
"paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide --strategy localStorage preferredLanguage baseLocale"
},
"license": "MIT",
"dependencies": {
Expand All @@ -29,6 +31,7 @@
"@fontsource-variable/roboto-condensed": "^5.2.8",
"@hugeicons/core-free-icons": "^1.0.13",
"@hugeicons/svelte": "^1.0.2",
"@inlang/paraglide-js": "^2.15.0",
"@metastate-foundation/platform-icons": "workspace:*",
"@tailwindcss/container-queries": "^0.1.1",
"@tauri-apps/api": "^2.11.0",
Expand Down
12 changes: 12 additions & 0 deletions infrastructure/eid-wallet/project.inlang/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
},
"baseLocale": "en",
"locales": ["en", "ru", "uk"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env node
/**
* Generates the catalog served at PUBLIC_TRANSLATIONS_URL from the message
* files. Keys the app refuses are left out, or every launch logs rejections.
*
* node scripts/build-translations-catalog.mjs write the file
* node scripts/build-translations-catalog.mjs --check fail if it is stale
*/
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const walletRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolve(walletRoot, "../..");
const OUTPUT = resolve(repoRoot, "docs/static/translations.json");

const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));

const policy = readJson(resolve(walletRoot, "src/lib/i18n/policy.json"));
const { locales } = readJson(
resolve(walletRoot, "project.inlang/settings.json"),
);

function build() {
const base = readJson(resolve(walletRoot, "messages/en.json"));
let plural = 0;
const messages = {};

for (const locale of locales) {
const translations = readJson(
resolve(walletRoot, `messages/${locale}.json`),
);
const entries = {};

// Driven by the English file so ordering is stable across locales.
for (const [key, value] of Object.entries(base)) {
if (key === "$schema") continue;
if (Array.isArray(value)) {
plural++;
continue;
}
// Absent here: the compiled message already falls back to English.
if (typeof translations[key] === "string") {
entries[key] = translations[key];
}
}
messages[locale] = entries;
}

const perLocale = locales.length;
return {
json: `${JSON.stringify({ version: policy.formatVersion, messages }, null, 2)}\n`,
counts: {
published: Object.values(messages).map(
(m) => Object.keys(m).length,
),
plural: plural / perLocale,
},
};
}

const { json, counts } = build();
const where = relative(repoRoot, OUTPUT);

if (process.argv.includes("--check")) {
let current = null;
try {
current = readFileSync(OUTPUT, "utf8");
} catch {
}
if (current !== json) {
console.error(
`${where} is out of date. Run \`pnpm translations:build\` and commit the result.`,
);
process.exit(1);
}
console.log(`${where} is up to date.`);
} else {
writeFileSync(OUTPUT, json);
console.log(
`${where}: ${counts.published.join("/")} keys for ${locales.join("/")}` +
` (${counts.plural} plural keys excluded)`,
);
}
1 change: 1 addition & 0 deletions infrastructure/eid-wallet/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare module "$env/static/public" {
export const PUBLIC_PROVISIONER_SHARED_SECRET: string;
export const PUBLIC_PICTIQUE_BASE_URL: string;
export const PUBLIC_BLABSY_BASE_URL: string;
export const PUBLIC_TRANSLATIONS_URL: string;
}

/** App version from package.json, injected by vite.config.js at build time. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { m } from "$lib/i18n";
import * as Button from "$lib/ui/Button";
import { cn } from "$lib/utils";
import { cn, identityFieldLabel, identityFieldValue } from "$lib/utils";
import {
CheckmarkBadge02Icon,
Copy01Icon,
Expand Down Expand Up @@ -87,7 +88,7 @@ const baseClasses = $derived(
class="bg-white text-black flex items-center leading-0 justify-center rounded-full h-7 px-5 text-xs font-medium"
>
{#if userData}
{userData.isFake ? "DEMO ID" : "VERIFIED ID"}
{userData.isFake ? m.identity_demo_id() : m.identity_verified_id()}
{/if}
</p>
{#if viewBtn}
Expand All @@ -100,13 +101,15 @@ const baseClasses = $derived(
{/if}
{:else if variant === "eVault"}
<h3 class="text-black-300 text-3xl font-semibold mb-1 z-[1]">
{state.progressWidth} Used
{m.evault_storage_percent_used({
percent: state.progressWidth,
})}
</h3>
{/if}
</div>
<div>
{#if variant === "eName"}
<p class="text-gray font-normal">Your eName</p>
<p class="text-gray font-normal">{m.identity_your_ename()}</p>
<div class="flex items-center justify-between w-full">
<p class="text-white w-full font-medium">{userId}</p>
</div>
Expand All @@ -115,17 +118,25 @@ const baseClasses = $derived(
{#if userData}
{#each Object.entries(userData).filter(([f, v]) => f !== "isFake") as [fieldName, value]}
<div class="flex justify-between">
<p class="text-gray capitalize">{fieldName}</p>
<p class=" font-medium text-white">{value}</p>
<p class="text-gray capitalize">
{identityFieldLabel(fieldName)}
</p>
<p class=" font-medium text-white">
{value == null ? "" : identityFieldValue(String(value), fieldName)}
</p>
</div>
{/each}
{/if}
</div>
{:else if variant === "eVault"}
<div>
<div class="flex justify-between mb-1">
<p class="z-[1]">{usedStorage}GB Used</p>
<p class="z-[1]">{totalStorage}GB total storage</p>
<p class="z-[1]">
{m.evault_storage_used_gb({ used: usedStorage })}
</p>
<p class="z-[1]">
{m.evault_storage_total_gb({ total: totalStorage })}
</p>
</div>
<div
class="relative w-full h-3 rounded-full overflow-hidden bg-primary-400"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { m } from "$lib/i18n";
import { ButtonAction } from "$lib/ui";
import { onMount } from "svelte";
import { cubicOut } from "svelte/easing";
Expand Down Expand Up @@ -69,7 +70,7 @@ onMount(async () => {
aria-hidden={!open}
>
<p class="text-white text-2xl font-medium leading-[120%]">
Your Digital Self
{m.onboarding_hero_title()}
</p>
</div>
<img
Expand Down Expand Up @@ -98,35 +99,35 @@ onMount(async () => {
callback={oncreate}
class="w-full uppercase tracking-wide active:bg-primary-400"
>
Create Digital Self
{m.onboarding_create_cta()}
</ButtonAction>
<ButtonAction
variant="soft"
callback={onrestore}
class="w-full uppercase tracking-wide text-black active:bg-primary-200"
>
Restore Digital Self
{m.splash_restore_cta()}
</ButtonAction>
<p
class="text-center font-medium text-md text-black-700/50 leading-normal"
>
By continuing you agree to our
{m.onboarding_terms_prefix()}
<a
href="https://metastate.foundation/"
target="_blank"
rel="noopener noreferrer"
class="text-primary"
>
Terms &amp; Conditions
{m.onboarding_terms_link()}
</a>
and
{m.common_and()}
<a
href="https://metastate.foundation/"
target="_blank"
rel="noopener noreferrer"
class="text-primary"
>
Privacy Policy
{m.common_privacy_policy()}
</a>
</p>
</div>
Expand Down
64 changes: 64 additions & 0 deletions infrastructure/eid-wallet/src/lib/i18n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Runtime translation corrections

Wording fixes reach users without an app store release. `messages/*.json`
remains the source of truth: the file served at `PUBLIC_TRANSLATIONS_URL` is
generated from it and layered over the strings compiled into the build, which
stay the fallback.

## Fixing wording

1. Edit `messages/ru.json` (or `en.json` / `uk.json`).
2. `pnpm translations:build` — regenerates `docs/static/translations.json`.
3. Open a PR and merge it. The docs site redeploys and the correction is live
in a couple of minutes, with no app release.

`pnpm check` fails when the generated file is stale, so the published catalog
cannot drift from the message files. Leaving `PUBLIC_TRANSLATIONS_URL` empty
disables the fetch entirely, which is the default.

## What this cannot fix

**New strings.** A correction only replaces a key that already shipped, so a
new screen still needs a release to introduce its keys. It does not need its
translations ready first: a key missing from `ru.json` falls back to English
rather than failing the build, and once the release is out its wording is
correctable like everything else.

**Plural messages.** They compile to a form selector, which a flat replacement
string cannot express. Everything else is correctable.

## Why there is no protected list

Wording on the PIN, recovery and signing screens can talk someone into
revealing a secret or approving something the confirmation misdescribes, so an
earlier version refused corrections for those keys.

It was dropped because publishing goes through a pull request: changing what
users read requires the same merge as changing the code, so the list guarded
nothing the repo did not already guard, while locking the screens where a
clumsy translation is most expensive behind an app release.

**Reinstate it if that ever stops being true** — a bucket upload, a CMS, or an
outside translator with an account. It is the mandatory review that makes the
list unnecessary, not the fact that only developers have access.

## What the app refuses

Bad entries are dropped individually and the rest still applied. A file is
discarded whole only when its `version` is not one the build understands.

| Refused | Why |
| --- | --- |
| Unknown key or locale | Nothing in the build renders it |
| Plural message | See above |
| Placeholder mismatch | `{platform}` must survive the correction, or the message renders a gap |

Rejections are logged with the offending key and reason.

## Behaviour

The catalog is fetched once per launch, after mount, and never blocks
rendering. The request revalidates, so an unchanged catalog costs a 304 rather
than a download. The last good copy is cached in `localStorage`, so
corrections are already on screen at first paint and the app keeps working
offline.
Loading
Loading