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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,19 @@ scope. Add yours.
containers is a `dict` too, so every lookup returned the table and the test
failed for a reason that looked like the code's. → A fake that serves one
thing or many decides by a field of the thing, not by its type.
- **A slice anchored on the first match, not the intended one.** Editing a
test file with a Python slice from `subprocess.run([NODE, "-e", script]` to
`return result.stdout.strip()` cut from the first test that contained the
first string to the helper that contained the second, deleting ninety tests
in between; the file was not committed and had to be rebuilt from memory. →
Anchor both ends of a slice inside the block you mean, assert the slice's
length is what you expect, and commit a test file before rewriting it by
script.
- **A key registered before it was used.** The extraction script wrote a
catalogue entry as soon as it saw a candidate and only then checked the
candidate was safe to rewrite, leaving a dozen keys no page asked for. →
Register an effect after the decision to take it, never before; a test
that every key is used (`test_no_key_is_left_unused`) catches the rest.

### Running the tests (read this before trusting "green")

Expand Down
22 changes: 22 additions & 0 deletions Docs/I18n/guide
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ and markup that carries its words as data, translated in one pass at load:
A key nobody defined shows as itself — ugly on purpose. `applyI18n(root)`
translates markup a page built later.

### What a page carries

A static sentence keeps its **English inside the element** and names its key:

```html
<button data-i18n="common.save">Save</button>
```

The English is what shows before `/i18n.js` answers, and what a page shows with
scripts off; `tests/test_i18n.py` holds it to the catalogue's English word for
word, so the two cannot drift. Markup that JS builds later is translated as it
arrives (`setHTML` translates before it compares, a `MutationObserver` catches
the rest). A message JS composes asks `t("area.key", {param: value})`;
`plural(n, "node")` looks up `count.node`, so a noun takes its language's forms.

Two things are **not** done yet and are the next ones to migrate, in this order:
paragraphs that mix text with inline markup (`<p>… <code>x</code> …</p>`), and
JS strings composed outside `toast()`, `setMessage()` and `.textContent =`. A
test refuses any new literal in those three calls. The chat page is being
rewritten on this system and is not migrated here. Text a *machine* reads
(API errors, protocol names) stays English.

## In an app

* **Python**: `from src import i18n` then `i18n.t("common.saved", "de")`.
Expand Down
3 changes: 2 additions & 1 deletion Docs/WebConsole/design
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ refusal, not as a sign-out.

Every page loads `/i18n.js` right after `/theme.js`; `ui.JS` then offers `t(key,
params)` and `applyI18n(root)` (`data-i18n`, `data-i18n-attr`). A page writes
**no sentence of its own**: it asks for a key. See
**no sentence of its own**: it asks for a key; a static sentence keeps its
English inside the element beside `data-i18n`, and a test holds the two equal. See
[`../I18n/guide`](../I18n/guide).

## Constraints you cannot see
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.backends.legacy:build"

[project]
name = "nmesh"
version = "0.4.117"
version = "0.4.118"
license = "GPL-3.0-only"

[tool.pytest.ini_options]
Expand Down
881 changes: 880 additions & 1 deletion src/locales/de.json

Large diffs are not rendered by default.

881 changes: 880 additions & 1 deletion src/locales/en.json

Large diffs are not rendered by default.

881 changes: 880 additions & 1 deletion src/locales/fr.json

Large diffs are not rendered by default.

913 changes: 912 additions & 1 deletion src/locales/pl.json

Large diffs are not rendered by default.

913 changes: 912 additions & 1 deletion src/locales/ru.json

Large diffs are not rendered by default.

913 changes: 912 additions & 1 deletion src/locales/sk.json

Large diffs are not rendered by default.

865 changes: 864 additions & 1 deletion src/locales/zh.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import re

__version__ = "0.4.117"
__version__ = "0.4.118"

# Releases are tagged ``vX.Y.Z`` (see .github/workflows/release.yml). Anything
# after the numbers (``-rc1``, ``+build``) is kept but only used to break ties
Expand Down
1,196 changes: 592 additions & 604 deletions src/webassets/console.py

Large diffs are not rendered by default.

561 changes: 280 additions & 281 deletions src/webassets/fleet.py

Large diffs are not rendered by default.

56 changes: 28 additions & 28 deletions src/webassets/nodeview.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@
'<div class="nv-sub">' + badges.join(" ") + "</div></div>" +
this.menuHTML(view, extras) + "</header>" +
'<div class="copyable nv-id"><code class="mono" data-nv-id>' + esc(view.id) +
'</code><button class="sm" data-nv-act="copy">Copy</button></div>';
'</code><button class="sm" data-nv-act="copy" data-i18n="common.copy">Copy</button></div>';
},

