From 3032399aeac4d055b97eeff122284def7733a650 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 10 Oct 2026 08:56:02 -0700 Subject: Italiano. coinos speaks Italian: all 1,445 strings (the fullest of the locales — the others carry 923 or fewer and fall back to English), placeholders checked, informal "tu", wallet = portafoglio. An Italian phone now opens coinos in Italian, and posts translate into Italian because that's the app's language. The "phone language" translation fallback reads the app's own LANGS (no second list), and its test moved to a Dutch phone: tools/translate-phone-lang-test.js. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3 --- tools/translate-italian-test.js | 101 ------------------------------------- tools/translate-phone-lang-test.js | 101 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 101 deletions(-) delete mode 100644 tools/translate-italian-test.js create mode 100644 tools/translate-phone-lang-test.js diff --git a/tools/translate-italian-test.js b/tools/translate-italian-test.js deleted file mode 100644 index ab26e48..0000000 --- a/tools/translate-italian-test.js +++ /dev/null @@ -1,101 +0,0 @@ -// Translating into the phone's language: isolabellart's phone is Italian, -// coinos has no Italian UI (so it ran in English), and posts were never -// translated for them — the target was the app's language, and the guess -// didn't know Italian. Now an English post is translated into Italian on an -// Italian phone; an Italian one is left alone; Settings → Translation names -// the target and takes a DeepL key, which then rides with each request. -// The registrar's /translate is stubbed (window.fetch shim). -// Run: bun tools/translate-italian-test.js -import puppeteer from './puppeteer.js'; -import { cacheKeyFor } from '../src/wallet.js'; -import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; -import { buildHtml } from '../build.js'; -const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); -let ok = true; -const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; -const AUTHOR = (() => { const sk = generateSecretKey(); return { sk, pk: getPublicKey(sk) }; })(); -const now = Math.floor(Date.now() / 1000); -const post = (content, ago) => finalizeEvent({ kind: 1, created_at: now - ago, tags: [], content }, AUTHOR.sk); -const EN = post('I think that the market is going to be very busy today and you should come with us', 100); -const IT = post('Penso che il mercato sarà molto affollato oggi e anche tu dovresti venire con noi per il caffè', 200); -const EN2 = post('This is another post in English that is not yet translated for you and the others', 300); -const ANSWERS = { - [EN.content]: { text: 'Penso che il mercato sarà molto affollato oggi e dovresti venire con noi', source: 'en' }, - [EN2.content]: { text: 'Questo è un altro post in inglese', source: 'en' }, -}; -const html = await buildHtml({ minify: true, pwa: false }); -const server = Bun.serve({ port: 5317, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); -const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); -const page = await browser.newPage(); -await page.evaluateOnNewDocument((answers) => { - Object.defineProperty(navigator, 'languages', { get: () => ['it-IT', 'it', 'en'] }); - Object.defineProperty(navigator, 'language', { get: () => 'it-IT' }); - window.__tr = []; - try { delete self.Translator; } catch {} - const real = window.fetch; - window.fetch = async (url, opts) => { - if (String(url) === 'https://names.coinos.io/translate') { - const body = JSON.parse(opts.body); - window.__tr.push({ ...body, deepl: (opts.headers || {})['x-deepl-key'] || null }); - const a = answers[body.text]; - return new Response(JSON.stringify(a || { error: 'nope' }), { status: a ? 200 : 503, headers: { 'content-type': 'application/json' } }); - } - return real(url, opts); - }; - const Real = window.WebSocket; - window.WebSocket = function (...a) { const ws = new Real(...a); const send = ws.send.bind(ws); ws.send = (d) => { try { const m = JSON.parse(d); if (m[0] === 'REQ') { setTimeout(() => ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EOSE', m[1]]) })), 0); return; } if (m[0] === 'EVENT') return; } catch {} return send(d); }; return ws; }; - window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, Real); -}, ANSWERS); -const errs = []; -page.on('pageerror', (e) => errs.push(String(e))); -const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); -const text = () => page.evaluate(() => document.body.innerText); -const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms / 250; i++) { if ((await text()).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); } return false; }; -try { - await page.setViewport({ width: 390, height: 844 }); - await page.goto('http://localhost:5317/', { waitUntil: 'domcontentloaded' }); - await page.evaluate(() => { localStorage.setItem('btc-wallet-network', 'regtest'); localStorage.setItem('btc-wallet-lang', 'en'); }); - await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400); - await click('create a new wallet'); await sleep(600); - await page.waitForSelector('.words .w .t'); - const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' '); - await click('skip verification'); - await waitText('receive', 20000); - const base = cacheKeyFor(mn + '\n'); - await page.evaluate(([k, pk, ns]) => { - localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', pk]], c: '', at: Math.floor(Date.now() / 1000) })); - localStorage.setItem(k + ':feedNotes', JSON.stringify(ns)); - localStorage.setItem(k + ':profiles', JSON.stringify({ [pk]: { name: 'Mixed', t: Date.now() } })); - localStorage.setItem('btc-wallet-feed:' + k, 'following'); - }, [base, AUTHOR.pk, [EN, IT]]); - await page.reload({ waitUntil: 'domcontentloaded' }); - await waitText('receive', 20000); - await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]')?.click()); - - console.log('\n[an Italian phone, coinos in English]'); - check('the English post is shown in Italian', await waitText('Penso che il mercato sarà molto affollato oggi e dovresti', 15000)); - const asked = await page.evaluate(() => window.__tr); - check('...asked of the registrar with target it', asked.some((b) => b.target === 'it' && b.text === EN.content), JSON.stringify(asked.map((b) => b.target))); - check('the Italian post is left alone', !asked.some((b) => b.text === IT.content)); - - console.log('\n[Settings → Translation, with a DeepL key]'); - await page.evaluate(() => { const b = [...document.querySelectorAll('button, a')].find((x) => x.getAttribute('aria-label') === 'Settings' || x.title === 'Settings'); b && b.click(); }); - await sleep(800); - await click('nostr'); await sleep(1000); - check('the card names Italian as the target', await page.evaluate(() => { const s = document.querySelector('.translate-to'); return !!s && s.value === 'it'; })); - await page.evaluate(() => document.querySelector('.deepl-key')?.scrollIntoView()); - await page.type('.deepl-key', 'abcdef01-2345-6789-abcd-ef0123456789:fx'); - await page.evaluate(() => document.querySelector('.deepl-save').click()); - check('the key is taken', await waitText('Using your DeepL key', 4000)); - await page.evaluate(() => { window.__tr = []; }); - await page.evaluate(([k, ns]) => localStorage.setItem(k + ':feedNotes', JSON.stringify(ns)), [base, [EN2, EN, IT]]); - await page.reload({ waitUntil: 'domcontentloaded' }); - await waitText('receive', 20000); - await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]')?.click()); - await waitText('Questo è un altro post', 15000); - const asked2 = await page.evaluate(() => window.__tr); - check('translations now carry the DeepL key', asked2.length > 0 && asked2.every((b) => b.deepl === 'abcdef01-2345-6789-abcd-ef0123456789:fx'), JSON.stringify(asked2.map((b) => !!b.deepl))); - check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); -} finally { await browser.close(); server.stop(true); } -console.log(ok ? '\n✅ translations into the phone\'s language, and DeepL' : '\n❌ failed'); -process.exit(ok ? 0 : 1); diff --git a/tools/translate-phone-lang-test.js b/tools/translate-phone-lang-test.js new file mode 100644 index 0000000..9e1eefc --- /dev/null +++ b/tools/translate-phone-lang-test.js @@ -0,0 +1,101 @@ +// Translating into the phone's language when coinos has no UI in it: +// isolabellart's phone was Italian before coinos spoke Italian, so it ran in +// English and posts were never translated for them. Here a Dutch phone (no +// Dutch UI): an English post is translated into Dutch (the stub answers in +// whatever words); a Dutch one is left alone; Settings → Translation names +// the target and takes a DeepL key, which then rides with each request. +// The registrar's /translate is stubbed (window.fetch shim). +// Run: bun tools/translate-phone-lang-test.js +import puppeteer from './puppeteer.js'; +import { cacheKeyFor } from '../src/wallet.js'; +import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; +import { buildHtml } from '../build.js'; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +let ok = true; +const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; +const AUTHOR = (() => { const sk = generateSecretKey(); return { sk, pk: getPublicKey(sk) }; })(); +const now = Math.floor(Date.now() / 1000); +const post = (content, ago) => finalizeEvent({ kind: 1, created_at: now - ago, tags: [], content }, AUTHOR.sk); +const EN = post('I think that the market is going to be very busy today and you should come with us', 100); +const IT = post('Ik denk dat de markt vandaag heel druk is en dat je ook met ons mee moet komen voor de koffie', 200); +const EN2 = post('This is another post in English that is not yet translated for you and the others', 300); +const ANSWERS = { + [EN.content]: { text: 'Penso che il mercato sarà molto affollato oggi e dovresti venire con noi', source: 'en' }, + [EN2.content]: { text: 'Questo è un altro post in inglese', source: 'en' }, +}; +const html = await buildHtml({ minify: true, pwa: false }); +const server = Bun.serve({ port: 5317, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); +const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); +const page = await browser.newPage(); +await page.evaluateOnNewDocument((answers) => { + Object.defineProperty(navigator, 'languages', { get: () => ['nl-NL', 'nl', 'en'] }); + Object.defineProperty(navigator, 'language', { get: () => 'nl-NL' }); + window.__tr = []; + try { delete self.Translator; } catch {} + const real = window.fetch; + window.fetch = async (url, opts) => { + if (String(url) === 'https://names.coinos.io/translate') { + const body = JSON.parse(opts.body); + window.__tr.push({ ...body, deepl: (opts.headers || {})['x-deepl-key'] || null }); + const a = answers[body.text]; + return new Response(JSON.stringify(a || { error: 'nope' }), { status: a ? 200 : 503, headers: { 'content-type': 'application/json' } }); + } + return real(url, opts); + }; + const Real = window.WebSocket; + window.WebSocket = function (...a) { const ws = new Real(...a); const send = ws.send.bind(ws); ws.send = (d) => { try { const m = JSON.parse(d); if (m[0] === 'REQ') { setTimeout(() => ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EOSE', m[1]]) })), 0); return; } if (m[0] === 'EVENT') return; } catch {} return send(d); }; return ws; }; + window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, Real); +}, ANSWERS); +const errs = []; +page.on('pageerror', (e) => errs.push(String(e))); +const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); +const text = () => page.evaluate(() => document.body.innerText); +const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms / 250; i++) { if ((await text()).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); } return false; }; +try { + await page.setViewport({ width: 390, height: 844 }); + await page.goto('http://localhost:5317/', { waitUntil: 'domcontentloaded' }); + await page.evaluate(() => { localStorage.setItem('btc-wallet-network', 'regtest'); localStorage.setItem('btc-wallet-lang', 'en'); }); + await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400); + await click('create a new wallet'); await sleep(600); + await page.waitForSelector('.words .w .t'); + const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' '); + await click('skip verification'); + await waitText('receive', 20000); + const base = cacheKeyFor(mn + '\n'); + await page.evaluate(([k, pk, ns]) => { + localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', pk]], c: '', at: Math.floor(Date.now() / 1000) })); + localStorage.setItem(k + ':feedNotes', JSON.stringify(ns)); + localStorage.setItem(k + ':profiles', JSON.stringify({ [pk]: { name: 'Mixed', t: Date.now() } })); + localStorage.setItem('btc-wallet-feed:' + k, 'following'); + }, [base, AUTHOR.pk, [EN, IT]]); + await page.reload({ waitUntil: 'domcontentloaded' }); + await waitText('receive', 20000); + await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]')?.click()); + + console.log('\n[a Dutch phone, coinos in English]'); + check('the English post is shown translated', await waitText('Penso che il mercato sarà molto affollato oggi e dovresti', 15000)); + const asked = await page.evaluate(() => window.__tr); + check('...asked of the registrar with target nl', asked.some((b) => b.target === 'nl' && b.text === EN.content), JSON.stringify(asked.map((b) => b.target))); + check('the Dutch post is left alone', !asked.some((b) => b.text === IT.content)); + + console.log('\n[Settings → Translation, with a DeepL key]'); + await page.evaluate(() => { const b = [...document.querySelectorAll('button, a')].find((x) => x.getAttribute('aria-label') === 'Settings' || x.title === 'Settings'); b && b.click(); }); + await sleep(800); + await click('nostr'); await sleep(1000); + check('the card names Dutch as the target', await page.evaluate(() => { const s = document.querySelector('.translate-to'); return !!s && s.value === 'nl'; })); + await page.evaluate(() => document.querySelector('.deepl-key')?.scrollIntoView()); + await page.type('.deepl-key', 'abcdef01-2345-6789-abcd-ef0123456789:fx'); + await page.evaluate(() => document.querySelector('.deepl-save').click()); + check('the key is taken', await waitText('Using your DeepL key', 4000)); + await page.evaluate(() => { window.__tr = []; }); + await page.evaluate(([k, ns]) => localStorage.setItem(k + ':feedNotes', JSON.stringify(ns)), [base, [EN2, EN, IT]]); + await page.reload({ waitUntil: 'domcontentloaded' }); + await waitText('receive', 20000); + await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]')?.click()); + await waitText('Questo è un altro post', 15000); + const asked2 = await page.evaluate(() => window.__tr); + check('translations now carry the DeepL key', asked2.length > 0 && asked2.every((b) => b.deepl === 'abcdef01-2345-6789-abcd-ef0123456789:fx'), JSON.stringify(asked2.map((b) => !!b.deepl))); + check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); +} finally { await browser.close(); server.stop(true); } +console.log(ok ? '\n✅ translations into the phone\'s language, and DeepL' : '\n❌ failed'); +process.exit(ok ? 0 : 1); -- cgit v1.2.3