1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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('');
}
|