lomavuokraus/app/api/listings/translate/route.ts
2025-11-30 03:54:08 +02:00

136 lines
4.1 KiB
TypeScript

import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import { requireAuth } from '../../../../lib/jwt';
import type { Locale } from '../../../../lib/i18n';
type LocaleFields = { title: string; teaser: string; description: string };
const SUPPORTED_LOCALES: Locale[] = ['en', 'fi', 'sv'];
function loadApiKey() {
if (process.env.OPENAI_TRANSLATIONS_KEY) return process.env.OPENAI_TRANSLATIONS_KEY;
const newKeyPath = path.join(process.cwd(), 'creds', 'openai-translations.key');
try {
return fs.readFileSync(newKeyPath, 'utf8').trim();
} catch {
// ignore
}
if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY;
const fallbackPath = path.join(process.cwd(), 'creds', 'openai.key');
try {
return fs.readFileSync(fallbackPath, 'utf8').trim();
} catch {
return null;
}
}
export async function POST(req: Request) {
try {
await requireAuth(req);
} catch (err) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const apiKey = loadApiKey();
if (!apiKey) {
return NextResponse.json({ error: 'Missing OpenAI API key' }, { status: 500 });
}
let body: any;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
}
const incoming = body?.translations as Record<Locale, LocaleFields> | undefined;
const currentLocale = (body?.currentLocale as Locale) ?? 'en';
if (!incoming) {
return NextResponse.json({ error: 'Missing translations' }, { status: 400 });
}
const payload = SUPPORTED_LOCALES.reduce(
(acc, loc) => ({
...acc,
[loc]: {
title: incoming[loc]?.title || '',
teaser: incoming[loc]?.teaser || '',
description: incoming[loc]?.description || '',
},
}),
{} as Record<Locale, LocaleFields>,
);
const messages = [
{
role: 'system',
content:
'You are translating holiday rental listing copy between Finnish, Swedish, and English. Fill in missing locales, keep existing text unchanged, preserve meaning and tone, and respond with JSON only. Suggest localized slugs if missing.',
},
{
role: 'user',
content: JSON.stringify(
{
sourceLocale: currentLocale,
targetLocales: SUPPORTED_LOCALES.filter((l) => l !== currentLocale),
locales: payload,
askForSlugs: true,
},
null,
2,
),
},
{
role: 'system',
content:
'Return JSON with top-level "locales" containing keys en, fi, sv. Each locale has title, teaser, description, and slug. Do not include explanations.',
},
];
let content = '';
try {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'gpt-4o-mini',
temperature: 0.2,
messages,
}),
});
if (!res.ok) {
const errText = await res.text();
return NextResponse.json({ error: 'AI request failed', detail: errText }, { status: res.status || 500 });
}
const data = await res.json();
content = data?.choices?.[0]?.message?.content ?? '';
} catch (err: any) {
return NextResponse.json({ error: 'AI request failed', detail: err?.message }, { status: 500 });
}
const jsonText = content.match(/\{[\s\S]*\}/)?.[0] ?? content;
try {
const parsed = JSON.parse(jsonText);
const locales = parsed?.locales || parsed;
if (!locales) throw new Error('missing locales');
const result = SUPPORTED_LOCALES.reduce(
(acc, loc) => ({
...acc,
[loc]: {
title: locales[loc]?.title ?? '',
teaser: locales[loc]?.teaser ?? '',
description: locales[loc]?.description ?? '',
},
}),
{} as Record<Locale, LocaleFields>,
);
return NextResponse.json({ translations: result });
} catch (err) {
return NextResponse.json({ error: 'Could not parse AI response' }, { status: 500 });
}
}