-Fast [EigenTrust](https://nlp.stanford.edu/pubs/eigentrust.pdf) in Rust. Runs on the command line and in the browser through WebAssembly.
+
+ Global trust scores from local trust. Fast, in Rust and WebAssembly.
+
-**Try it: [eigentrust.jenyadoesapps.com](https://eigentrust.jenyadoesapps.com)**. Build a trust network, pick the peers you already trust, and watch every peer get ranked as you edit. The playground can also rank a generated network of 250,000 peers right in the browser.
+
+
+
+
+
+
+
+
+
+
## What it does
-You give it two things:
+- **Input:** who trusts whom (`alice,bob,3`), plus a few seed peers you already trust.
+- **Output:** a score for every peer. The scores add up to 1.
+- **Why:** trust spreads from the seeds, so fake accounts that only vouch for each other get almost nothing.
+
+Based on the [EigenTrust paper](https://nlp.stanford.edu/pubs/eigentrust.pdf) (Kamvar et al., 2003).
-- **Local trust**: who trusts whom, and how much. `alice,bob,3` means alice trusts bob with weight 3.
-- **Seeds (pre-trust)**: a few peers you already trust, such as your own accounts or a vetted core team.
+## Try it
-It returns a global trust score for every peer. The scores add up to 1. Trust starts at the seeds and flows along the network. At every step a share **α** of it goes back to the seeds, so a cluster of fake accounts that only vouch for each other can't create trust out of nothing. Raising α tightens that further.
+**[eigentrust.jenyadoesapps.com](https://eigentrust.jenyadoesapps.com)**: edit a network, move the α slider, see the ranking update. You can also rank 250,000 peers in the browser.
-Typical uses are reputation in peer-to-peer and social networks, spam and Sybil resistance, and ranking contributors or accounts by who vouches for them.
+Available in English, Español, 中文, हिन्दी, العربية, Português, Français, Deutsch, Русский and 日本語. The language follows your browser; you can change it in the header or with `?lang=`.
-## Quick start
+## Command line
```sh
-cargo run --release -- ./example/localtrust.csv ./example/pretrust.csv
+cargo run --release -- ./example/localtrust.csv ./example/pretrust.csv [alpha]
```
-The output is one `peer,score` line per peer, highest first:
-
```
alice,0.6666666865348816
bob,0.3333333134651184
```
-An optional third argument sets α (default `0.5`):
-
-```sh
-cargo run --release -- ./example/localtrust2.csv ./example/pretrust2.csv 0.2
-```
-
-Set `RUST_LOG=debug` or `RUST_LOG=trace` to follow the iterations.
-
-## Input format
-
-Both files are plain CSV. A header row is optional. Spaces, quoted fields, Windows line endings and a UTF-8 BOM are all handled.
-
-Local trust has one statement per line: `from,to[,weight]`. The weight defaults to 1.
-
-```csv
-from,to,weight
-alice,bob,2
-bob,charlie,2
-alice,charlie,1
-charlie,bob
-```
-
-Seeds have one peer per line: `peer[,weight]`. Weights are normalized.
-
-```csv
-peer,weight
-alice,1
-```
-
-Details worth knowing:
-
-- If the same `from,to` pair appears more than once, the last line wins.
-- A peer who trusts nobody passes their trust on to the seeds.
-- With no seeds, every peer starts with equal trust, which behaves like PageRank.
-- Peer ids are any strings without commas: names, numbers, wallet addresses.
+α defaults to `0.5`. A higher α keeps trust closer to the seeds.
-## In the browser
-
-Build the WebAssembly package:
-
-```sh
-wasm-pack build --target web --release
-```
-
-Then call it, ideally from a Web Worker so the page stays responsive:
+## Browser
```js
import init, { run } from './pkg/eigentrust.js'
await init()
-
-const localtrust = new TextEncoder().encode('alice,bob,2\nbob,charlie,2\nalice,charlie,1\ncharlie,bob,1\n')
-const pretrust = new TextEncoder().encode('alice,1\n')
-
-const result = JSON.parse(run(localtrust, pretrust, 0.5))
-// { Ok: [["alice", 0.5], ["bob", 0.278], ["charlie", 0.222]] }
-// or { Err: "Cannot parse local trust CSV record #3: ..." }
+const enc = new TextEncoder()
+const result = JSON.parse(run(enc.encode('alice,bob,2\nbob,carol,1\n'), enc.encode('alice\n'), 0.5))
+// { Ok: [["alice", ...], ["bob", ...], ["carol", ...]] } or { Err: "..." }
```
-[`demo/worker.js`](demo/worker.js) is a ready-made worker. It also uses the multithreaded build when the page allows it.
+Build with `./build.sh`. For a ready-made Web Worker, see [`demo/worker.js`](demo/worker.js).
-### Multithreading
-
-There are two WebAssembly builds:
+## Input format
-| Build | Toolchain | Where it runs |
+| File | Line | Example |
| --- | --- | --- |
-| `pkg/` | stable | everywhere |
-| `pkg-parallel/` | nightly, `+atomics` | pages served with `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` |
+| Local trust | `from,to[,weight]` | `alice,bob,2` |
+| Seeds | `peer[,weight]` | `alice,1` |
-The parallel build spreads the matrix-vector product over Web Workers using [wasm-bindgen-rayon](https://github.com/RReverser/wasm-bindgen-rayon). Call `initThreadPool(navigator.hardwareConcurrency)` once before `run`. `./build.sh` builds both.
-
-In practice the iteration is already fast. Most of the time on big inputs goes to reading the CSV, which runs on one thread, so threads give about 5–15% end to end.
+- A header row is optional. Spaces, quoted fields, CRLF and a UTF-8 BOM are fine.
+- Weights default to 1. Repeated `from,to` pairs: the last line wins.
+- With no seeds, every peer starts equal (like PageRank).
## Performance
-Measured on a laptop with random networks where each peer trusts 10 others. Times include parsing the CSV.
+Random networks, 10 links per peer, CSV parsing included.
-| Network | Native CLI | Browser, 1 thread | Browser, all threads |
-| --- | --- | --- | --- |
-| 20,000 peers, 200,000 links | 0.06 s | 58 ms | |
-| 100,000 peers, 1,000,000 links | 0.3–0.4 s | 212 ms | 200 ms |
-| 250,000 peers, 2,500,000 links | | 709 ms | 614 ms |
+| Peers | Links | CLI | Browser |
+| ---: | ---: | ---: | ---: |
+| 20,000 | 200,000 | 0.06 s | 58 ms |
+| 100,000 | 1,000,000 | 0.4 s | 0.2 s |
+| 250,000 | 2,500,000 | | 0.6 s |
-Each iteration of the power method costs O(links): it multiplies a sparse matrix by a dense vector, using compensated (Kahan–Babuška) summation for stable results.
+The browser build uses all CPU cores when the page is cross-origin isolated (see [`demo/vercel.json`](demo/vercel.json)).
## Development
```sh
-cargo test --release # unit tests
-./build.sh # both WASM builds, copied into demo/
-python3 -m http.server -d demo 8000 # playground, single-threaded
-```
-
-The playground in [`demo/`](demo) is static HTML and JavaScript with no build step. It's deployed to Vercel from that folder, and [`demo/vercel.json`](demo/vercel.json) sets the headers the multithreaded build needs.
-
-After cloning, enable the repository's git hooks once:
-
-```sh
-git config core.hooksPath .githooks
+cargo test --release # tests
+./build.sh # WASM builds, copied into demo/
+python3 -m http.server -d demo # run the playground locally
+git config core.hooksPath .githooks # once per clone
```
-
-## Reference
-
-Sepandar D. Kamvar, Mario T. Schlosser, Hector Garcia-Molina. [The EigenTrust Algorithm for Reputation Management in P2P Networks](https://nlp.stanford.edu/pubs/eigentrust.pdf). WWW 2003.
diff --git a/demo/app.js b/demo/app.js
index 1bdf2cd..a34c814 100644
--- a/demo/app.js
+++ b/demo/app.js
@@ -1,5 +1,7 @@
// EigenTrust playground: an editable trust network ranked live by the WASM engine.
+import { LANGUAGES, detectLanguage, saveLanguage, setLanguage, lang, t, num } from './i18n.js'
+
const MAX_GRAPH_PEERS = 400
const NAMES = ['alice', 'bob', 'carol', 'dave', 'erin', 'frank', 'grace', 'heidi', 'ivan', 'judy',
'mallory', 'niaj', 'olivia', 'peggy', 'rupert', 'trent', 'victor', 'walter', 'yara', 'zoe']
@@ -190,18 +192,19 @@ async function compute() {
$('stats').textContent = res.error
} else {
state.scores = res.scores
- state.lastRun = { ms: res.ms, mode: res.mode }
+ state.lastRun = { ms: res.ms, threads: res.threads }
}
- if (res.mode) $('engine').textContent = engineText()
+ if (res.threads) $('engine').textContent = engineText()
applyScores()
if (dirty) compute()
}
+const fmtMs = (ms) => ms < 1 ? num(ms, { maximumFractionDigits: 2 }) : ms < 10 ? num(ms, { maximumFractionDigits: 1 }) : num(Math.round(ms))
+const modeText = (threads) => t('mode', { n: num(threads || 1) })
+
function engineText() {
- if (!state.lastRun) return 'Running in your browser'
- const ms = state.lastRun.ms
- const t = ms < 1 ? ms.toFixed(2) : ms < 10 ? ms.toFixed(1) : Math.round(ms).toLocaleString()
- return `Ranked in ${t} ms, ${state.lastRun.mode}`
+ if (!state.lastRun) return t('running')
+ return t('rankedIn', { ms: fmtMs(state.lastRun.ms), mode: modeText(state.lastRun.threads) })
}
function applyScores() {
@@ -521,10 +524,10 @@ function select(id) {
// ---------- panel ----------
const fmtShare = (s) => {
- const pct = s * 100
- if (pct === 0) return '0%'
- if (pct < 0.01) return '<0.01%'
- return (pct < 1 ? pct.toFixed(2) : pct.toFixed(1)) + '%'
+ if (s === 0) return num(0, { style: 'percent' })
+ if (s < 0.0001) return '<' + num(0.0001, { style: 'percent', maximumFractionDigits: 2 })
+ const digits = s < 0.01 ? 2 : 1
+ return num(s, { style: 'percent', minimumFractionDigits: digits, maximumFractionDigits: digits })
}
function el(tag, attrs = {}, ...children) {
@@ -548,11 +551,11 @@ function renderStage() {
const note = $('stageNote')
let text = ''
if (state.large) {
- text = `This network has ${state.large.peers.toLocaleString()} peers, too many to draw. The full ranking is in the panel. Pick a starting network to go back to the graph.`
+ text = t('noteLarge', { n: num(state.large.peers) })
} else if (state.peers.size === 0) {
- text = 'Double-click anywhere to add a peer, or pick a starting network in the panel.'
+ text = t('noteEmpty')
} else if (state.edges.size === 0) {
- text = 'Click a peer, then click another to add trust between them.'
+ text = t('noteNoLinks')
}
note.textContent = text
note.hidden = !text
@@ -578,26 +581,26 @@ function renderRanking() {
onclick: () => { if (!state.large) select(id === state.selected ? null : id) },
'aria-pressed': String(id === state.selected),
},
- el('span', { class: 'rank' }, score > 0 ? String(i + 1) : ''),
+ el('span', { class: 'rank' }, score > 0 ? num(i + 1) : ''),
el('span', { class: 'name' + (state.seeds.has(id) ? ' is-seed' : ''), title: id }, id),
el('span', { class: 'bar' }, el('i', { style: `width:${(score / max) * 100}%` })),
el('span', { class: 'score' }, fmtShare(score)))
list.append(el('li', {}, button))
})
- if (rows.length > shown.length) list.append(el('li', { class: 'ranking-more' }, `and ${(rows.length - shown.length).toLocaleString()} more`))
- if (!rows.length) list.append(el('li', { class: 'ranking-more' }, 'Scores appear once peers trust each other.'))
+ if (rows.length > shown.length) list.append(el('li', { class: 'ranking-more' }, t('more', { n: num(rows.length - shown.length) })))
+ if (!rows.length) list.append(el('li', { class: 'ranking-more' }, t('noScores')))
if (state.large) {
- stats.textContent = `${state.large.peers.toLocaleString()} peers`
+ stats.textContent = t('statPeers', { n: num(state.large.peers) })
} else {
- const parts = [`${state.peers.size} peers, ${state.edges.size} links`]
+ const parts = [t('statNetwork', { p: num(state.peers.size), l: num(state.edges.size) })]
const sybils = [...state.peers.values()].filter((p) => p.group === 'sybil')
if (sybils.length && state.scores.length) {
- parts.push(`sybils hold ${fmtShare(sybils.reduce((s, p) => s + p.score, 0))}`)
+ parts.push(t('statSybil', { x: fmtShare(sybils.reduce((s, p) => s + p.score, 0)) }))
} else if (state.seeds.size === 0 && state.edges.size) {
- parts.push('no seeds, everyone starts equal')
+ parts.push(t('statNoSeeds'))
}
- stats.textContent = parts.join(', ')
+ stats.textContent = parts.join(lang() === 'zh' || lang() === 'ja' ? ',' : lang() === 'ar' ? '، ' : ', ')
}
$('engine').textContent = engineText()
}
@@ -613,30 +616,30 @@ function renderSelected() {
const others = [...state.peers.keys()].filter((o) => o !== id && !state.edges.has(edgeKey(id, o)))
const isSeed = state.seeds.has(id)
- const addSelect = el('select', { 'aria-label': `Peer ${id} should trust` }, others.map((o) => el('option', { value: o }, o)))
+ const addSelect = el('select', { 'aria-label': t('ariaShouldTrust', { id }) }, others.map((o) => el('option', { value: o }, o)))
box.replaceChildren(
el('div', { class: 'sel-head' },
el('h3', {}, id),
- el('span', { class: 'sel-score' }, p.score > 0 ? `${fmtShare(p.score)}, rank ${rank + 1}` : 'no trust yet')),
+ el('span', { class: 'sel-score' }, p.score > 0 ? t('selScore', { x: fmtShare(p.score), r: num(rank + 1) }) : t('noTrustYet'))),
el('div', { class: 'sel-actions' },
- el('button', { type: 'button', class: isSeed ? 'seed-on' : '', 'aria-pressed': String(isSeed), onclick: () => toggleSeed(id) }, isSeed ? 'Seed peer' : 'Make seed'),
- el('button', { type: 'button', onclick: () => removePeer(id) }, 'Remove peer'),
- el('button', { type: 'button', onclick: () => select(null) }, 'Done')),
- el('h4', {}, `Trusts (${out.length})`),
+ el('button', { type: 'button', class: isSeed ? 'seed-on' : '', 'aria-pressed': String(isSeed), onclick: () => toggleSeed(id) }, isSeed ? t('isSeed') : t('makeSeed')),
+ el('button', { type: 'button', onclick: () => removePeer(id) }, t('removePeer')),
+ el('button', { type: 'button', onclick: () => select(null) }, t('done'))),
+ el('h4', {}, t('trusts', { n: num(out.length) })),
out.length
? el('ul', { class: 'edges' }, out.map((e) => el('li', {},
el('span', {}, e.to),
el('input', {
- type: 'number', min: '0', step: '1', value: String(e.w), 'aria-label': `Trust from ${id} to ${e.to}`,
+ type: 'number', min: '0', step: '1', value: String(e.w), 'aria-label': t('ariaTrust', { a: id, b: e.to }),
onchange: (ev) => setEdge(id, e.to, parseFloat(ev.target.value)),
}),
- el('button', { type: 'button', 'aria-label': `Remove trust from ${id} to ${e.to}`, onclick: () => setEdge(id, e.to, 0) }, 'Remove'))))
- : el('p', { class: 'empty-note' }, 'Click another peer in the graph to add trust.'),
+ el('button', { type: 'button', 'aria-label': t('ariaRemoveTrust', { a: id, b: e.to }), onclick: () => setEdge(id, e.to, 0) }, t('remove')))))
+ : el('p', { class: 'empty-note' }, t('trustsEmpty')),
others.length ? el('div', { class: 'add-edge' }, addSelect,
- el('button', { type: 'button', onclick: () => { setEdge(id, addSelect.value, 1); reheat(0.25) } }, 'Add trust')) : null,
- el('h4', {}, `Trusted by (${inc.length})`),
- el('p', { class: 'empty-note' }, inc.length ? inc.map((e) => `${e.from} (${e.w})`).join(', ') : 'Nobody yet.'),
+ el('button', { type: 'button', onclick: () => { setEdge(id, addSelect.value, 1); reheat(0.25) } }, t('addTrust'))) : null,
+ el('h4', {}, t('trustedBy', { n: num(inc.length) })),
+ el('p', { class: 'empty-note' }, inc.length ? inc.map((e) => `${e.from} (${num(e.w)})`).join(', ') : t('nobody')),
)
box.hidden = false
}
@@ -646,7 +649,7 @@ function renderSelected() {
function setAlpha(a) {
state.alpha = a
$('alpha').value = a
- $('alphaOut').textContent = a.toFixed(2)
+ $('alphaOut').textContent = num(a, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
$('alpha').addEventListener('input', (ev) => { setAlpha(parseFloat(ev.target.value)); changed() })
@@ -661,11 +664,11 @@ $('share').addEventListener('click', async () => {
writeHash()
try {
await navigator.clipboard.writeText(location.href)
- $('share').textContent = 'Link copied'
+ $('share').textContent = t('linkCopied')
} catch {
- $('share').textContent = 'Copy from the address bar'
+ $('share').textContent = t('copyFallback')
}
- setTimeout(() => { $('share').textContent = 'Copy link' }, 1600)
+ setTimeout(() => { $('share').textContent = t('copyLink') }, 1600)
})
// tabs
@@ -676,7 +679,8 @@ for (const t of tabs) {
$('tab-network').parentElement.addEventListener('keydown', (ev) => {
if (ev.key !== 'ArrowRight' && ev.key !== 'ArrowLeft') return
const cur = tabs.findIndex((t) => $('tab-' + t).getAttribute('aria-selected') === 'true')
- const next = tabs[(cur + (ev.key === 'ArrowRight' ? 1 : tabs.length - 1)) % tabs.length]
+ const forward = (ev.key === 'ArrowRight') !== (document.documentElement.dir === 'rtl')
+ const next = tabs[(cur + (forward ? 1 : tabs.length - 1)) % tabs.length]
showTab(next)
$('tab-' + next).focus()
})
@@ -710,7 +714,7 @@ for (const [key, fileId, textId] of [['lt', 'ltFile', 'ltText'], ['pt', 'ptFile'
const text = await file.text()
if (text.length > BIG_TEXT) {
bigFiles[key] = text
- $(textId).value = `${file.name}: ${countLines(text).toLocaleString()} lines, too large to edit here.`
+ $(textId).value = t('tooLarge', { name: file.name, n: num(countLines(text)) })
$(textId).readOnly = true
} else {
bigFiles[key] = null
@@ -757,12 +761,12 @@ $('csvLoad').addEventListener('click', async () => {
const pt = bigFiles.pt ?? $('ptText').value
const btn = $('csvLoad')
btn.disabled = true
- btn.textContent = 'Ranking'
+ btn.textContent = t('busy')
const res = await runEngine(lt, pt, state.alpha)
btn.disabled = false
- btn.textContent = 'Load into network'
+ btn.textContent = t('loadNetwork')
if (res.error) {
- err.textContent = `The engine rejected this input: ${res.error}`
+ err.textContent = t('rejected', { e: res.error })
err.hidden = false
return
}
@@ -774,7 +778,7 @@ $('csvLoad').addEventListener('click', async () => {
const count = ltRows ? peers.size : new Set(lt.split('\n').flatMap((l) => l.split(',').slice(0, 2).map(clean)).filter(Boolean)).size
state.large = { lt, pt, peers: count }
state.scores = res.scores
- state.lastRun = { ms: res.ms, mode: res.mode }
+ state.lastRun = { ms: res.ms, threads: res.threads }
applyScores()
renderPanel()
wake()
@@ -829,12 +833,12 @@ $('benchRun').addEventListener('click', async () => {
const out = $('benchOut')
const btn = $('benchRun')
btn.disabled = true
- out.replaceChildren(el('p', {}, `Generating ${n.toLocaleString()} peers`))
+ out.replaceChildren(el('p', {}, t('generating', { n: num(n) })))
await new Promise((r) => setTimeout(r, 30))
const t0 = performance.now()
const net = generateNetwork(n, degree)
const genMs = performance.now() - t0
- out.replaceChildren(el('p', {}, `Ranking ${net.edges.toLocaleString()} trust statements`))
+ out.replaceChildren(el('p', {}, t('rankingN', { n: num(net.edges) })))
const size = net.lt.length
const res = await runEngine(net.lt, net.pt, state.alpha)
btn.disabled = false
@@ -843,8 +847,11 @@ $('benchRun').addEventListener('click', async () => {
return
}
out.replaceChildren(
- el('div', { class: 'big' }, `${Math.round(res.ms).toLocaleString()} ms`),
- el('p', {}, `to parse ${(size / 1e6).toFixed(1)} MB of CSV and rank ${n.toLocaleString()} peers from ${net.edges.toLocaleString()} trust statements with α ${state.alpha.toFixed(2)}. ${res.mode}. Generating the data took ${Math.round(genMs)} ms.`),
+ el('div', { class: 'big' }, `${num(Math.round(res.ms))} ms`),
+ el('p', {}, t('benchResult', {
+ mb: num(size / 1e6, { maximumFractionDigits: 1 }), n: num(n), e: num(net.edges),
+ a: num(state.alpha, { minimumFractionDigits: 2 }), mode: modeText(res.threads), g: num(Math.round(genMs)),
+ })),
el('ol', { class: 'ranking' }, res.scores.slice(0, 8).map(([p, s], i) => el('li', {}, el('button', { type: 'button', tabindex: '-1' },
el('span', { class: 'rank' }, String(i + 1)), el('span', { class: 'name' }, p),
el('span', { class: 'bar' }, el('i', { style: `width:${(s / res.scores[0][1]) * 100}%` })),
@@ -891,7 +898,30 @@ function readHash() {
}
}
+// ---------- language ----------
+
+function localizeNumbers() {
+ for (const o of $('benchPeers').options) o.textContent = num(parseInt(o.value, 10))
+ for (const o of $('benchDegree').options) o.textContent = num(parseInt(o.value, 10))
+}
+
+function applyLanguage(code) {
+ setLanguage(code)
+ $('lang').value = lang()
+ localizeNumbers()
+ setAlpha(state.alpha)
+ renderPanel()
+ wake()
+}
+
+$('lang').replaceChildren(...Object.entries(LANGUAGES).map(([code, name]) => el('option', { value: code, lang: code }, name)))
+$('lang').addEventListener('change', (ev) => {
+ saveLanguage(ev.target.value)
+ applyLanguage(ev.target.value)
+})
+
// ---------- start ----------
+applyLanguage(detectLanguage())
resize()
if (!readHash()) loadNetwork(PRESETS.friends)
diff --git a/demo/i18n.js b/demo/i18n.js
new file mode 100644
index 0000000..33919c3
--- /dev/null
+++ b/demo/i18n.js
@@ -0,0 +1,786 @@
+// UI translations. Language is picked from ?lang=, then the saved choice, then the browser.
+
+export const LANGUAGES = {
+ en: 'English',
+ es: 'Español',
+ zh: '中文',
+ hi: 'हिन्दी',
+ ar: 'العربية',
+ pt: 'Português',
+ fr: 'Français',
+ de: 'Deutsch',
+ ru: 'Русский',
+ ja: '日本語',
+}
+
+const RTL = new Set(['ar'])
+
+const STRINGS = {
+ en: {
+ title: 'EigenTrust playground',
+ description: 'Build a trust network and watch EigenTrust rank every peer, live in your browser.',
+ source: 'Source',
+ language: 'Language',
+ h1: 'Trust flows from the peers you already trust.',
+ intro: 'Click a peer, then click another to add trust between them. Double-click a peer to make it a seed. Bigger circles hold more trust.',
+ canvasLabel: 'Trust network graph. Use the panel to edit it with a keyboard.',
+ addPeer: 'Add peer',
+ tidy: 'Tidy layout',
+ copyLink: 'Copy link',
+ linkCopied: 'Link copied',
+ copyFallback: 'Copy from the address bar',
+ legendSeed: 'Seed peer',
+ legendScore: 'Trust score',
+ legendEdge: 'Trust given',
+ tabNetwork: 'Network',
+ tabCsv: 'CSV',
+ tabBench: 'Benchmark',
+ startFrom: 'Start from',
+ presetFriends: 'A small community',
+ presetSybil: 'A Sybil attack',
+ presetChain: 'A chain of referrals',
+ presetEmpty: 'An empty network',
+ seedWeight: 'Seed weight',
+ alphaHint: 'How strongly every step pulls trust back to the seeds. Higher keeps trust close to them and starves Sybil clusters. Lower lets it travel further.',
+ ranking: 'Ranking',
+ csvHint: 'One trust statement per line: from,to,weight. Seeds are peer,weight. Headers, quotes and spaces are fine.',
+ localTrust: 'Local trust',
+ upload: 'Upload',
+ seeds: 'Seeds (pre-trust)',
+ loadNetwork: 'Load into network',
+ busy: 'Ranking',
+ downloadScores: 'Download scores',
+ benchHint: 'Generate a random network in the browser and rank it. Nothing leaves your machine.',
+ peers: 'Peers',
+ perPeer: 'Trust statements per peer',
+ benchRun: 'Generate and rank',
+ loadingWasm: 'Loading WebAssembly',
+ running: 'Running in your browser',
+ rankedIn: 'Ranked in {ms} ms, {mode}',
+ mode: 'WebAssembly, threads: {n}',
+ noteLarge: 'This network has {n} peers, too many to draw. The full ranking is in the panel. Pick a starting network to go back to the graph.',
+ noteEmpty: 'Double-click anywhere to add a peer, or pick a starting network in the panel.',
+ noteNoLinks: 'Click a peer, then click another to add trust between them.',
+ more: 'and {n} more',
+ noScores: 'Scores appear once peers trust each other.',
+ statPeers: 'peers: {n}',
+ statNetwork: 'peers: {p}, links: {l}',
+ statSybil: 'sybils hold {x}',
+ statNoSeeds: 'no seeds, everyone starts equal',
+ selScore: '{x}, rank {r}',
+ noTrustYet: 'no trust yet',
+ isSeed: 'Seed peer',
+ makeSeed: 'Make seed',
+ removePeer: 'Remove peer',
+ done: 'Done',
+ trusts: 'Trusts ({n})',
+ trustsEmpty: 'Click another peer in the graph to add trust.',
+ addTrust: 'Add trust',
+ trustedBy: 'Trusted by ({n})',
+ nobody: 'Nobody yet.',
+ remove: 'Remove',
+ ariaShouldTrust: 'Peer {id} should trust',
+ ariaTrust: 'Trust from {a} to {b}',
+ ariaRemoveTrust: 'Remove trust from {a} to {b}',
+ tooLarge: '{name}: {n} lines, too large to edit here.',
+ rejected: 'The engine rejected this input: {e}',
+ generating: 'Generating {n} peers',
+ rankingN: 'Ranking {n} trust statements',
+ benchResult: 'to parse {mb} MB of CSV and rank {n} peers from {e} trust statements with α {a}. {mode}. Generating the data took {g} ms.',
+ },
+ es: {
+ title: 'EigenTrust interactivo',
+ description: 'Crea una red de confianza y observa cómo EigenTrust clasifica a cada participante, en vivo en tu navegador.',
+ source: 'Código',
+ language: 'Idioma',
+ h1: 'La confianza fluye desde quienes ya son de confianza.',
+ intro: 'Haz clic en un participante y luego en otro para añadir confianza entre ellos. Haz doble clic en un participante para convertirlo en semilla. Los círculos más grandes acumulan más confianza.',
+ canvasLabel: 'Grafo de la red de confianza. Usa el panel para editarlo con el teclado.',
+ addPeer: 'Añadir participante',
+ tidy: 'Reordenar',
+ copyLink: 'Copiar enlace',
+ linkCopied: 'Enlace copiado',
+ copyFallback: 'Cópialo desde la barra de direcciones',
+ legendSeed: 'Semilla',
+ legendScore: 'Puntuación de confianza',
+ legendEdge: 'Confianza otorgada',
+ tabNetwork: 'Red',
+ tabCsv: 'CSV',
+ tabBench: 'Rendimiento',
+ startFrom: 'Empezar con',
+ presetFriends: 'Una comunidad pequeña',
+ presetSybil: 'Un ataque Sybil',
+ presetChain: 'Una cadena de recomendaciones',
+ presetEmpty: 'Una red vacía',
+ seedWeight: 'Peso de las semillas',
+ alphaHint: 'Cuánto devuelve cada paso la confianza a las semillas. Un valor alto la mantiene cerca de ellas y deja sin confianza a los grupos Sybil. Uno bajo le permite llegar más lejos.',
+ ranking: 'Clasificación',
+ csvHint: 'Una declaración de confianza por línea: from,to,weight. Las semillas son peer,weight. Se admiten encabezados, comillas y espacios.',
+ localTrust: 'Confianza local',
+ upload: 'Subir',
+ seeds: 'Semillas (confianza previa)',
+ loadNetwork: 'Cargar en la red',
+ busy: 'Calculando',
+ downloadScores: 'Descargar puntuaciones',
+ benchHint: 'Genera una red aleatoria en el navegador y clasifícala. Nada sale de tu equipo.',
+ peers: 'Participantes',
+ perPeer: 'Declaraciones de confianza por participante',
+ benchRun: 'Generar y clasificar',
+ loadingWasm: 'Cargando WebAssembly',
+ running: 'Se ejecuta en tu navegador',
+ rankedIn: 'Clasificado en {ms} ms, {mode}',
+ mode: 'WebAssembly, hilos: {n}',
+ noteLarge: 'Esta red tiene {n} participantes, demasiados para dibujarla. La clasificación completa está en el panel. Elige una red inicial para volver al grafo.',
+ noteEmpty: 'Haz doble clic en cualquier lugar para añadir un participante, o elige una red inicial en el panel.',
+ noteNoLinks: 'Haz clic en un participante y luego en otro para añadir confianza entre ellos.',
+ more: 'y {n} más',
+ noScores: 'Las puntuaciones aparecen cuando los participantes confían entre sí.',
+ statPeers: 'participantes: {n}',
+ statNetwork: 'participantes: {p}, enlaces: {l}',
+ statSybil: 'los Sybil tienen {x}',
+ statNoSeeds: 'sin semillas, todos empiezan igual',
+ selScore: '{x}, puesto {r}',
+ noTrustYet: 'aún sin confianza',
+ isSeed: 'Es semilla',
+ makeSeed: 'Hacer semilla',
+ removePeer: 'Eliminar',
+ done: 'Listo',
+ trusts: 'Confía en ({n})',
+ trustsEmpty: 'Haz clic en otro participante del grafo para añadir confianza.',
+ addTrust: 'Añadir confianza',
+ trustedBy: 'Recibe confianza de ({n})',
+ nobody: 'Nadie todavía.',
+ remove: 'Quitar',
+ ariaShouldTrust: '{id} debe confiar en',
+ ariaTrust: 'Confianza de {a} en {b}',
+ ariaRemoveTrust: 'Quitar la confianza de {a} en {b}',
+ tooLarge: '{name}: {n} líneas, demasiado grande para editar aquí.',
+ rejected: 'El motor rechazó esta entrada: {e}',
+ generating: 'Generando {n} participantes',
+ rankingN: 'Clasificando {n} declaraciones de confianza',
+ benchResult: 'para leer {mb} MB de CSV y clasificar {n} participantes a partir de {e} declaraciones de confianza con α {a}. {mode}. Generar los datos llevó {g} ms.',
+ },
+ zh: {
+ title: 'EigenTrust 交互演示',
+ description: '构建信任网络,在浏览器中实时查看 EigenTrust 为每个节点排名。',
+ source: '源代码',
+ language: '语言',
+ h1: '信任从你已信任的节点流出。',
+ intro: '点击一个节点,再点击另一个节点,即可在它们之间添加信任。双击节点可将其设为种子。圆圈越大,信任越多。',
+ canvasLabel: '信任网络图。可使用面板通过键盘编辑。',
+ addPeer: '添加节点',
+ tidy: '重新布局',
+ copyLink: '复制链接',
+ linkCopied: '链接已复制',
+ copyFallback: '请从地址栏复制',
+ legendSeed: '种子节点',
+ legendScore: '信任分数',
+ legendEdge: '给出的信任',
+ tabNetwork: '网络',
+ tabCsv: 'CSV',
+ tabBench: '性能测试',
+ startFrom: '起始网络',
+ presetFriends: '一个小社区',
+ presetSybil: '女巫攻击',
+ presetChain: '推荐链',
+ presetEmpty: '空网络',
+ seedWeight: '种子权重',
+ alphaHint: '每一步将信任拉回种子的力度。数值越高,信任越集中在种子附近,女巫集群得到的信任越少;数值越低,信任传播得越远。',
+ ranking: '排名',
+ csvHint: '每行一条信任声明:from,to,weight。种子格式为 peer,weight。可以包含表头、引号和空格。',
+ localTrust: '局部信任',
+ upload: '上传',
+ seeds: '种子(预信任)',
+ loadNetwork: '载入网络',
+ busy: '计算中',
+ downloadScores: '下载分数',
+ benchHint: '在浏览器中生成随机网络并计算排名。数据不会离开你的设备。',
+ peers: '节点数',
+ perPeer: '每个节点的信任声明数',
+ benchRun: '生成并排名',
+ loadingWasm: '正在加载 WebAssembly',
+ running: '在浏览器中运行',
+ rankedIn: '排名用时 {ms} 毫秒,{mode}',
+ mode: 'WebAssembly,线程数:{n}',
+ noteLarge: '该网络有 {n} 个节点,无法全部绘制。完整排名见面板。选择一个起始网络即可返回图形视图。',
+ noteEmpty: '双击任意位置添加节点,或在面板中选择起始网络。',
+ noteNoLinks: '点击一个节点,再点击另一个节点,即可在它们之间添加信任。',
+ more: '另外 {n} 个',
+ noScores: '节点之间建立信任后即显示分数。',
+ statPeers: '节点:{n}',
+ statNetwork: '节点:{p},连接:{l}',
+ statSybil: '女巫节点占 {x}',
+ statNoSeeds: '没有种子,所有节点起点相同',
+ selScore: '{x},第 {r} 名',
+ noTrustYet: '尚无信任',
+ isSeed: '种子节点',
+ makeSeed: '设为种子',
+ removePeer: '删除节点',
+ done: '完成',
+ trusts: '信任({n})',
+ trustsEmpty: '在图中点击另一个节点即可添加信任。',
+ addTrust: '添加信任',
+ trustedBy: '被信任({n})',
+ nobody: '暂无。',
+ remove: '移除',
+ ariaShouldTrust: '{id} 应信任',
+ ariaTrust: '{a} 对 {b} 的信任',
+ ariaRemoveTrust: '移除 {a} 对 {b} 的信任',
+ tooLarge: '{name}:{n} 行,太大,无法在此编辑。',
+ rejected: '引擎拒绝了此输入:{e}',
+ generating: '正在生成 {n} 个节点',
+ rankingN: '正在对 {n} 条信任声明进行排名',
+ benchResult: '解析 {mb} MB 的 CSV,并根据 {e} 条信任声明对 {n} 个节点排名(α {a})。{mode}。生成数据用时 {g} 毫秒。',
+ },
+ hi: {
+ title: 'EigenTrust प्लेग्राउंड',
+ description: 'एक भरोसे का नेटवर्क बनाइए और देखिए कि EigenTrust आपके ब्राउज़र में हर पीयर को कैसे रैंक करता है।',
+ source: 'सोर्स कोड',
+ language: 'भाषा',
+ h1: 'भरोसा उन पीयर से बहता है जिन पर आप पहले से भरोसा करते हैं।',
+ intro: 'किसी पीयर पर क्लिक करें, फिर दूसरे पर, ताकि उनके बीच भरोसा जुड़ जाए। किसी पीयर को सीड बनाने के लिए उस पर डबल-क्लिक करें। बड़े वृत्तों के पास ज़्यादा भरोसा है।',
+ canvasLabel: 'भरोसे के नेटवर्क का ग्राफ़। कीबोर्ड से बदलाव के लिए पैनल का उपयोग करें।',
+ addPeer: 'पीयर जोड़ें',
+ tidy: 'लेआउट सुधारें',
+ copyLink: 'लिंक कॉपी करें',
+ linkCopied: 'लिंक कॉपी हो गया',
+ copyFallback: 'एड्रेस बार से कॉपी करें',
+ legendSeed: 'सीड पीयर',
+ legendScore: 'भरोसा स्कोर',
+ legendEdge: 'दिया गया भरोसा',
+ tabNetwork: 'नेटवर्क',
+ tabCsv: 'CSV',
+ tabBench: 'बेंचमार्क',
+ startFrom: 'शुरुआत करें',
+ presetFriends: 'एक छोटा समुदाय',
+ presetSybil: 'एक सिबिल हमला',
+ presetChain: 'सिफ़ारिशों की एक श्रृंखला',
+ presetEmpty: 'खाली नेटवर्क',
+ seedWeight: 'सीड का भार',
+ alphaHint: 'हर चरण में भरोसा कितनी मज़बूती से सीड की ओर लौटता है। ज़्यादा मान भरोसे को सीड के पास रखता है और सिबिल समूहों को भरोसे से वंचित करता है। कम मान उसे और दूर तक जाने देता है।',
+ ranking: 'रैंकिंग',
+ csvHint: 'हर पंक्ति में एक भरोसा कथन: from,to,weight। सीड का प्रारूप peer,weight है। हेडर, उद्धरण चिह्न और स्पेस चल जाते हैं।',
+ localTrust: 'स्थानीय भरोसा',
+ upload: 'अपलोड',
+ seeds: 'सीड (पूर्व-भरोसा)',
+ loadNetwork: 'नेटवर्क में लोड करें',
+ busy: 'गणना हो रही है',
+ downloadScores: 'स्कोर डाउनलोड करें',
+ benchHint: 'ब्राउज़र में एक रैंडम नेटवर्क बनाइए और उसे रैंक कीजिए। कुछ भी आपके डिवाइस से बाहर नहीं जाता।',
+ peers: 'पीयर',
+ perPeer: 'प्रति पीयर भरोसा कथन',
+ benchRun: 'बनाएँ और रैंक करें',
+ loadingWasm: 'WebAssembly लोड हो रहा है',
+ running: 'आपके ब्राउज़र में चल रहा है',
+ rankedIn: '{ms} ms में रैंक किया, {mode}',
+ mode: 'WebAssembly, थ्रेड: {n}',
+ noteLarge: 'इस नेटवर्क में {n} पीयर हैं, जो बनाने के लिए बहुत ज़्यादा हैं। पूरी रैंकिंग पैनल में है। ग्राफ़ पर लौटने के लिए कोई शुरुआती नेटवर्क चुनें।',
+ noteEmpty: 'पीयर जोड़ने के लिए कहीं भी डबल-क्लिक करें, या पैनल में कोई शुरुआती नेटवर्क चुनें।',
+ noteNoLinks: 'किसी पीयर पर क्लिक करें, फिर दूसरे पर, ताकि उनके बीच भरोसा जुड़ जाए।',
+ more: 'और {n}',
+ noScores: 'जब पीयर एक-दूसरे पर भरोसा करेंगे, तब स्कोर दिखेंगे।',
+ statPeers: 'पीयर: {n}',
+ statNetwork: 'पीयर: {p}, लिंक: {l}',
+ statSybil: 'सिबिल के पास {x}',
+ statNoSeeds: 'कोई सीड नहीं, सभी बराबर से शुरू करते हैं',
+ selScore: '{x}, रैंक {r}',
+ noTrustYet: 'अभी कोई भरोसा नहीं',
+ isSeed: 'सीड पीयर',
+ makeSeed: 'सीड बनाएँ',
+ removePeer: 'पीयर हटाएँ',
+ done: 'हो गया',
+ trusts: 'भरोसा करता है ({n})',
+ trustsEmpty: 'भरोसा जोड़ने के लिए ग्राफ़ में किसी दूसरे पीयर पर क्लिक करें।',
+ addTrust: 'भरोसा जोड़ें',
+ trustedBy: 'इन्हें भरोसा है ({n})',
+ nobody: 'अभी कोई नहीं।',
+ remove: 'हटाएँ',
+ ariaShouldTrust: '{id} किस पर भरोसा करे',
+ ariaTrust: '{a} का {b} पर भरोसा',
+ ariaRemoveTrust: '{a} का {b} पर भरोसा हटाएँ',
+ tooLarge: '{name}: {n} पंक्तियाँ, यहाँ संपादित करने के लिए बहुत बड़ी।',
+ rejected: 'इंजन ने यह इनपुट अस्वीकार किया: {e}',
+ generating: '{n} पीयर बनाए जा रहे हैं',
+ rankingN: '{n} भरोसा कथनों की रैंकिंग हो रही है',
+ benchResult: '{mb} MB CSV पढ़ने और {e} भरोसा कथनों से {n} पीयर को रैंक करने में (α {a})। {mode}। डेटा बनाने में {g} ms लगे।',
+ },
+ ar: {
+ title: 'ساحة تجربة EigenTrust',
+ description: 'أنشئ شبكة ثقة وشاهد كيف يرتّب EigenTrust كل عقدة مباشرةً في متصفحك.',
+ source: 'الشيفرة المصدرية',
+ language: 'اللغة',
+ h1: 'تتدفق الثقة من العقد التي تثق بها أصلًا.',
+ intro: 'انقر على عقدة ثم على أخرى لإضافة ثقة بينهما. انقر نقرًا مزدوجًا على عقدة لتجعلها بذرة. الدوائر الأكبر تحمل ثقة أكبر.',
+ canvasLabel: 'رسم بياني لشبكة الثقة. استخدم اللوحة لتعديله بلوحة المفاتيح.',
+ addPeer: 'إضافة عقدة',
+ tidy: 'إعادة الترتيب',
+ copyLink: 'نسخ الرابط',
+ linkCopied: 'تم نسخ الرابط',
+ copyFallback: 'انسخه من شريط العنوان',
+ legendSeed: 'عقدة بذرة',
+ legendScore: 'درجة الثقة',
+ legendEdge: 'ثقة ممنوحة',
+ tabNetwork: 'الشبكة',
+ tabCsv: 'CSV',
+ tabBench: 'اختبار الأداء',
+ startFrom: 'ابدأ من',
+ presetFriends: 'مجتمع صغير',
+ presetSybil: 'هجوم سيبيل',
+ presetChain: 'سلسلة توصيات',
+ presetEmpty: 'شبكة فارغة',
+ seedWeight: 'وزن البذور',
+ alphaHint: 'مدى قوة إعادة كل خطوة للثقة إلى البذور. القيمة الأعلى تُبقي الثقة قريبة منها وتحرم مجموعات سيبيل منها، والقيمة الأدنى تسمح لها بالانتقال أبعد.',
+ ranking: 'الترتيب',
+ csvHint: 'عبارة ثقة واحدة في كل سطر: from,to,weight. البذور بصيغة peer,weight. العناوين وعلامات الاقتباس والمسافات مقبولة.',
+ localTrust: 'الثقة المحلية',
+ upload: 'رفع',
+ seeds: 'البذور (ثقة مسبقة)',
+ loadNetwork: 'تحميل في الشبكة',
+ busy: 'جارٍ الحساب',
+ downloadScores: 'تنزيل الدرجات',
+ benchHint: 'أنشئ شبكة عشوائية في المتصفح ورتّبها. لا يغادر شيء جهازك.',
+ peers: 'العقد',
+ perPeer: 'عبارات الثقة لكل عقدة',
+ benchRun: 'إنشاء وترتيب',
+ loadingWasm: 'جارٍ تحميل WebAssembly',
+ running: 'يعمل في متصفحك',
+ rankedIn: 'تم الترتيب في {ms} ms، {mode}',
+ mode: 'WebAssembly، الخيوط: {n}',
+ noteLarge: 'تضم هذه الشبكة {n} عقدة، وهذا أكثر من أن يُرسم. الترتيب الكامل في اللوحة. اختر شبكة بداية للعودة إلى الرسم.',
+ noteEmpty: 'انقر نقرًا مزدوجًا في أي مكان لإضافة عقدة، أو اختر شبكة بداية من اللوحة.',
+ noteNoLinks: 'انقر على عقدة ثم على أخرى لإضافة ثقة بينهما.',
+ more: 'و{n} أخرى',
+ noScores: 'تظهر الدرجات عندما تثق العقد ببعضها.',
+ statPeers: 'العقد: {n}',
+ statNetwork: 'العقد: {p}، الروابط: {l}',
+ statSybil: 'نصيب سيبيل {x}',
+ statNoSeeds: 'لا توجد بذور، الجميع يبدأ بالتساوي',
+ selScore: '{x}، المرتبة {r}',
+ noTrustYet: 'لا ثقة بعد',
+ isSeed: 'عقدة بذرة',
+ makeSeed: 'اجعلها بذرة',
+ removePeer: 'حذف العقدة',
+ done: 'تم',
+ trusts: 'تثق بـ ({n})',
+ trustsEmpty: 'انقر على عقدة أخرى في الرسم لإضافة ثقة.',
+ addTrust: 'إضافة ثقة',
+ trustedBy: 'يثق بها ({n})',
+ nobody: 'لا أحد بعد.',
+ remove: 'إزالة',
+ ariaShouldTrust: 'العقدة {id} يجب أن تثق بـ',
+ ariaTrust: 'ثقة {a} بـ {b}',
+ ariaRemoveTrust: 'إزالة ثقة {a} بـ {b}',
+ tooLarge: '{name}: {n} سطر، أكبر من أن يُحرَّر هنا.',
+ rejected: 'رفض المحرك هذا الإدخال: {e}',
+ generating: 'جارٍ إنشاء {n} عقدة',
+ rankingN: 'جارٍ ترتيب {n} عبارة ثقة',
+ benchResult: 'لقراءة {mb} MB من CSV وترتيب {n} عقدة من {e} عبارة ثقة بقيمة α {a}. {mode}. استغرق إنشاء البيانات {g} ms.',
+ },
+ pt: {
+ title: 'EigenTrust interativo',
+ description: 'Monte uma rede de confiança e veja o EigenTrust classificar cada participante, ao vivo no seu navegador.',
+ source: 'Código',
+ language: 'Idioma',
+ h1: 'A confiança flui de quem você já confia.',
+ intro: 'Clique em um participante e depois em outro para adicionar confiança entre eles. Clique duas vezes em um participante para torná-lo semente. Círculos maiores têm mais confiança.',
+ canvasLabel: 'Grafo da rede de confiança. Use o painel para editá-lo pelo teclado.',
+ addPeer: 'Adicionar participante',
+ tidy: 'Reorganizar',
+ copyLink: 'Copiar link',
+ linkCopied: 'Link copiado',
+ copyFallback: 'Copie pela barra de endereço',
+ legendSeed: 'Semente',
+ legendScore: 'Pontuação de confiança',
+ legendEdge: 'Confiança dada',
+ tabNetwork: 'Rede',
+ tabCsv: 'CSV',
+ tabBench: 'Desempenho',
+ startFrom: 'Começar com',
+ presetFriends: 'Uma comunidade pequena',
+ presetSybil: 'Um ataque Sybil',
+ presetChain: 'Uma cadeia de indicações',
+ presetEmpty: 'Uma rede vazia',
+ seedWeight: 'Peso das sementes',
+ alphaHint: 'Quanto cada passo puxa a confiança de volta para as sementes. Valores altos a mantêm perto delas e deixam grupos Sybil sem confiança. Valores baixos a deixam ir mais longe.',
+ ranking: 'Classificação',
+ csvHint: 'Uma declaração de confiança por linha: from,to,weight. Sementes são peer,weight. Cabeçalhos, aspas e espaços são aceitos.',
+ localTrust: 'Confiança local',
+ upload: 'Enviar',
+ seeds: 'Sementes (pré-confiança)',
+ loadNetwork: 'Carregar na rede',
+ busy: 'Calculando',
+ downloadScores: 'Baixar pontuações',
+ benchHint: 'Gere uma rede aleatória no navegador e classifique-a. Nada sai do seu computador.',
+ peers: 'Participantes',
+ perPeer: 'Declarações de confiança por participante',
+ benchRun: 'Gerar e classificar',
+ loadingWasm: 'Carregando WebAssembly',
+ running: 'Rodando no seu navegador',
+ rankedIn: 'Classificado em {ms} ms, {mode}',
+ mode: 'WebAssembly, threads: {n}',
+ noteLarge: 'Esta rede tem {n} participantes, demais para desenhar. A classificação completa está no painel. Escolha uma rede inicial para voltar ao grafo.',
+ noteEmpty: 'Clique duas vezes em qualquer lugar para adicionar um participante, ou escolha uma rede inicial no painel.',
+ noteNoLinks: 'Clique em um participante e depois em outro para adicionar confiança entre eles.',
+ more: 'e mais {n}',
+ noScores: 'As pontuações aparecem quando os participantes confiam uns nos outros.',
+ statPeers: 'participantes: {n}',
+ statNetwork: 'participantes: {p}, ligações: {l}',
+ statSybil: 'os Sybil têm {x}',
+ statNoSeeds: 'sem sementes, todos começam iguais',
+ selScore: '{x}, posição {r}',
+ noTrustYet: 'ainda sem confiança',
+ isSeed: 'É semente',
+ makeSeed: 'Tornar semente',
+ removePeer: 'Remover',
+ done: 'Pronto',
+ trusts: 'Confia em ({n})',
+ trustsEmpty: 'Clique em outro participante no grafo para adicionar confiança.',
+ addTrust: 'Adicionar confiança',
+ trustedBy: 'Recebe confiança de ({n})',
+ nobody: 'Ninguém ainda.',
+ remove: 'Remover',
+ ariaShouldTrust: '{id} deve confiar em',
+ ariaTrust: 'Confiança de {a} em {b}',
+ ariaRemoveTrust: 'Remover a confiança de {a} em {b}',
+ tooLarge: '{name}: {n} linhas, grande demais para editar aqui.',
+ rejected: 'O motor rejeitou esta entrada: {e}',
+ generating: 'Gerando {n} participantes',
+ rankingN: 'Classificando {n} declarações de confiança',
+ benchResult: 'para ler {mb} MB de CSV e classificar {n} participantes a partir de {e} declarações de confiança com α {a}. {mode}. Gerar os dados levou {g} ms.',
+ },
+ fr: {
+ title: 'EigenTrust interactif',
+ description: 'Construisez un réseau de confiance et regardez EigenTrust classer chaque pair, en direct dans votre navigateur.',
+ source: 'Code source',
+ language: 'Langue',
+ h1: 'La confiance part des pairs en qui vous avez déjà confiance.',
+ intro: 'Cliquez sur un pair, puis sur un autre pour ajouter de la confiance entre eux. Double-cliquez sur un pair pour en faire une graine. Plus le cercle est grand, plus la confiance est forte.',
+ canvasLabel: 'Graphe du réseau de confiance. Utilisez le panneau pour le modifier au clavier.',
+ addPeer: 'Ajouter un pair',
+ tidy: 'Réorganiser',
+ copyLink: 'Copier le lien',
+ linkCopied: 'Lien copié',
+ copyFallback: 'Copiez-le depuis la barre d’adresse',
+ legendSeed: 'Pair graine',
+ legendScore: 'Score de confiance',
+ legendEdge: 'Confiance accordée',
+ tabNetwork: 'Réseau',
+ tabCsv: 'CSV',
+ tabBench: 'Performances',
+ startFrom: 'Partir de',
+ presetFriends: 'Une petite communauté',
+ presetSybil: 'Une attaque Sybil',
+ presetChain: 'Une chaîne de recommandations',
+ presetEmpty: 'Un réseau vide',
+ seedWeight: 'Poids des graines',
+ alphaHint: 'La force avec laquelle chaque étape ramène la confiance vers les graines. Plus élevé, la confiance reste près d’elles et les groupes Sybil en sont privés. Plus bas, elle va plus loin.',
+ ranking: 'Classement',
+ csvHint: 'Une déclaration de confiance par ligne : from,to,weight. Les graines sont peer,weight. En-têtes, guillemets et espaces acceptés.',
+ localTrust: 'Confiance locale',
+ upload: 'Importer',
+ seeds: 'Graines (pré-confiance)',
+ loadNetwork: 'Charger dans le réseau',
+ busy: 'Calcul en cours',
+ downloadScores: 'Télécharger les scores',
+ benchHint: 'Générez un réseau aléatoire dans le navigateur et classez-le. Rien ne quitte votre machine.',
+ peers: 'Pairs',
+ perPeer: 'Déclarations de confiance par pair',
+ benchRun: 'Générer et classer',
+ loadingWasm: 'Chargement de WebAssembly',
+ running: 'S’exécute dans votre navigateur',
+ rankedIn: 'Classé en {ms} ms, {mode}',
+ mode: 'WebAssembly, threads : {n}',
+ noteLarge: 'Ce réseau compte {n} pairs, trop pour être dessiné. Le classement complet est dans le panneau. Choisissez un réseau de départ pour revenir au graphe.',
+ noteEmpty: 'Double-cliquez n’importe où pour ajouter un pair, ou choisissez un réseau de départ dans le panneau.',
+ noteNoLinks: 'Cliquez sur un pair, puis sur un autre pour ajouter de la confiance entre eux.',
+ more: 'et {n} de plus',
+ noScores: 'Les scores apparaissent dès que des pairs se font confiance.',
+ statPeers: 'pairs : {n}',
+ statNetwork: 'pairs : {p}, liens : {l}',
+ statSybil: 'les Sybil détiennent {x}',
+ statNoSeeds: 'aucune graine, tout le monde part à égalité',
+ selScore: '{x}, rang {r}',
+ noTrustYet: 'pas encore de confiance',
+ isSeed: 'Pair graine',
+ makeSeed: 'En faire une graine',
+ removePeer: 'Supprimer',
+ done: 'Terminé',
+ trusts: 'Fait confiance à ({n})',
+ trustsEmpty: 'Cliquez sur un autre pair du graphe pour ajouter de la confiance.',
+ addTrust: 'Ajouter',
+ trustedBy: 'A la confiance de ({n})',
+ nobody: 'Personne pour l’instant.',
+ remove: 'Retirer',
+ ariaShouldTrust: '{id} doit faire confiance à',
+ ariaTrust: 'Confiance de {a} envers {b}',
+ ariaRemoveTrust: 'Retirer la confiance de {a} envers {b}',
+ tooLarge: '{name} : {n} lignes, trop volumineux pour être modifié ici.',
+ rejected: 'Le moteur a rejeté cette entrée : {e}',
+ generating: 'Génération de {n} pairs',
+ rankingN: 'Classement de {n} déclarations de confiance',
+ benchResult: 'pour lire {mb} Mo de CSV et classer {n} pairs à partir de {e} déclarations de confiance avec α {a}. {mode}. La génération des données a pris {g} ms.',
+ },
+ de: {
+ title: 'EigenTrust zum Ausprobieren',
+ description: 'Baue ein Vertrauensnetz und sieh zu, wie EigenTrust jeden Teilnehmer live im Browser bewertet.',
+ source: 'Quellcode',
+ language: 'Sprache',
+ h1: 'Vertrauen fließt von denen, denen du schon vertraust.',
+ intro: 'Klicke auf einen Teilnehmer und dann auf einen anderen, um Vertrauen zwischen ihnen hinzuzufügen. Doppelklicke auf einen Teilnehmer, um ihn zum Startpunkt zu machen. Größere Kreise haben mehr Vertrauen.',
+ canvasLabel: 'Graph des Vertrauensnetzes. Mit dem Seitenbereich lässt er sich per Tastatur bearbeiten.',
+ addPeer: 'Teilnehmer hinzufügen',
+ tidy: 'Neu anordnen',
+ copyLink: 'Link kopieren',
+ linkCopied: 'Link kopiert',
+ copyFallback: 'Aus der Adressleiste kopieren',
+ legendSeed: 'Startpunkt',
+ legendScore: 'Vertrauenswert',
+ legendEdge: 'Vergebenes Vertrauen',
+ tabNetwork: 'Netz',
+ tabCsv: 'CSV',
+ tabBench: 'Benchmark',
+ startFrom: 'Ausgangsnetz',
+ presetFriends: 'Eine kleine Gemeinschaft',
+ presetSybil: 'Ein Sybil-Angriff',
+ presetChain: 'Eine Empfehlungskette',
+ presetEmpty: 'Ein leeres Netz',
+ seedWeight: 'Gewicht der Startpunkte',
+ alphaHint: 'Wie stark jeder Schritt das Vertrauen zu den Startpunkten zurückzieht. Ein hoher Wert hält es nah bei ihnen und lässt Sybil-Gruppen leer ausgehen. Ein niedriger Wert lässt es weiter wandern.',
+ ranking: 'Rangliste',
+ csvHint: 'Eine Vertrauensangabe pro Zeile: from,to,weight. Startpunkte: peer,weight. Kopfzeilen, Anführungszeichen und Leerzeichen sind kein Problem.',
+ localTrust: 'Lokales Vertrauen',
+ upload: 'Hochladen',
+ seeds: 'Startpunkte (Vorvertrauen)',
+ loadNetwork: 'Ins Netz laden',
+ busy: 'Wird berechnet',
+ downloadScores: 'Werte herunterladen',
+ benchHint: 'Erzeuge im Browser ein zufälliges Netz und bewerte es. Nichts verlässt deinen Rechner.',
+ peers: 'Teilnehmer',
+ perPeer: 'Vertrauensangaben pro Teilnehmer',
+ benchRun: 'Erzeugen und bewerten',
+ loadingWasm: 'WebAssembly wird geladen',
+ running: 'Läuft in deinem Browser',
+ rankedIn: 'Bewertet in {ms} ms, {mode}',
+ mode: 'WebAssembly, Threads: {n}',
+ noteLarge: 'Dieses Netz hat {n} Teilnehmer, zu viele zum Zeichnen. Die vollständige Rangliste steht im Seitenbereich. Wähle ein Ausgangsnetz, um zum Graphen zurückzukehren.',
+ noteEmpty: 'Doppelklicke irgendwo, um einen Teilnehmer hinzuzufügen, oder wähle im Seitenbereich ein Ausgangsnetz.',
+ noteNoLinks: 'Klicke auf einen Teilnehmer und dann auf einen anderen, um Vertrauen zwischen ihnen hinzuzufügen.',
+ more: 'und {n} weitere',
+ noScores: 'Werte erscheinen, sobald sich Teilnehmer vertrauen.',
+ statPeers: 'Teilnehmer: {n}',
+ statNetwork: 'Teilnehmer: {p}, Verbindungen: {l}',
+ statSybil: 'Sybils halten {x}',
+ statNoSeeds: 'keine Startpunkte, alle starten gleich',
+ selScore: '{x}, Platz {r}',
+ noTrustYet: 'noch kein Vertrauen',
+ isSeed: 'Startpunkt',
+ makeSeed: 'Zum Startpunkt machen',
+ removePeer: 'Entfernen',
+ done: 'Fertig',
+ trusts: 'Vertraut ({n})',
+ trustsEmpty: 'Klicke im Graphen auf einen anderen Teilnehmer, um Vertrauen hinzuzufügen.',
+ addTrust: 'Vertrauen hinzufügen',
+ trustedBy: 'Vertraut von ({n})',
+ nobody: 'Noch niemand.',
+ remove: 'Entfernen',
+ ariaShouldTrust: '{id} soll vertrauen',
+ ariaTrust: 'Vertrauen von {a} zu {b}',
+ ariaRemoveTrust: 'Vertrauen von {a} zu {b} entfernen',
+ tooLarge: '{name}: {n} Zeilen, zu groß zum Bearbeiten.',
+ rejected: 'Die Engine hat diese Eingabe abgelehnt: {e}',
+ generating: '{n} Teilnehmer werden erzeugt',
+ rankingN: '{n} Vertrauensangaben werden bewertet',
+ benchResult: 'um {mb} MB CSV zu lesen und {n} Teilnehmer aus {e} Vertrauensangaben mit α {a} zu bewerten. {mode}. Das Erzeugen der Daten dauerte {g} ms.',
+ },
+ ru: {
+ title: 'Песочница EigenTrust',
+ description: 'Постройте сеть доверия и посмотрите, как EigenTrust ранжирует каждого участника прямо в браузере.',
+ source: 'Исходный код',
+ language: 'Язык',
+ h1: 'Доверие исходит от тех, кому вы уже доверяете.',
+ intro: 'Нажмите на участника, затем на другого, чтобы добавить доверие между ними. Двойной щелчок делает участника опорным. Чем больше круг, тем больше доверия.',
+ canvasLabel: 'Граф сети доверия. Для редактирования с клавиатуры используйте панель.',
+ addPeer: 'Добавить участника',
+ tidy: 'Упорядочить',
+ copyLink: 'Скопировать ссылку',
+ linkCopied: 'Ссылка скопирована',
+ copyFallback: 'Скопируйте из адресной строки',
+ legendSeed: 'Опорный участник',
+ legendScore: 'Уровень доверия',
+ legendEdge: 'Выданное доверие',
+ tabNetwork: 'Сеть',
+ tabCsv: 'CSV',
+ tabBench: 'Бенчмарк',
+ startFrom: 'Начать с',
+ presetFriends: 'Небольшое сообщество',
+ presetSybil: 'Атака Сивиллы',
+ presetChain: 'Цепочка рекомендаций',
+ presetEmpty: 'Пустая сеть',
+ seedWeight: 'Вес опорных участников',
+ alphaHint: 'Насколько сильно каждый шаг возвращает доверие к опорным участникам. Высокое значение удерживает доверие рядом с ними и лишает его группы сивилл. Низкое позволяет ему уходить дальше.',
+ ranking: 'Рейтинг',
+ csvHint: 'Одно утверждение о доверии на строку: from,to,weight. Опорные участники: peer,weight. Заголовки, кавычки и пробелы допускаются.',
+ localTrust: 'Локальное доверие',
+ upload: 'Загрузить',
+ seeds: 'Опорные участники (предварительное доверие)',
+ loadNetwork: 'Загрузить в сеть',
+ busy: 'Считаем',
+ downloadScores: 'Скачать оценки',
+ benchHint: 'Сгенерируйте случайную сеть в браузере и ранжируйте её. Данные не покидают ваше устройство.',
+ peers: 'Участники',
+ perPeer: 'Утверждений о доверии на участника',
+ benchRun: 'Сгенерировать и ранжировать',
+ loadingWasm: 'Загрузка WebAssembly',
+ running: 'Работает в вашем браузере',
+ rankedIn: 'Ранжировано за {ms} мс, {mode}',
+ mode: 'WebAssembly, потоков: {n}',
+ noteLarge: 'Участников в этой сети: {n}. Это слишком много для отрисовки. Полный рейтинг на панели. Выберите начальную сеть, чтобы вернуться к графу.',
+ noteEmpty: 'Дважды щёлкните в любом месте, чтобы добавить участника, или выберите начальную сеть на панели.',
+ noteNoLinks: 'Нажмите на участника, затем на другого, чтобы добавить доверие между ними.',
+ more: 'и ещё {n}',
+ noScores: 'Оценки появятся, когда участники начнут доверять друг другу.',
+ statPeers: 'участников: {n}',
+ statNetwork: 'участников: {p}, связей: {l}',
+ statSybil: 'у сивилл {x}',
+ statNoSeeds: 'нет опорных участников, все начинают на равных',
+ selScore: '{x}, место {r}',
+ noTrustYet: 'доверия пока нет',
+ isSeed: 'Опорный',
+ makeSeed: 'Сделать опорным',
+ removePeer: 'Удалить',
+ done: 'Готово',
+ trusts: 'Доверяет ({n})',
+ trustsEmpty: 'Нажмите на другого участника на графе, чтобы добавить доверие.',
+ addTrust: 'Добавить доверие',
+ trustedBy: 'Ему доверяют ({n})',
+ nobody: 'Пока никто.',
+ remove: 'Убрать',
+ ariaShouldTrust: 'Кому доверяет {id}',
+ ariaTrust: 'Доверие {a} к {b}',
+ ariaRemoveTrust: 'Убрать доверие {a} к {b}',
+ tooLarge: '{name}: строк: {n}, слишком большой файл для редактирования здесь.',
+ rejected: 'Движок отклонил эти данные: {e}',
+ generating: 'Генерируем участников: {n}',
+ rankingN: 'Ранжируем утверждений о доверии: {n}',
+ benchResult: 'на чтение {mb} МБ CSV и ранжирование участников ({n}) по утверждениям о доверии ({e}) при α {a}. {mode}. Генерация данных заняла {g} мс.',
+ },
+ ja: {
+ title: 'EigenTrust プレイグラウンド',
+ description: '信頼ネットワークを作って、EigenTrust がすべてのピアをブラウザ上でリアルタイムにランク付けする様子を見てみましょう。',
+ source: 'ソースコード',
+ language: '言語',
+ h1: '信頼は、すでに信頼しているピアから流れ出す。',
+ intro: 'ピアをクリックしてから別のピアをクリックすると、2つの間に信頼が追加されます。ダブルクリックでピアをシードにできます。円が大きいほど信頼が高いことを示します。',
+ canvasLabel: '信頼ネットワークのグラフ。キーボードで編集するにはパネルを使ってください。',
+ addPeer: 'ピアを追加',
+ tidy: 'レイアウトを整える',
+ copyLink: 'リンクをコピー',
+ linkCopied: 'コピーしました',
+ copyFallback: 'アドレスバーからコピーしてください',
+ legendSeed: 'シードピア',
+ legendScore: '信頼スコア',
+ legendEdge: '与えた信頼',
+ tabNetwork: 'ネットワーク',
+ tabCsv: 'CSV',
+ tabBench: 'ベンチマーク',
+ startFrom: '初期ネットワーク',
+ presetFriends: '小さなコミュニティ',
+ presetSybil: 'シビル攻撃',
+ presetChain: '紹介の連鎖',
+ presetEmpty: '空のネットワーク',
+ seedWeight: 'シードの重み',
+ alphaHint: '各ステップで信頼をシードへ引き戻す強さです。高いほど信頼はシードの近くにとどまり、シビルの集団には届きません。低いほど遠くまで広がります。',
+ ranking: 'ランキング',
+ csvHint: '1行に1つの信頼: from,to,weight。シードは peer,weight。ヘッダー、引用符、空白があっても問題ありません。',
+ localTrust: 'ローカル信頼',
+ upload: 'アップロード',
+ seeds: 'シード(事前信頼)',
+ loadNetwork: 'ネットワークに読み込む',
+ busy: '計算中',
+ downloadScores: 'スコアをダウンロード',
+ benchHint: 'ブラウザでランダムなネットワークを生成してランク付けします。データが端末の外に出ることはありません。',
+ peers: 'ピア数',
+ perPeer: 'ピアあたりの信頼の数',
+ benchRun: '生成してランク付け',
+ loadingWasm: 'WebAssembly を読み込み中',
+ running: 'ブラウザで動作中',
+ rankedIn: '{ms} ms でランク付け、{mode}',
+ mode: 'WebAssembly、スレッド数: {n}',
+ noteLarge: 'このネットワークには {n} 個のピアがあり、描画するには多すぎます。全体のランキングはパネルにあります。初期ネットワークを選ぶとグラフに戻ります。',
+ noteEmpty: '好きな場所をダブルクリックしてピアを追加するか、パネルで初期ネットワークを選んでください。',
+ noteNoLinks: 'ピアをクリックしてから別のピアをクリックすると、2つの間に信頼が追加されます。',
+ more: 'ほか {n} 件',
+ noScores: 'ピア同士が信頼し合うとスコアが表示されます。',
+ statPeers: 'ピア: {n}',
+ statNetwork: 'ピア: {p}、リンク: {l}',
+ statSybil: 'シビルの取り分 {x}',
+ statNoSeeds: 'シードなし、全員が同じ条件で開始',
+ selScore: '{x}、{r} 位',
+ noTrustYet: 'まだ信頼なし',
+ isSeed: 'シードピア',
+ makeSeed: 'シードにする',
+ removePeer: 'ピアを削除',
+ done: '完了',
+ trusts: '信頼している({n})',
+ trustsEmpty: 'グラフで別のピアをクリックすると信頼を追加できます。',
+ addTrust: '信頼を追加',
+ trustedBy: '信頼されている({n})',
+ nobody: 'まだいません。',
+ remove: '削除',
+ ariaShouldTrust: '{id} が信頼する相手',
+ ariaTrust: '{a} から {b} への信頼',
+ ariaRemoveTrust: '{a} から {b} への信頼を削除',
+ tooLarge: '{name}: {n} 行。ここで編集するには大きすぎます。',
+ rejected: 'エンジンがこの入力を受け付けませんでした: {e}',
+ generating: '{n} 個のピアを生成中',
+ rankingN: '{n} 件の信頼をランク付け中',
+ benchResult: '{mb} MB の CSV を読み込み、{e} 件の信頼から {n} 個のピアをランク付け(α {a})。{mode}。データ生成には {g} ms かかりました。',
+ },
+}
+
+const STORE_KEY = 'eigentrust-lang'
+
+export function detectLanguage() {
+ const param = new URLSearchParams(location.search).get('lang')
+ if (param && STRINGS[param]) return param
+ try {
+ const saved = localStorage.getItem(STORE_KEY)
+ if (saved && STRINGS[saved]) return saved
+ } catch {}
+ for (const tag of navigator.languages || [navigator.language]) {
+ const base = String(tag).toLowerCase().split('-')[0]
+ if (STRINGS[base]) return base
+ }
+ return 'en'
+}
+
+export function saveLanguage(lang) {
+ try { localStorage.setItem(STORE_KEY, lang) } catch {}
+}
+
+let current = 'en'
+
+export const lang = () => current
+
+export function setLanguage(code) {
+ current = STRINGS[code] ? code : 'en'
+ document.documentElement.lang = current
+ document.documentElement.dir = RTL.has(current) ? 'rtl' : 'ltr'
+ applyDom()
+}
+
+// t('key', { n: 3 }) with {placeholders}; missing keys fall back to English
+export function t(key, vars = {}) {
+ const s = STRINGS[current][key] ?? STRINGS.en[key] ?? key
+ return s.replace(/\{(\w+)\}/g, (_, k) => (vars[k] ?? `{${k}}`))
+}
+
+export const num = (n, opts) => Number(n).toLocaleString(current, opts)
+
+function applyDom() {
+ document.title = t('title')
+ document.querySelector('meta[name="description"]')?.setAttribute('content', t('description'))
+ for (const el of document.querySelectorAll('[data-i18n]')) el.textContent = t(el.dataset.i18n)
+ for (const el of document.querySelectorAll('[data-i18n-html]')) el.innerHTML = t(el.dataset.i18nHtml)
+ for (const el of document.querySelectorAll('[data-i18n-aria]')) el.setAttribute('aria-label', t(el.dataset.i18nAria))
+}
diff --git a/demo/index.html b/demo/index.html
index bd67411..8ae0005 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -20,94 +20,102 @@
EigenTrust
-
Trust flows from the peers you already trust.
-
Click a peer, then click another to add trust between them. Double-click a peer to make it a seed. Bigger circles hold more trust.
+
Trust flows from the peers you already trust.
+
Click a peer, then click another to add trust between them. Double-click a peer to make it a seed. Bigger circles hold more trust.
-
+
-
-
-
+
+
+
- Seed peer
- Trust score
- Trust given
+ Seed peer
+ Trust score
+ Trust given