diff options
| author | Adam Soltys <[email protected]> | 2026-10-16 12:57:19 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-16 12:57:19 -0700 |
| commit | 1dd70cc2c2b9b0a9f65a704ded9e322afabf736f (patch) | |
| tree | 95df69f7c311632326fb93670d88159cc8d41502 /names/vendor/translate-split.js | |
| parent | c88f0d860c089e49cebabd53d652859d04143457 (diff) | |
| download | coinosv3-server-1dd70cc2c2b9b0a9f65a704ded9e322afabf736f.tar.gz coinosv3-server-1dd70cc2c2b9b0a9f65a704ded9e322afabf736f.zip | |
Make this the coinosv3-server repo: the services, with coinosv3 pinned
Carved out of coinosv3 (names, feed, nwcpush, blossom, vmedia, bridge and
their tests, with history). The shared wallet code they import (src/ark,
noffer, bolt12, popular-feed) now comes from a coinosv3 submodule pinned at
082fc2e, the commit the services have actually run on, so moving here changes
no behaviour; bumping it is its own commit. The two text helpers translation
uses arrived on the server later than that commit and are vendored under
names/vendor at the versions deployed, until the pin moves past them.
Root package.json pins the npm versions installed on the server. compose.yml
runs names, nwcpush and blossom from this checkout (read-only) with configs
and state under $STATE, outside the repo, and pins the Bun image.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'names/vendor/translate-split.js')
| -rw-r--r-- | names/vendor/translate-split.js | 61 |
1 files changed, 61 insertions, 0 deletions
diff --git a/names/vendor/translate-split.js b/names/vendor/translate-split.js new file mode 100644 index 0000000..95f6468 --- /dev/null +++ b/names/vendor/translate-split.js @@ -0,0 +1,61 @@ +// 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). +import { scriptOf, LANG_SCRIPT } from './lang-guess.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 || ''); + +// A hashtag stays a hashtag (it's a link to its topic), but one written in +// another script than the reader's means nothing to them: such a tag gets +// its translation in brackets after it, "#総合部絵チャ (general art chat)". +const tagWords = (tag) => tag.slice(1).replace(/_/g, ' '); +export function glossable(tag, target) { + if (!tag.startsWith('#') || !target) return false; + const s = scriptOf(tagWords(tag), 1); + return !!s && !!LANG_SCRIPT[target] && s !== LANG_SCRIPT[target]; +} +// What a translator gets to see of a post: its prose, then the words of the +// hashtags the reader can't read (also what language detection runs on). +export function translatable(text, target) { + const parts = splitProse(text); + return { + parts, + prose: parts.filter((p) => p.prose && hasWords(p.prose)), + tags: parts.filter((p) => p.keep && glossable(p.keep, target)), + }; +} + +// Translate the prose pieces and the glossed tags (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, target = null) { + const { parts, prose, tags } = translatable(text, target); + if (!prose.length && !tags.length) return text; + const outs = await translateMany([...prose.map((p) => p.prose.trim()), ...tags.map((p) => tagWords(p.keep))]); + 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; + }); + tags.forEach((p, i) => { + const g = String(outs[prose.length + i] ?? '').trim(); + p.out = g && g.toLowerCase() !== tagWords(p.keep).toLowerCase() ? p.keep + ' (' + g + ')' : p.keep; + }); + return parts.map((p) => p.out ?? p.keep ?? p.prose).join(''); +} |
