diff options
| author | Adam Soltys <[email protected]> | 2026-10-09 10:36:39 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-09 10:36:39 -0700 |
| commit | 41d7b9d4e5d0e10cdac708d9c9cd4838e47240ec (patch) | |
| tree | 767d6dffc8dc14b90c368606d8fc39c68b050fb2 /src/translate-split.js | |
| parent | ad028dd1c61e41913e4628046b96bf162bc4ba4a (diff) | |
| download | coinosv3-41d7b9d4e5d0e10cdac708d9c9cd4838e47240ec.tar.gz coinosv3-41d7b9d4e5d0e10cdac708d9c9cd4838e47240ec.zip | |
Translation: posts in another language are shown in yours — "Translated from Spanish · Show original" under the words, automatic by default for posts the language guess calls foreign (Settings → Nostr → Translation switches it off and leaves a Translate link instead), and ⋯ → Translate for any post. The browser's on-device Translator does it when the language pair is already installed (desktop Chrome); otherwise the registrar's new POST /translate (names/translate.js): a cached, 4-at-a-time front for a LibreTranslate container on cs (names/compose-libretranslate.yml, v1.9.6, 32 languages; none for sw/mr/ta/te, so those readers get no offer). Links, nostr: mentions, :emoji:, #tags and long codes are never sent to the model — only the prose between them, batched in one call (src/translate-split.js, shared). Translations are remembered (coinos-translations, 300). tools/translate-test.js
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
Diffstat (limited to 'src/translate-split.js')
| -rw-r--r-- | src/translate-split.js | 35 |
1 files changed, 35 insertions, 0 deletions
diff --git a/src/translate-split.js b/src/translate-split.js new file mode 100644 index 0000000..78833da --- /dev/null +++ b/src/translate-split.js @@ -0,0 +1,35 @@ +// What must come back from a translation exactly as it went in: links, +// nostr: mentions, custom emoji shortcodes, hashtags, @handles, invoices and +// other long codes. Translation models mangle them ("https: / / example.com +// / fot.jpg"), so only the prose between them is translated and the text is +// put back together around the untouched tokens. Shared by the registrar +// (names/translate.js) and the browser's own translator (messages.js). +const KEEP = /(https?:\/\/\S+|nostr:[a-z0-9]+|:[a-z0-9_+-]{1,40}:|#[\p{L}\p{N}_]+|@[\w.-]+(?:@[\w.-]+)?|\b(?:ln|lightning:|cashu|bitcoin:|nevent|npub|nprofile|note|naddr)[a-z0-9]{20,}|\S{40,})/giu; + +export function splitProse(text) { + const parts = []; + let last = 0; + for (const m of String(text || '').matchAll(KEEP)) { + if (m.index > last) parts.push({ prose: text.slice(last, m.index) }); + parts.push({ keep: m[0] }); + last = m.index + m[0].length; + } + if (last < text.length) parts.push({ prose: text.slice(last) }); + return parts; +} +// a piece worth translating: has words, not just spaces and punctuation +export const hasWords = (s) => /\p{L}{2,}/u.test(s || ''); + +// Translate the prose pieces (one call for all of them) and reassemble. +// Each piece keeps its own leading/trailing whitespace — line breaks matter. +export async function translatePieces(text, translateMany) { + const parts = splitProse(text); + const prose = parts.filter((p) => p.prose && hasWords(p.prose)); + if (!prose.length) return text; + const outs = await translateMany(prose.map((p) => p.prose.trim())); + prose.forEach((p, i) => { + const lead = p.prose.match(/^\s*/)[0], trail = p.prose.match(/\s*$/)[0]; + p.out = lead + String(outs[i] ?? p.prose.trim()) + trail; + }); + return parts.map((p) => p.keep ?? p.out ?? p.prose).join(''); +} |