// What we are to each other, in sentences. The two fleet directions are
Expand Down Expand Up @@ -550,18 +550,18 @@
// go to be never found.
actionsHTML(view, extras, options){
if(view.self)
return '<div class="btn-row"><button data-nv-act="refresh">Refresh</button></div>';
return '<div class="btn-row"><button data-nv-act="refresh" data-i18n="common.refresh">Refresh</button></div>';
const hide = options.hide || [];
const chat = extras.chat, fleet = extras.fleet;
const buttons = [];
if(chat && hide.indexOf("chat") < 0)
buttons.push('<button class="primary" data-nv-act="message">Message' +
(chat.unread ? " " + badge(chat.unread, "danger") : "") + "</button>");
if(fleet && hide.indexOf("fleet") < 0){
if(fleet.managed) buttons.push('<button data-nv-act="fleet">Open in Fleet</button>');
if(fleet.managed) buttons.push('<button data-nv-act="fleet" data-i18n="node.open_in_fleet">Open in Fleet</button>');
else if(fleet.waiting_on_them)
buttons.push('<button disabled>Access requested</button>');
else buttons.push('<button data-nv-act="enrol">Request access</button>');
buttons.push('<button disabled data-i18n="node.access_requested">Access requested</button>');
else buttons.push('<button data-nv-act="enrol" data-i18n="node.request_access">Request access</button>');
}
buttons.push('<button data-nv-act="ping">' + icon("pulse") + "Ping</button>");
// Only where there is a link to load. A speed test with no direct link is
Expand Down Expand Up @@ -600,7 +600,7 @@
return '<div class="menu-wrap"><button class="icon" data-menu="nv-menu" ' +
'aria-haspopup="true" aria-expanded="false" aria-label="More about this node">' +
icon("dots") + "</button>" +
'<div id="nv-menu" class="menu" role="region" hidden aria-label="Node actions">' +
'<div id="nv-menu" class="menu" role="region" hidden aria-label="Node actions" data-i18n-attr="aria-label:node.node_actions">' +
items.join("") + "</div></div>";
},

Expand Down Expand Up @@ -773,7 +773,7 @@
const offered = !view.self && (this.here || CHANNEL.has("node.retry"));
const action = offered ? '<th class="tight"></th>' : "";
return '<div class="table-wrap"><table><thead><tr>' +
'<th>Address</th><th>State</th><th class="num">Tried</th>' + action +
'<th data-i18n="fleet.address">Address</th><th data-i18n="node.state">State</th><th class="num" data-i18n="node.tried">Tried</th>' + action +
"</tr></thead><tbody>" +
rows.map((row) => '<tr><td class="mono">' + esc(row.uri) + "</td><td>" +
'<span class="badge" data-base="badge" data-v="' +
Expand Down Expand Up @@ -1058,13 +1058,13 @@
const bar = element.querySelector("[data-nv-speed-bar]");
if(bar) bar.value = seconds;
const clock = element.querySelector("[data-nv-speed-clock]");
if(clock) clock.textContent = Math.floor(seconds) + " s of at most " + this.SPEED_SECONDS;
if(clock) clock.textContent = t("node.s_of_at_most", {seconds: Math.floor(seconds), speed_seconds: this.SPEED_SECONDS});
},

