first commit
This commit is contained in:
516
server.js
Normal file
516
server.js
Normal file
@@ -0,0 +1,516 @@
|
||||
/**
|
||||
* Scheer Enterprise Translation & Transcription Tool — local proxy server.
|
||||
*
|
||||
* Why this file exists: the DeepL API blocks direct calls from browser
|
||||
* JavaScript (CORS), and your API key must never sit in public client code.
|
||||
* This tiny Node server (no external dependencies, uses Node 18+ built-in
|
||||
* fetch/FormData/Blob) does two things:
|
||||
* 1. Serves the single-page UI (index.html)
|
||||
* 2. Relays translate/document/usage requests to DeepL using the API key
|
||||
* configured here on the server (via DEEPL_API_KEY env var or a local
|
||||
* .env file). The key is never sent to or stored in the browser.
|
||||
*
|
||||
* Setup: copy .env.example to .env and fill in DEEPL_API_KEY, or export
|
||||
* DEEPL_API_KEY as an environment variable before starting.
|
||||
* Run: node server.js
|
||||
* Open: http://localhost:3000
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ---- Minimal .env loader (no dependency) -------------------------------
|
||||
// Reads KEY=VALUE lines from a .env file next to this script, without
|
||||
// overriding variables already set in the real environment.
|
||||
(function loadDotEnv() {
|
||||
const envPath = path.join(__dirname, '.env');
|
||||
if (!fs.existsSync(envPath)) return;
|
||||
const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const idx = trimmed.indexOf('=');
|
||||
if (idx === -1) continue;
|
||||
const key = trimmed.slice(0, idx).trim();
|
||||
let value = trimmed.slice(idx + 1).trim();
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (!(key in process.env)) process.env[key] = value;
|
||||
}
|
||||
})();
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DEEPL_API_KEY = process.env.DEEPL_API_KEY || '';
|
||||
|
||||
if (!DEEPL_API_KEY) {
|
||||
console.warn(
|
||||
'\n[warning] DEEPL_API_KEY is not set. Set it in a .env file (see .env.example) ' +
|
||||
'or as an environment variable before running the server. All translation ' +
|
||||
'requests will fail until it is configured.\n'
|
||||
);
|
||||
}
|
||||
|
||||
function baseUrlFor(apiKey) {
|
||||
return apiKey && apiKey.trim().endsWith(':fx')
|
||||
? 'https://api-free.deepl.com'
|
||||
: 'https://api.deepl.com';
|
||||
}
|
||||
|
||||
// ---- Domain customization (glossaries) ---------------------------------
|
||||
// Each of the four Scheer entities gets two fixed-direction glossaries —
|
||||
// German -> English and English -> German — persisted locally in
|
||||
// domains.json (which glossary_id belongs to which domain/direction; the
|
||||
// actual glossary content lives on DeepL's servers). domains.json lives on
|
||||
// the server disk, not in any user's browser, so every visitor to the site
|
||||
// sees and edits the same shared glossaries.
|
||||
|
||||
const DOMAINS_FILE = path.join(__dirname, 'domains.json');
|
||||
const VALID_DOMAINS = ['scheer-group', 'scheer-ids', 'scheer-pas', 'scheer-imc'];
|
||||
const DOMAIN_LABELS = {
|
||||
'scheer-group': 'Scheer Group',
|
||||
'scheer-ids': 'Scheer IDS',
|
||||
'scheer-pas': 'Scheer PAS',
|
||||
'scheer-imc': 'Scheer IMC',
|
||||
};
|
||||
const GLOSSARY_DIRECTIONS = {
|
||||
'de-en': { source_lang: 'DE', target_lang: 'EN', label: 'German → English' },
|
||||
'en-de': { source_lang: 'EN', target_lang: 'DE', label: 'English → German' },
|
||||
};
|
||||
|
||||
function emptyDomain(id) {
|
||||
return { label: DOMAIN_LABELS[id], glossaries: { 'de-en': null, 'en-de': null } };
|
||||
}
|
||||
|
||||
function loadDomains() {
|
||||
let data = {};
|
||||
if (fs.existsSync(DOMAINS_FILE)) {
|
||||
try {
|
||||
data = JSON.parse(fs.readFileSync(DOMAINS_FILE, 'utf-8'));
|
||||
} catch (e) {
|
||||
data = {};
|
||||
}
|
||||
}
|
||||
let changed = false;
|
||||
for (const id of VALID_DOMAINS) {
|
||||
if (!data[id]) {
|
||||
data[id] = emptyDomain(id);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
const d = data[id];
|
||||
// Drop retired style-rules/translation-memory fields from older versions.
|
||||
if ('style' in d || 'translationMemory' in d) {
|
||||
delete d.style;
|
||||
delete d.translationMemory;
|
||||
changed = true;
|
||||
}
|
||||
// Migrate the old single "glossary" field (one selectable language
|
||||
// pair) into the new fixed two-direction "glossaries" map.
|
||||
if ('glossary' in d) {
|
||||
if (!d.glossaries) d.glossaries = { 'de-en': null, 'en-de': null };
|
||||
const g = d.glossary;
|
||||
if (g && g.source_lang && g.target_lang) {
|
||||
const src = String(g.source_lang).toUpperCase();
|
||||
const tgt = String(g.target_lang).split('-')[0].toUpperCase();
|
||||
if (src === 'DE' && tgt === 'EN') d.glossaries['de-en'] = g;
|
||||
else if (src === 'EN' && tgt === 'DE') d.glossaries['en-de'] = g;
|
||||
// Any other legacy direction is no longer representable and is dropped.
|
||||
}
|
||||
delete d.glossary;
|
||||
changed = true;
|
||||
}
|
||||
if (!d.glossaries) {
|
||||
d.glossaries = { 'de-en': null, 'en-de': null };
|
||||
changed = true;
|
||||
}
|
||||
for (const dir of Object.keys(GLOSSARY_DIRECTIONS)) {
|
||||
if (!(dir in d.glossaries)) {
|
||||
d.glossaries[dir] = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed || !fs.existsSync(DOMAINS_FILE)) saveDomains(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
function saveDomains(data) {
|
||||
fs.writeFileSync(DOMAINS_FILE, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
// Decide whether one of a domain's two glossaries applies to a given
|
||||
// translation request (the request's language pair has to exactly match
|
||||
// the fixed direction the glossary was created for), and build the extra
|
||||
// DeepL params.
|
||||
function computeDomainParams(domains, domainId, sourceLang, targetLang) {
|
||||
const applied = { glossary: false };
|
||||
const extra = {};
|
||||
const d = domainId && domains[domainId];
|
||||
if (!d || !d.glossaries) return { extra, applied };
|
||||
const targetBase = String(targetLang || '').split('-')[0].toUpperCase();
|
||||
const srcUpper = sourceLang ? String(sourceLang).toUpperCase() : '';
|
||||
|
||||
for (const [dir, dirCfg] of Object.entries(GLOSSARY_DIRECTIONS)) {
|
||||
const g = d.glossaries[dir];
|
||||
if (g && g.id && srcUpper === dirCfg.source_lang && targetBase === dirCfg.target_lang) {
|
||||
extra.glossary_id = g.id;
|
||||
applied.glossary = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { extra, applied };
|
||||
}
|
||||
|
||||
async function handleGetDomains(req, res) {
|
||||
sendJson(res, 200, loadDomains());
|
||||
}
|
||||
|
||||
async function handleSaveGlossary(req, res, domainId, direction) {
|
||||
if (!VALID_DOMAINS.includes(domainId)) return sendJson(res, 400, { error: 'Unknown domain.' });
|
||||
if (!GLOSSARY_DIRECTIONS[direction]) return sendJson(res, 400, { error: 'Unknown glossary direction.' });
|
||||
if (!DEEPL_API_KEY) return sendJson(res, 500, { error: 'Server is missing DEEPL_API_KEY.' });
|
||||
const { entries } = await parseJsonBody(req);
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
return sendJson(res, 400, { error: 'At least one glossary entry is required.' });
|
||||
}
|
||||
const cleanEntries = entries
|
||||
.filter((e) => e && e.source != null && e.target != null)
|
||||
.map((e) => ({ source: String(e.source).trim(), target: String(e.target).trim() }))
|
||||
.filter((e) => e.source && e.target);
|
||||
if (cleanEntries.length === 0) return sendJson(res, 400, { error: 'No valid (source, target) entries provided.' });
|
||||
|
||||
const { source_lang, target_lang, label } = GLOSSARY_DIRECTIONS[direction];
|
||||
const name = `${DOMAIN_LABELS[domainId]} ${label} Glossary`;
|
||||
const tsv = cleanEntries.map((e) => `${e.source}\t${e.target}`).join('\n');
|
||||
const base = baseUrlFor(DEEPL_API_KEY);
|
||||
|
||||
// Delete the old glossary for this domain/direction FIRST, before creating
|
||||
// the replacement. DeepL accounts have a cap on total glossary count
|
||||
// ("Too many glossaries" / 456 error), so freeing this slot up front
|
||||
// avoids briefly needing N+1 slots for what is logically still N
|
||||
// glossaries (one per domain per direction).
|
||||
const domains = loadDomains();
|
||||
const existing = domains[domainId].glossaries[direction];
|
||||
if (existing && existing.id) {
|
||||
const delResp = await fetch(`${base}/v2/glossaries/${existing.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `DeepL-Auth-Key ${DEEPL_API_KEY}` },
|
||||
}).catch(() => null);
|
||||
// A 404 here just means it was already gone (e.g. deleted manually on
|
||||
// DeepL's side) - fine to proceed. Other failures are logged but not
|
||||
// fatal; we still attempt the create below.
|
||||
if (delResp && !delResp.ok && delResp.status !== 404) {
|
||||
console.warn(`Warning: failed to delete old glossary ${existing.id} (status ${delResp.status}) before replacing it.`);
|
||||
}
|
||||
}
|
||||
|
||||
const createResp = await fetch(`${base}/v2/glossaries`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `DeepL-Auth-Key ${DEEPL_API_KEY}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, source_lang, target_lang, entries: tsv, entries_format: 'tsv' }),
|
||||
});
|
||||
if (!createResp.ok) {
|
||||
// The old glossary is already gone at this point, so make sure that's
|
||||
// reflected in domains.json even though the new save failed, instead of
|
||||
// leaving a dangling reference to a now-deleted glossary ID.
|
||||
if (existing && existing.id) {
|
||||
domains[domainId].glossaries[direction] = null;
|
||||
saveDomains(domains);
|
||||
}
|
||||
return sendJson(res, createResp.status, { error: await forwardDeepLError(createResp) });
|
||||
}
|
||||
const created = await createResp.json();
|
||||
|
||||
domains[domainId].glossaries[direction] = {
|
||||
id: created.glossary_id,
|
||||
name: created.name,
|
||||
source_lang: created.source_lang,
|
||||
target_lang: created.target_lang,
|
||||
entry_count: created.entry_count,
|
||||
entries: cleanEntries,
|
||||
};
|
||||
saveDomains(domains);
|
||||
sendJson(res, 200, domains[domainId].glossaries[direction]);
|
||||
}
|
||||
|
||||
async function handleDeleteGlossary(req, res, domainId, direction) {
|
||||
if (!VALID_DOMAINS.includes(domainId)) return sendJson(res, 400, { error: 'Unknown domain.' });
|
||||
if (!GLOSSARY_DIRECTIONS[direction]) return sendJson(res, 400, { error: 'Unknown glossary direction.' });
|
||||
const domains = loadDomains();
|
||||
const existing = domains[domainId].glossaries[direction];
|
||||
if (existing && existing.id && DEEPL_API_KEY) {
|
||||
const base = baseUrlFor(DEEPL_API_KEY);
|
||||
await fetch(`${base}/v2/glossaries/${existing.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `DeepL-Auth-Key ${DEEPL_API_KEY}` },
|
||||
}).catch(() => {});
|
||||
}
|
||||
domains[domainId].glossaries[direction] = null;
|
||||
saveDomains(domains);
|
||||
sendJson(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
function sendJson(res, status, obj) {
|
||||
const body = JSON.stringify(obj);
|
||||
res.writeHead(status, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
req.on('data', (c) => {
|
||||
size += c.length;
|
||||
if (size > 60 * 1024 * 1024) {
|
||||
reject(new Error('Payload too large (max 60MB)'));
|
||||
req.destroy();
|
||||
return;
|
||||
}
|
||||
chunks.push(c);
|
||||
});
|
||||
req.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function parseJsonBody(req) {
|
||||
const raw = await readBody(req);
|
||||
if (!raw.length) return {};
|
||||
try {
|
||||
return JSON.parse(raw.toString('utf-8'));
|
||||
} catch (e) {
|
||||
const err = new Error('Invalid JSON body');
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function forwardDeepLError(resp) {
|
||||
let detail = '';
|
||||
try {
|
||||
detail = await resp.text();
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
return `DeepL API error (${resp.status} ${resp.statusText}): ${detail || 'no detail'}`;
|
||||
}
|
||||
|
||||
// ---- Route handlers ---------------------------------------------------
|
||||
|
||||
async function handleTranslate(req, res) {
|
||||
const { texts, target_lang, source_lang, domain } = await parseJsonBody(req);
|
||||
const apiKey = DEEPL_API_KEY;
|
||||
if (!apiKey) return sendJson(res, 500, { error: 'Server is missing DEEPL_API_KEY. Ask an admin to configure it (see .env.example).' });
|
||||
if (!Array.isArray(texts) || texts.length === 0) {
|
||||
return sendJson(res, 400, { error: 'No text provided to translate.' });
|
||||
}
|
||||
if (!target_lang) return sendJson(res, 400, { error: 'Missing target_lang.' });
|
||||
|
||||
const domains = loadDomains();
|
||||
const { extra, applied } = computeDomainParams(domains, domain, source_lang, target_lang);
|
||||
|
||||
const base = baseUrlFor(apiKey);
|
||||
const params = new URLSearchParams();
|
||||
for (const t of texts) params.append('text', t);
|
||||
params.append('target_lang', target_lang);
|
||||
if (source_lang) params.append('source_lang', source_lang);
|
||||
params.append('preserve_formatting', '1');
|
||||
if (extra.glossary_id) params.append('glossary_id', extra.glossary_id);
|
||||
|
||||
const resp = await fetch(`${base}/v2/translate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${apiKey}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: params,
|
||||
});
|
||||
|
||||
if (!resp.ok) return sendJson(res, resp.status, { error: await forwardDeepLError(resp) });
|
||||
const data = await resp.json();
|
||||
data.applied = applied;
|
||||
sendJson(res, 200, data);
|
||||
}
|
||||
|
||||
async function handleUsage(req, res) {
|
||||
await parseJsonBody(req); // drain any request body (unused — key comes from server env)
|
||||
const apiKey = DEEPL_API_KEY;
|
||||
if (!apiKey) return sendJson(res, 500, { error: 'Server is missing DEEPL_API_KEY. Ask an admin to configure it (see .env.example).' });
|
||||
const base = baseUrlFor(apiKey);
|
||||
const resp = await fetch(`${base}/v2/usage`, {
|
||||
headers: { Authorization: `DeepL-Auth-Key ${apiKey}` },
|
||||
});
|
||||
if (!resp.ok) return sendJson(res, resp.status, { error: await forwardDeepLError(resp) });
|
||||
sendJson(res, 200, await resp.json());
|
||||
}
|
||||
|
||||
async function handleDocumentUpload(req, res) {
|
||||
const { filename, fileBase64, target_lang, source_lang, domain } = await parseJsonBody(req);
|
||||
const apiKey = DEEPL_API_KEY;
|
||||
if (!apiKey) return sendJson(res, 500, { error: 'Server is missing DEEPL_API_KEY. Ask an admin to configure it (see .env.example).' });
|
||||
if (!fileBase64 || !filename) return sendJson(res, 400, { error: 'Missing file data.' });
|
||||
if (!target_lang) return sendJson(res, 400, { error: 'Missing target_lang.' });
|
||||
|
||||
const domains = loadDomains();
|
||||
const { extra, applied } = computeDomainParams(domains, domain, source_lang, target_lang);
|
||||
|
||||
const base = baseUrlFor(apiKey);
|
||||
const buf = Buffer.from(fileBase64, 'base64');
|
||||
const blob = new Blob([buf]);
|
||||
const form = new FormData();
|
||||
form.append('file', blob, filename);
|
||||
form.append('target_lang', target_lang);
|
||||
if (source_lang) form.append('source_lang', source_lang);
|
||||
if (extra.glossary_id) form.append('glossary_id', extra.glossary_id);
|
||||
|
||||
const resp = await fetch(`${base}/v2/document`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `DeepL-Auth-Key ${apiKey}` },
|
||||
body: form,
|
||||
});
|
||||
if (!resp.ok) return sendJson(res, resp.status, { error: await forwardDeepLError(resp) });
|
||||
const data = await resp.json();
|
||||
data.applied = applied;
|
||||
sendJson(res, 200, data);
|
||||
}
|
||||
|
||||
async function handleDocumentStatus(req, res) {
|
||||
const { document_id, document_key } = await parseJsonBody(req);
|
||||
const apiKey = DEEPL_API_KEY;
|
||||
if (!apiKey) return sendJson(res, 500, { error: 'Server is missing DEEPL_API_KEY. Ask an admin to configure it (see .env.example).' });
|
||||
if (!document_id || !document_key) {
|
||||
return sendJson(res, 400, { error: 'Missing document_id or document_key.' });
|
||||
}
|
||||
const base = baseUrlFor(apiKey);
|
||||
const resp = await fetch(`${base}/v2/document/${encodeURIComponent(document_id)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ document_key }),
|
||||
});
|
||||
if (!resp.ok) return sendJson(res, resp.status, { error: await forwardDeepLError(resp) });
|
||||
sendJson(res, 200, await resp.json());
|
||||
}
|
||||
|
||||
async function handleDocumentResult(req, res) {
|
||||
const { document_id, document_key } = await parseJsonBody(req);
|
||||
const apiKey = DEEPL_API_KEY;
|
||||
if (!apiKey) return sendJson(res, 500, { error: 'Server is missing DEEPL_API_KEY. Ask an admin to configure it (see .env.example).' });
|
||||
if (!document_id || !document_key) {
|
||||
return sendJson(res, 400, { error: 'Missing document_id or document_key.' });
|
||||
}
|
||||
const base = baseUrlFor(apiKey);
|
||||
const resp = await fetch(`${base}/v2/document/${encodeURIComponent(document_id)}/result`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ document_key }),
|
||||
});
|
||||
if (!resp.ok) return sendJson(res, resp.status, { error: await forwardDeepLError(resp) });
|
||||
const arrayBuf = await resp.arrayBuffer();
|
||||
sendJson(res, 200, { fileBase64: Buffer.from(arrayBuf).toString('base64') });
|
||||
}
|
||||
|
||||
// ---- Static file + router ---------------------------------------------
|
||||
|
||||
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css' };
|
||||
|
||||
function serveStatic(req, res) {
|
||||
const urlPath = req.url.split('?')[0];
|
||||
const file = urlPath === '/' ? 'index.html' : urlPath.replace(/^\//, '');
|
||||
const filePath = path.join(__dirname, file);
|
||||
if (!filePath.startsWith(__dirname)) return res.writeHead(403).end('Forbidden');
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
return res.end('Not found');
|
||||
}
|
||||
const ext = path.extname(filePath);
|
||||
res.writeHead(200, {
|
||||
'Content-Type': MIME[ext] || 'application/octet-stream',
|
||||
// Prevent browsers/proxies from caching a stale copy of the app shell —
|
||||
// an outdated cached index.html calling routes that no longer exist on
|
||||
// a newer server.js is a common source of confusing 404s after updates.
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
});
|
||||
res.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
const routes = {
|
||||
'POST /api/translate': handleTranslate,
|
||||
'POST /api/usage': handleUsage,
|
||||
'POST /api/document/upload': handleDocumentUpload,
|
||||
'POST /api/document/status': handleDocumentStatus,
|
||||
'POST /api/document/result': handleDocumentResult,
|
||||
'GET /api/domains': handleGetDomains,
|
||||
};
|
||||
|
||||
// Routes with :domain and :direction path segments
|
||||
// (e.g. /api/domains/scheer-group/glossary/de-en).
|
||||
const DYNAMIC_ROUTES = [
|
||||
{ method: 'POST', regex: /^\/api\/domains\/([a-z-]+)\/glossary\/(de-en|en-de)$/, handler: handleSaveGlossary },
|
||||
{ method: 'DELETE', regex: /^\/api\/domains\/([a-z-]+)\/glossary\/(de-en|en-de)$/, handler: handleDeleteGlossary },
|
||||
];
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const urlPath = req.url.split('?')[0];
|
||||
|
||||
for (const route of DYNAMIC_ROUTES) {
|
||||
if (route.method !== req.method) continue;
|
||||
const m = urlPath.match(route.regex);
|
||||
if (m) {
|
||||
try {
|
||||
await route.handler(req, res, ...m.slice(1));
|
||||
} catch (e) {
|
||||
sendJson(res, e.statusCode || 500, { error: e.message || 'Internal server error' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const key = `${req.method} ${urlPath}`;
|
||||
const handler = routes[key];
|
||||
if (handler) {
|
||||
try {
|
||||
await handler(req, res);
|
||||
} catch (e) {
|
||||
sendJson(res, e.statusCode || 500, { error: e.message || 'Internal server error' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET') return serveStatic(req, res);
|
||||
|
||||
// Old (pre-direction) glossary route shape, e.g. POST/DELETE
|
||||
// /api/domains/scheer-group/glossary (no /:direction suffix). This can't
|
||||
// be handled — we don't know which of the two directional lists was
|
||||
// intended — but a bare 404 here looks like a server bug rather than what
|
||||
// it usually is: a browser tab with an old cached copy of index.html
|
||||
// still calling the old API shape. Point the user at the actual fix.
|
||||
const legacyGlossaryMatch = urlPath.match(/^\/api\/domains\/([a-z-]+)\/glossary$/);
|
||||
if (legacyGlossaryMatch && (req.method === 'POST' || req.method === 'DELETE')) {
|
||||
return sendJson(res, 400, {
|
||||
error:
|
||||
'This endpoint is outdated (missing the glossary direction, e.g. /de-en or /en-de). ' +
|
||||
'This usually means your browser is running a cached, older version of the page. ' +
|
||||
'Please hard-refresh (Ctrl+F5 or Cmd+Shift+R) and try saving the terms again.',
|
||||
});
|
||||
}
|
||||
|
||||
sendJson(res, 404, { error: 'Not found' });
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Scheer Translation Tool running at http://localhost:${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user