feat(i18n): update settings and dashboard layout for dynamic translations

This commit is contained in:
poyrazavsever
2026-07-20 11:08:32 +03:00
parent e5d609426d
commit 542cb66759
11 changed files with 496 additions and 30 deletions
@@ -0,0 +1,28 @@
"use server";
import { revalidatePath } from "next/cache";
import { getSqliteConnection } from "@/server/db/client";
import { I18nService } from "@/server/i18n/service";
import { requireFreelancerBackend } from "@/server/web/freelancer";
export async function exportTranslationsAction() {
const { actor } = await requireFreelancerBackend();
const service = new I18nService(getSqliteConnection().db);
const pkg = service.exportPackage(actor);
return { package: pkg };
}
export async function importTranslationsAction(content: string) {
const { actor } = await requireFreelancerBackend();
const service = new I18nService(getSqliteConnection().db);
try {
const data = JSON.parse(content);
service.importPackage(actor, data);
revalidatePath("/settings/languages");
return { success: true };
} catch (error: any) {
console.error("Import error:", error);
return { errorKey: "common.error.description", details: error.message };
}
}
@@ -0,0 +1,124 @@
"use client";
import { useState, useTransition } from "react";
import { Download, Upload, AlertTriangle } from "lucide-react";
import { Button, Card, CardContent, Label, Input } from "poyraz-ui/atoms";
import { Alert, AlertDescription, toast } from "poyraz-ui/molecules";
import { useTranslations } from "@/components/i18n/i18n-provider";
import { exportTranslationsAction, importTranslationsAction } from "./actions";
export function ImportExportForm() {
const t = useTranslations();
const [pending, startTransition] = useTransition();
const [file, setFile] = useState<File | null>(null);
const handleExport = () => {
startTransition(async () => {
const result = await exportTranslationsAction();
if (result.package) {
const blob = new Blob([JSON.stringify(result.package, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `neta-i18n-export-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success("Dışa aktarma başarılı.");
}
});
};
const handleImport = () => {
if (!file) return;
if (!window.confirm("Bu işlem mevcut çevirilerin üzerine yazabilir. Devam etmek istiyor musunuz?")) {
return;
}
startTransition(async () => {
try {
const text = await file.text();
const result = await importTranslationsAction(text);
if (result.errorKey) {
toast.error(t(result.errorKey) + (result.details ? ` (${result.details})` : ""));
} else {
toast.success("İçe aktarma başarılı.");
setFile(null);
}
} catch (err) {
toast.error("Dosya okunurken bir hata oluştu.");
}
});
};
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-foreground">İçe / Dışa Aktarma</h2>
<p className="text-sm text-muted-foreground mt-1">Dil paketlerini ve çevirileri taşıyın veya yedekleyin.</p>
</div>
<div className="grid gap-6 xl:grid-cols-2">
<Card>
<CardContent className="p-6 space-y-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<Download className="h-5 w-5" />
</div>
<h3 className="font-semibold">Dışa Aktar</h3>
</div>
<p className="text-sm text-muted-foreground">
Mevcut tüm dilleri, aktif çevirileri ve tercih edilen varsayılan dil bilgisini içeren bir JSON yedeği oluşturun.
</p>
<Button onClick={handleExport} loading={pending} effect="shine" className="w-full">
Dışa Aktar (.json)
</Button>
</CardContent>
</Card>
<Card>
<CardContent className="p-6 space-y-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<Upload className="h-5 w-5" />
</div>
<h3 className="font-semibold">İçe Aktar</h3>
</div>
<p className="text-sm text-muted-foreground">
Daha önce Neta üzerinden dışa aktarılmış bir dil paketini içeri yükleyin.
</p>
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription className="text-xs">
Aynı dil koduna sahip çevirilerin üzerine yazılacaktır.
</AlertDescription>
</Alert>
<div className="space-y-2">
<Label>JSON Dosyası Seç</Label>
<Input
type="file"
accept=".json,application/json"
onChange={(e) => setFile(e.target.files?.[0] || null)}
disabled={pending}
/>
</div>
<Button
onClick={handleImport}
disabled={!file}
loading={pending}
effect="shine"
className="w-full"
>
İçe Aktar
</Button>
</CardContent>
</Card>
</div>
</div>
);
}
@@ -0,0 +1,5 @@
import { ImportExportForm } from "./import-export-form";
export default function ImportExportPage() {
return <ImportExportForm />;
}