Add admin import overview page and extend CMS translations to all 6 locales
Local Build and Deploy / deploy (push) Successful in 51s

This commit is contained in:
openhands committed 2026-07-12 21:28:16 +02:00
1 parent 2e4ed76121
commit 6933fb77d5
4 files changed
+100 -29

No files matched your search

+10 -3
View File
@@ -2,10 +2,17 @@ import fs from "node:fs/promises";
import path from "node:path";
import { TranslationsClient } from "../translations-client";
const LOCALES = ["en", "it", "nl", "de", "fr", "es"] as const;
export default async function CmsTranslationsPage() {
const messagesDir = path.join(process.cwd(), "messages");
const enContent = await fs.readFile(path.join(messagesDir, "en.json"), "utf-8");
const itContent = await fs.readFile(path.join(messagesDir, "it.json"), "utf-8");
return <TranslationsClient enJson={enContent} itJson={itContent} />;
const contents: Record<string, string> = {};
await Promise.all(
LOCALES.map(async (locale) => {
contents[locale] = await fs.readFile(path.join(messagesDir, `${locale}.json`), "utf-8");
}),
);
return <TranslationsClient localeContents={contents} />;
}
@@ -8,34 +8,49 @@ import { Input } from "@/components/ui/input";
import { useServerAction } from "@/hooks/use-server-action";
import { flattenObject, unflattenObject } from "@/lib/translations-utils";
type Locale = "en" | "it" | "nl" | "de" | "fr" | "es";
const LOCALES: { code: Locale; label: string }[] = [
{ code: "en", label: "EN" },
{ code: "it", label: "IT" },
{ code: "nl", label: "NL" },
{ code: "de", label: "DE" },
{ code: "fr", label: "FR" },
{ code: "es", label: "ES" },
];
interface Props {
enJson: string;
itJson: string;
localeContents: Record<string, string>;
}
export function TranslationsClient({ enJson, itJson }: Props) {
const [en, setEn] = useState(() => flattenObject(JSON.parse(enJson)));
const [it, setIt] = useState(() => flattenObject(JSON.parse(itJson)));
export function TranslationsClient({ localeContents }: Props) {
const [data, setData] = useState<Record<string, Record<string, string>>>(() => {
const initial: Record<string, Record<string, string>> = {};
for (const [locale, content] of Object.entries(localeContents)) {
initial[locale] = flattenObject(JSON.parse(content));
}
return initial;
});
const [search, setSearch] = useState("");
const [group, setGroup] = useState("");
const [activeLang, setActiveLang] = useState<"en" | "it">("en");
const [activeLang, setActiveLang] = useState<Locale>("en");
const { run, isPending } = useServerAction();
const PER_PAGE = 30;
const [page, setPage] = useState(1);
const activeData = activeLang === "en" ? en : it;
const setActiveData = activeLang === "en" ? setEn : setIt;
const activeData = data[activeLang];
const groups = useMemo(() => {
const g = new Set<string>();
Object.keys(en).forEach((k) => {
Object.keys(data["en"] ?? {}).forEach((k) => {
const parts = k.split(".");
if (parts.length > 1) g.add(parts[0]);
});
return Array.from(g).sort();
}, [en]);
}, [data]);
const filteredKeys = useMemo(() => {
if (!activeData) return [];
return Object.keys(activeData).filter((key) => {
const matchSearch =
!search ||
@@ -50,7 +65,7 @@ export function TranslationsClient({ enJson, itJson }: Props) {
const pageKeys = filteredKeys.slice((page - 1) * PER_PAGE, page * PER_PAGE);
function updateValue(key: string, value: string) {
setActiveData({ ...activeData, [key]: value });
setData({ ...data, [activeLang]: { ...activeData, [key]: value } });
}
function handleSave() {
@@ -90,20 +105,19 @@ export function TranslationsClient({ enJson, itJson }: Props) {
<div className="flex gap-2 flex-wrap">
<div className="flex gap-1">
<Button
variant={activeLang === "en" ? "default" : "outline"}
size="sm"
onClick={() => setActiveLang("en")}
>
EN
</Button>
<Button
variant={activeLang === "it" ? "default" : "outline"}
size="sm"
onClick={() => setActiveLang("it")}
>
IT
</Button>
{LOCALES.map(({ code, label }) => (
<Button
key={code}
variant={activeLang === code ? "default" : "outline"}
size="sm"
onClick={() => {
setActiveLang(code);
setPage(1);
}}
>
{label}
</Button>
))}
</div>
<Input
placeholder="Filter by key or value..."