speedHTML(id){
const run = this.speed[id];
if(!run) return "";
const head = (tail) => '<div class="nv-speed-head"><b>Speed test</b>' +
const head = (tail) => '<div class="nv-speed-head"><b data-i18n="node.speed_test">Speed test</b>' +
'<span class="grow"></span>' + tail + "</div>";
if(run.running)
return '<div class="nv-speed" role="status">' +
Expand Down Expand Up @@ -1137,7 +1137,7 @@
await withBusy(button, async () => {
try{
await this.call("chat", "contact", {node:id});
toast("Added to contacts", "ok");
toast(t("node.added_to_contacts"), "ok");
await this.mount(element, id, options);
}catch(error){ this.say(element, String(error.message || error), true); }
});
Expand All @@ -1153,10 +1153,10 @@
body:'<p class="muted small">That node makes it, not this one: whoever uses ' +
"it joins through that machine and has their certificate signed by it. " +
"Single use, and it stops working when the window closes.</p>" +
'<label class="field"><span>Stays live for</span><select id="nv-ttl">' +
'<option value="300">5 minutes</option>' +
'<option value="3600">1 hour</option>' +
'<option value="21600">6 hours</option></select></label>' +
'<label class="field"><span data-i18n="fleet.stays_live_for">Stays live for</span><select id="nv-ttl">' +
'<option value="300" data-i18n="node.5_minutes">5 minutes</option>' +
'<option value="3600" data-i18n="node.1_hour">1 hour</option>' +
'<option value="21600" data-i18n="node.6_hours">6 hours</option></select></label>' +
'<label class="check"><input id="nv-ticket" type="checkbox" checked>' +
"<span>Also make it scannable, if that node has a public address</span></label>",
});
Expand Down Expand Up @@ -1210,7 +1210,7 @@
try{
const result = await this.call("fleet", "enrol", {node:id, caps:wanted});
if(!result.sent){ this.say(element, "Fleet refused that request", true); return; }
toast("Request sent — someone there has to accept it", "ok");
toast(t("node.request_sent_someone_there_has_to"), "ok");
await this.mount(element, id, options);
}catch(error){ this.say(element, String(error.message || error), true); }
});
Expand All @@ -1228,7 +1228,7 @@
if(!agreed) return;
try{
await this.op("node.forget", {node:id});
toast("Node forgotten");
toast(t("node.node_forgotten"));
this.current = null;
if(options.onGone) options.onGone();
}catch(error){
Expand Down Expand Up @@ -1272,7 +1272,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#f6f8fa" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0a0e13" media="(prefers-color-scheme: dark)">
<title>NMesh node</title>
<title data-i18n="node.nmesh_node">NMesh node</title>
<script src="/theme.js"></script>
<script src="/i18n.js"></script>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
Expand All @@ -1282,23 +1282,23 @@

<div id="login" class="gate hidden">
<form id="login-form">
<div class="mark" aria-hidden="true">NM</div>
<div><p class="eyebrow">Node</p><h1>Sign in</h1></div>
<p class="muted small">This page reads the console of this node, so it needs
<div class="mark" aria-hidden="true" data-i18n="console.nm">NM</div>
<div><p class="eyebrow" data-i18n="node.node">Node</p><h1 data-i18n="fleet.sign_in">Sign in</h1></div>
<p class="muted small" data-i18n="node.this_page_reads_the_console_of_this">This page reads the console of this node, so it needs
the console password.</p>
<label class="field"><span>Console password</span>
<label class="field"><span data-i18n="console.console_password">Console password</span>
<input id="password" type="password" autocomplete="current-password" autofocus></label>
<button type="submit" class="primary wide">Enter</button>
<button type="submit" class="primary wide" data-i18n="fleet.enter">Enter</button>
<p id="login-error" class="msg error" role="alert"></p>
</form>
</div>

<main id="main" class="node-page hidden">""" + ui.ctx_bar(leave=False) + """
<header class="node-page-head">
<a class="brand" href="/"><span class="mark" aria-hidden="true">NM</span>
<span><b>NMesh</b><span>Node</span></span></a>
<a class="brand" href="/"><span class="mark" aria-hidden="true" data-i18n="console.nm">NM</span>
<span><b>NMesh</b><span data-i18n="node.node">Node</span></span></a>
<span class="grow"></span>
<button id="theme-toggle" class="icon" aria-label="Switch theme"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.5 14.8A8.6 8.6 0 0 1 9.2 3.5a8.6 8.6 0 1 0 11.3 11.3Z"/></svg></button>
<button id="theme-toggle" class="icon" aria-label="Switch theme" data-i18n-attr="aria-label:console.switch_theme"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.5 14.8A8.6 8.6 0 0 1 9.2 3.5a8.6 8.6 0 1 0 11.3 11.3Z"/></svg></button>
</header>
<div id="view"></div>
</main>
Expand All @@ -1308,8 +1308,8 @@
<header class="sheet-head"><h2 id="confirm-title"></h2></header>
<div class="sheet-body"><div id="confirm-body"></div></div>
<footer class="sheet-foot">
<button id="confirm-cancel">Cancel</button>
<button id="confirm-ok" class="primary">Confirm</button>
<button id="confirm-cancel" data-i18n="common.cancel">Cancel</button>
<button id="confirm-ok" class="primary" data-i18n="console.confirm">Confirm</button>
</footer>
</div>
</dialog>
Expand Down Expand Up @@ -1419,7 +1419,7 @@
}
SESSION.set(data.token);
enter();
}catch(_){ setMessage("login-error", "Console is not reachable", true); }
}catch(_){ setMessage("login-error", t("console.console_is_not_reachable"), true); }
});

// Ask before drawing. A stored token can be stale, and a console reached
Expand Down
Loading
Loading