aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 10:47:18 -0700
committerAdam Soltys <[email protected]>2026-10-09 10:47:18 -0700
commitf65aab3d4ac3a9d059e3e8a4cb430ab83ef8d479 (patch)
tree1bc4b3e166c832b0444d85341fd7f7ffabc40fdc
parentaa7b3221fb79545606974ece7b1a16c27a99a12b (diff)
downloadcoinosv3-server-f65aab3d4ac3a9d059e3e8a4cb430ab83ef8d479.tar.gz
coinosv3-server-f65aab3d4ac3a9d059e3e8a4cb430ab83ef8d479.zip
Translation: a hashtag in a script the reader can't read gets its translation in brackets after it — "#総合部絵チャ (…)" — the tag itself kept as the link it is. A post that is only such hashtags now counts as foreign (it was judged by its prose alone, of which it has none, so nothing ever offered to translate it), and ⋯ → Translate on a post with no words says so instead of "already in your language". The registrar's language detection reads the glossed tags too; lang-guess's scriptOf takes a minimum (a two-character tag has a script too).
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
-rw-r--r--names/translate.js10
-rw-r--r--tools/translate-test.js7
2 files changed, 10 insertions, 7 deletions
diff --git a/names/translate.js b/names/translate.js
index 7d6181d..5382711 100644
--- a/names/translate.js
+++ b/names/translate.js
@@ -3,7 +3,7 @@
// passes through here; the cache keys on the text itself, so a popular post
// is translated once whoever asks.
import { createHash } from 'node:crypto';
-import { splitProse, hasWords, translatePieces } from '../src/translate-split.js';
+import { translatable, translatePieces } from '../src/translate-split.js';
const LT = process.env.LT_URL || 'http://libretranslate:5000';
const MAX_CHARS = 5000;
@@ -48,8 +48,8 @@ export async function translate(text, target, source = 'auto') {
const key = createHash('sha256').update(target + '|' + source + '|' + text).digest('hex');
const hit = cache.get(key);
if (hit) { cache.delete(key); cache.set(key, hit); return hit; }
- const prose = splitProse(text).filter((p) => p.prose && hasWords(p.prose));
- if (!prose.length) return { text, source: source === 'auto' ? null : source };
+ const { prose, tags } = translatable(text, target);
+ if (!prose.length && !tags.length) return { text, source: source === 'auto' ? null : source, nothing: true };
await slot();
try {
const call = async (path, body) => {
@@ -63,7 +63,7 @@ export async function translate(text, target, source = 'auto') {
};
let from = source, confidence = null;
if (from === 'auto') {
- const d = await call('/detect', { q: prose.map((p) => p.prose).join('\n') });
+ const d = await call('/detect', { q: [...prose.map((p) => p.prose), ...tags.map((p) => p.keep.slice(1).replace(/_/g, ' '))].join('\n') });
from = norm(d[0]?.language) || null;
confidence = d[0]?.confidence ?? null;
if (!from) throw Object.assign(new Error('unknown language'), { status: 422 });
@@ -73,7 +73,7 @@ export async function translate(text, target, source = 'auto') {
const translated = await translatePieces(text, async (qs) => {
const r = await call('/translate', { q: qs, source: toLt(from), target: toLt(target), format: 'text' });
return Array.isArray(r.translatedText) ? r.translatedText : [r.translatedText];
- });
+ }, target);
const out = { text: translated, source: from, confidence };
cache.set(key, out);
if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
diff --git a/tools/translate-test.js b/tools/translate-test.js
index 2760693..d3fe095 100644
--- a/tools/translate-test.js
+++ b/tools/translate-test.js
@@ -15,11 +15,13 @@ const now = Math.floor(Date.now() / 1000);
const post = (content, ago) => finalizeEvent({ kind: 1, created_at: now - ago, tags: [], content }, AUTHOR.sk);
const ES = post('Hola a todos, hoy compré café con bitcoin en el mercado y fue muy fácil para todos los que estaban allí', 100);
const JA = post('ビットコインは素晴らしいです。明日また会いましょう。', 200);
+const TAG = post('#総合部絵チャ', 250);
const EN = post('An ordinary post in English about the weather and the market today', 300);
const ANSWERS = {
[ES.content]: { text: 'Hello everyone, today I bought coffee with bitcoin at the market and it was very easy for everyone there', source: 'es' },
[JA.content]: { text: 'Bitcoin is great. Let’s meet again tomorrow.', source: 'ja' },
[EN.content]: { text: EN.content, source: 'en' },
+ [TAG.content]: { text: '#総合部絵チャ (general art chat)', source: 'ja' },
};
const html = await buildHtml({ minify: true, pwa: false });
@@ -78,7 +80,7 @@ try {
localStorage.setItem(k + ':feedNotes', JSON.stringify(ns));
localStorage.setItem(k + ':profiles', JSON.stringify({ [pk]: { name: 'Globetrotter', t: Date.now() } }));
localStorage.setItem('btc-wallet-feed:' + k, 'following');
- }, [base, AUTHOR.pk, [ES, JA, EN]]);
+ }, [base, AUTHOR.pk, [ES, JA, TAG, EN]]);
await page.reload({ waitUntil: 'domcontentloaded' });
await waitText('receive', 20000);
await openFeed();
@@ -87,8 +89,9 @@ try {
check('the Spanish post is shown in English', await waitText('today I bought coffee with bitcoin', 10000) && !(await has('compré café')));
check('...saying where it came from', /Translated from Spanish/.test(await row('today I bought coffee') || ''), (await row('today I bought coffee') || '').split('\n').slice(-6).join(' | '));
check('the Japanese post too', await waitText('Bitcoin is great', 5000) && /Translated from Japanese/.test(await row('Bitcoin is great') || ''));
+ check('a post that is only a Japanese hashtag gets its tag glossed, the tag kept', await waitText('#総合部絵チャ (general art chat)', 5000));
const asked = await page.evaluate(() => window.__tr.map((b) => b.text.slice(0, 12) + '>' + b.target));
- check('the English post is left alone (never sent)', asked.length === 2 && !asked.some((x) => x.startsWith('An ordinary')), asked.join(', '));
+ check('the English post is left alone (never sent)', asked.length === 3 && !asked.some((x) => x.startsWith('An ordinary')), asked.join(', '));
await page.evaluate(() => { const r = [...document.querySelectorAll('[data-zap-post]')].find((n) => n.textContent.includes('today I bought coffee')); [...r.querySelectorAll('.note-translate-btn')].find((b) => /original/i.test(b.textContent)).click(); });
await sleep(400);
check('Show original brings the Spanish back', await has('compré café') && !(await has('today I bought coffee')));