lomavuokraus/app/api/listings/translate/route.ts
2025-11-29 20:23:06 +02:00

128 lines
3.7 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_API_KEY) return process.env.OPENAI_API_KEY;
const keyPath = path.join(process.cwd(), 'creds', 'openai.key');
try {
return fs.readFileSync(keyPath, '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.',
},
{
role: 'user',
content: JSON.stringify(
{
sourceLocale: currentLocale,
targetLocales: SUPPORTED_LOCALES.filter((l) => l !== currentLocale),
locales: payload,
},
null,
2,
),
},
{
role: 'system',
content:
'Return JSON with top-level "locales" containing keys en, fi, sv. Each locale has title, teaser, description. 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: 500 });
}
const data = await res.json();
content = data?.choices?.[0]?.message?.content ?? '';
} catch (err) {
return NextResponse.json({ error: 'AI request failed' }, { 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 });
}
}