Files
LingvAI/pages/api/translate/document.ts
Malin 0799101da3 feat: add admin panel, Replicate AI translation, and document translation
- Admin panel (/admin) with JWT auth: configure Replicate API token,
  JigsawStack API key, model version, enable/disable AI translation,
  change admin password. Settings persisted in data/settings.json.

- Replicate AI translation: POST /api/translate/replicate uses
  JigsawStack text-translate model via Replicate API. Main page
  switches to client-side AI translation when enabled.

- Document translation tab: supports PDF, DOCX, XLSX, XLS, CSV.
  Excel/Word formatting fully preserved (SheetJS + JSZip XML manipulation).
  PDF uses pdf-parse extraction + pdf-lib reconstruction.
  Column selector UI for tabular data (per-sheet, All/None toggles).

- Updated README with full implementation documentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:43:54 +01:00

137 lines
5.1 KiB
TypeScript

import { NextApiHandler } from "next";
import formidable, { File, Fields, Files } from "formidable";
import fs from "fs";
import path from "path";
import { translateExcel, getExcelColumns, ColumnSelection } from "@utils/document-processors/excel";
import { translateDocx } from "@utils/document-processors/docx";
import { translatePdf } from "@utils/document-processors/pdf";
import { readSettings } from "@utils/settings-store";
export const config = {
api: {
bodyParser: false,
responseLimit: "50mb"
}
};
type ParsedForm = {
fields: Fields;
files: Files;
};
function parseForm(req: Parameters<NextApiHandler>[0]): Promise<ParsedForm> {
const form = formidable({ maxFileSize: 50 * 1024 * 1024 }); // 50 MB
return new Promise((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) reject(err);
else resolve({ fields, files });
});
});
}
function getFileBuffer(file: File): Buffer {
return fs.readFileSync(file.filepath);
}
function getField(fields: Fields, key: string): string | undefined {
const val = fields[key];
return Array.isArray(val) ? val[0] : (val as string | undefined);
}
const handler: NextApiHandler = async (req, res) => {
if (req.method !== "POST") {
res.setHeader("Allow", ["POST"]);
return res.status(405).json({ error: "Method Not Allowed" });
}
const settings = readSettings();
if (!settings.replicateEnabled) {
return res.status(503).json({ error: "Replicate translation is not enabled. Configure it in the admin panel." });
}
let parsed: ParsedForm;
try {
parsed = await parseForm(req);
} catch (err) {
return res.status(400).json({ error: "Failed to parse upload" });
}
const { fields, files } = parsed;
const fileEntry = files["file"];
const file = Array.isArray(fileEntry) ? fileEntry[0] : fileEntry;
if (!file) {
return res.status(400).json({ error: "No file uploaded" });
}
const targetLanguage = getField(fields, "targetLanguage") ?? "en";
const sourceLanguage = getField(fields, "sourceLanguage");
const action = getField(fields, "action") ?? "translate";
const columnSelectionsRaw = getField(fields, "columnSelections");
const filename = file.originalFilename ?? file.newFilename ?? "file";
const ext = path.extname(filename).toLowerCase();
const buffer = getFileBuffer(file);
try {
// Action: getColumns - return column info for Excel files
if (action === "getColumns") {
if (![".xlsx", ".xls", ".csv"].includes(ext)) {
return res.status(400).json({ error: "Column selection only supported for Excel/CSV files" });
}
const columns = getExcelColumns(buffer, filename);
return res.status(200).json({ columns });
}
// Action: translate
let outBuffer: Buffer;
let outMime: string;
let outFilename: string;
if ([".xlsx", ".xls"].includes(ext)) {
let columnSelections: ColumnSelection[] = [];
if (columnSelectionsRaw) {
try {
columnSelections = JSON.parse(columnSelectionsRaw);
} catch { /* use empty = translate all */ }
}
outBuffer = await translateExcel(buffer, targetLanguage, columnSelections);
outMime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
outFilename = filename.replace(ext, `_${targetLanguage}${ext}`);
} else if (ext === ".csv") {
// Treat CSV as Excel
let columnSelections: ColumnSelection[] = [];
if (columnSelectionsRaw) {
try {
columnSelections = JSON.parse(columnSelectionsRaw);
} catch { /* use empty = translate all */ }
}
outBuffer = await translateExcel(buffer, targetLanguage, columnSelections);
outMime = "text/csv";
outFilename = filename.replace(ext, `_${targetLanguage}${ext}`);
} else if (ext === ".docx") {
outBuffer = await translateDocx(buffer, targetLanguage);
outMime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
outFilename = filename.replace(ext, `_${targetLanguage}${ext}`);
} else if (ext === ".pdf") {
outBuffer = await translatePdf(buffer, targetLanguage, sourceLanguage);
outMime = "application/pdf";
outFilename = filename.replace(ext, `_${targetLanguage}${ext}`);
} else {
return res.status(400).json({
error: `Unsupported file type: ${ext}. Supported: .pdf, .docx, .xlsx, .xls, .csv`
});
}
res.setHeader("Content-Type", outMime);
res.setHeader("Content-Disposition", `attachment; filename="${outFilename}"`);
res.setHeader("Content-Length", outBuffer.length);
return res.status(200).send(outBuffer);
} catch (err) {
const msg = err instanceof Error ? err.message : "Translation failed";
return res.status(500).json({ error: msg });
}
};
export default handler;