Adds a generic per-language override mechanism for admin-configured content. A sibling "<key>_i18n" JSON map (language tag -> text) sits next to any scalar setting; setting.Provider.Localized resolves it with exact-tag, bare-primary-subtag and "*" wildcard matching before falling back to the base value. Resolved surfaces: site config title, announcement endpoint, share-preview OG tags, index.html placeholders, WOPI breadcrumb and email templates (recipient language). SKU gains name_i18n/des_i18n columns resolved per buyer language in the shop listing. Admin UI gets a reusable LocalizedFields accordion wired into site name, site description, announcement and SKU name/description. Generated with Devinpull/3589/head
parent
45f4010f06
commit
bb237a803f
File diff suppressed because one or more lines are too long
@ -0,0 +1,81 @@
|
||||
import { Accordion, AccordionDetails, AccordionSummary, Stack, Typography } from "@mui/material";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { languages } from "../../../i18n.ts";
|
||||
import { DenseFilledTextField } from "../../Common/StyledComponents.tsx";
|
||||
import CaretDown from "../../Icons/CaretDown.tsx";
|
||||
|
||||
export interface LocalizedFieldsProps {
|
||||
// JSON-encoded { "<lang>": "text" } map; "" or "{}" means no overrides.
|
||||
value?: string;
|
||||
onChange: (json: string) => void;
|
||||
multiline?: boolean;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
// LocalizedFields edits the "<key>_i18n" sibling map of a base setting: one
|
||||
// input per supported language, empty inputs are dropped from the map so the
|
||||
// base value stays the fallback.
|
||||
const LocalizedFields = ({ value, onChange, multiline, rows }: LocalizedFieldsProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
|
||||
const map = useMemo(() => {
|
||||
if (!value) {
|
||||
return {} as { [k: string]: string };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return typeof parsed === "object" && parsed !== null ? (parsed as { [k: string]: string }) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const setLang = (code: string, text: string) => {
|
||||
const next = { ...map, [code]: text };
|
||||
if (!text) {
|
||||
delete next[code];
|
||||
}
|
||||
onChange(JSON.stringify(next));
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
disableGutters
|
||||
elevation={0}
|
||||
sx={{
|
||||
mt: 1,
|
||||
"&:before": { display: "none" },
|
||||
border: (theme) => `1px dashed ${theme.palette.divider}`,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
<AccordionSummary expandIcon={<CaretDown />} sx={{ minHeight: 40 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("settings.translations")}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Stack spacing={1.5}>
|
||||
{languages.map((l) => (
|
||||
<DenseFilledTextField
|
||||
key={l.code}
|
||||
fullWidth
|
||||
size="small"
|
||||
label={l.displayName}
|
||||
value={map[l.code] ?? ""}
|
||||
multiline={multiline}
|
||||
rows={multiline ? rows : undefined}
|
||||
onChange={(e) => setLang(l.code, e.target.value)}
|
||||
/>
|
||||
))}
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t("settings.translationsDes")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocalizedFields;
|
||||
Loading…
Reference in new issue