diff options
Diffstat (limited to 'names')
| -rw-r--r-- | names/server.js | 8 | ||||
| -rw-r--r-- | names/translate.js | 41 |
2 files changed, 46 insertions, 3 deletions
diff --git a/names/server.js b/names/server.js index aff4e54..77d758c 100644 --- a/names/server.js +++ b/names/server.js @@ -28,7 +28,7 @@ import { mnemonicToSeedSync } from '@scure/bip39'; import { lnBackend } from '../bridge/ln.js'; import { ArkManager } from '../src/ark/manager.js'; import { fetchPreview } from './preview.js'; -import { translate, languages } from './translate.js'; +import { translate, translateDeepl, languages } from './translate.js'; import { decodeAddress } from '../src/ark/proto.js'; import { decodeNoffer } from '../src/noffer.js'; import { decodeBolt11 } from '../src/ark/lightning.js'; @@ -904,7 +904,7 @@ const json = (body, status = 200) => { headers: { 'content-type': 'application/json', 'access-control-allow-origin': '*', - 'access-control-allow-headers': 'content-type,authorization', + 'access-control-allow-headers': 'content-type,authorization,x-deepl-key', 'access-control-allow-methods': 'GET,POST,DELETE,OPTIONS', }, }); @@ -1056,7 +1056,9 @@ Bun.serve({ let body = null; try { body = await req.json(); } catch {} if (!body || typeof body.text !== 'string' || typeof body.target !== 'string') return json({ error: 'bad request' }, 400); - try { return json(await translate(body.text, body.target, body.source)); } + // a reader's own DeepL key rides in a header: passed on, never kept or logged + const dk = (req.headers.get('x-deepl-key') || '').trim(); + try { return json(dk ? await translateDeepl(body.text, body.target, dk) : await translate(body.text, body.target, body.source)); } catch (e) { return json({ error: e.message || 'translation failed' }, e.status || 503); } } diff --git a/names/translate.js b/names/translate.js index 5382711..36ab8cf 100644 --- a/names/translate.js +++ b/names/translate.js @@ -80,3 +80,44 @@ export async function translate(text, target, source = 'auto') { return out; } finally { release(); } } + +// DeepL, with the reader's own API key (sent per request; never stored, and +// not part of the cache key beyond the provider). DeepL refuses browsers +// (no CORS), which is the only reason this passes through here. A free key +// ends in ":fx" and has its own host. +const DEEPL_TARGET = { en: 'EN-US', pt: 'PT-BR', zh: 'ZH-HANS' }; +export async function translateDeepl(text, target, key, fetchImpl = fetch) { + text = String(text || ''); + target = norm(target); + if (!text.trim()) return { text: '', source: null }; + if (text.length > MAX_CHARS) throw Object.assign(new Error('too long'), { status: 413 }); + if (!/^[0-9a-f-]{20,}(:fx)?$/i.test(key)) throw Object.assign(new Error('bad DeepL key'), { status: 401 }); + const ck = createHash('sha256').update('deepl|' + target + '|' + text).digest('hex'); + const hit = cache.get(ck); + if (hit) { cache.delete(ck); cache.set(ck, hit); return hit; } + const { prose, tags } = translatable(text, target); + if (!prose.length && !tags.length) return { text, source: null, nothing: true }; + const host = /:fx$/i.test(key) ? 'https://api-free.deepl.com' : 'https://api.deepl.com'; + let from = null; + const translated = await translatePieces(text, async (qs) => { + const r = await fetchImpl(host + '/v2/translate', { + method: 'POST', + headers: { 'content-type': 'application/json', authorization: 'DeepL-Auth-Key ' + key }, + body: JSON.stringify({ text: qs, target_lang: DEEPL_TARGET[target] || target.toUpperCase() }), + signal: AbortSignal.timeout(20_000), + }); + if (r.status === 403 || r.status === 401) throw Object.assign(new Error('DeepL refused the key'), { status: 401 }); + if (r.status === 456) throw Object.assign(new Error('DeepL quota exceeded'), { status: 402 }); + if (r.status === 400) throw Object.assign(new Error('DeepL can’t translate into that language'), { status: 422 }); + if (!r.ok) throw Object.assign(new Error('DeepL ' + r.status), { status: 502 }); + const j = await r.json(); + const out = (j.translations || []).map((x) => x.text); + from ||= norm(j.translations?.[0]?.detected_source_language) || null; + if (out.length !== qs.length) throw Object.assign(new Error('DeepL answered short'), { status: 502 }); + return out; + }, target); + const out = { text: from === target ? text : translated, source: from }; + cache.set(ck, out); + if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value); + return out; +} |
