aboutsummaryrefslogtreecommitdiffstats
path: root/src/translate-split.js
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
commit7e064d5c8b33f788bda2755a32e71e2fb01a00ba (patch)
tree66a3b32f07a98b5646ffc9b662362bc912eb7553 /src/translate-split.js
parent41d7b9d4e5d0e10cdac708d9c9cd4838e47240ec (diff)
downloadcoinosv3-7e064d5c8b33f788bda2755a32e71e2fb01a00ba.tar.gz
coinosv3-7e064d5c8b33f788bda2755a32e71e2fb01a00ba.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
Diffstat (limited to 'src/translate-split.js')
-rw-r--r--src/translate-split.js40
1 files changed, 33 insertions, 7 deletions
diff --git a/src/translate-split.js b/src/translate-split.js
index 78833da..95f6468 100644
--- a/src/translate-split.js
+++ b/src/translate-split.js
@@ -4,6 +4,8 @@
// / 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) {
@@ -20,16 +22,40 @@ export function splitProse(text) {
// 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) {
+// 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);
- 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()));
+ 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;
});
- return parts.map((p) => p.keep ?? p.out ?? p.prose).join('');
+ 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('');
}