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