medusa-plugin-tolgee
Version:
Medusa V2 Plugin to easily add translations to your models with Tolgee
568 lines (567 loc) • 23.8 kB
JavaScript
import { defineWidgetConfig } from "@medusajs/admin-sdk";
import { jsx, jsxs, Fragment as Fragment$1 } from "react/jsx-runtime";
import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
import { useTranslate, useTolgee, TolgeeProvider, Tolgee, FormatSimple } from "@tolgee/react";
import { InContextTools } from "@tolgee/web/tools";
import { Container as Container$1, clx, DropdownMenu, IconButton, Heading, Text, Button, toast, Select, Drawer, Kbd, Divider } from "@medusajs/ui";
import Medusa from "@medusajs/js-sdk";
import { Fragment, useEffect, Suspense } from "react";
import { Link } from "react-router-dom";
import { EllipsisHorizontal, TriangleRightMini, XMarkMini } from "@medusajs/icons";
import { useTranslation } from "react-i18next";
const formatKeyName = (keyName) => {
const parts = keyName.split(".");
if (parts.length > 1) {
return parts[1].charAt(0).toUpperCase() + parts[1].slice(1);
}
return keyName;
};
const sdk = new Medusa({
baseUrl: __BACKEND_URL__ ?? "/",
auth: {
type: "session"
}
});
const Container = (props) => {
return /* @__PURE__ */ jsx(Container$1, { ...props, className: clx(
"divide-y p-0",
props.className
) });
};
const ActionMenu = ({ groups }) => {
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { size: "small", variant: "transparent", children: /* @__PURE__ */ jsx(EllipsisHorizontal, {}) }) }),
/* @__PURE__ */ jsx(DropdownMenu.Content, { children: groups.map((group, index) => {
if (!group.actions.length) {
return null;
}
const isLast = index === groups.length - 1;
return /* @__PURE__ */ jsxs(DropdownMenu.Group, { children: [
group.actions.map((action, index2) => {
if (action.onClick) {
return /* @__PURE__ */ jsxs(
DropdownMenu.Item,
{
disabled: action.disabled,
onClick: (e) => {
e.stopPropagation();
action.onClick();
},
className: clx(
"[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
{
"[&_svg]:text-ui-fg-disabled": action.disabled
}
),
children: [
action.icon,
/* @__PURE__ */ jsx("span", { children: action.label })
]
},
index2
);
}
return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
DropdownMenu.Item,
{
className: clx(
"[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
{
"[&_svg]:text-ui-fg-disabled": action.disabled
}
),
asChild: true,
disabled: action.disabled,
children: /* @__PURE__ */ jsxs(Link, { to: action.to, onClick: (e) => e.stopPropagation(), children: [
action.icon,
/* @__PURE__ */ jsx("span", { children: action.label })
] })
}
) }, index2);
}),
!isLast && /* @__PURE__ */ jsx(DropdownMenu.Separator, {})
] }, index);
}) })
] });
};
const Header = ({
title,
subtitle,
actions = []
}) => {
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx(Heading, { level: "h2", children: title }),
subtitle && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: subtitle })
] }),
actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center gap-x-2", children: actions.map((action, index) => /* @__PURE__ */ jsxs(Fragment, { children: [
action.type === "button" && /* @__PURE__ */ jsx(
Button,
{
...action.props,
size: action.props.size || "small",
children: /* @__PURE__ */ jsxs(Fragment$1, { children: [
action.props.children,
action.link && /* @__PURE__ */ jsx(Link, { ...action.link })
] })
}
),
action.type === "action-menu" && /* @__PURE__ */ jsx(ActionMenu, { ...action.props }),
action.type === "custom" && action.children
] }, index)) })
] });
};
const SectionRow = ({ title, value, actions }) => {
const isValueString = typeof value === "string" || !value;
return /* @__PURE__ */ jsxs(
"div",
{
className: clx(
`text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`,
{
"grid-cols-[1fr_1fr_28px]": !!actions
}
),
children: [
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", leading: "compact", children: title }),
isValueString ? /* @__PURE__ */ jsx(
Text,
{
size: "small",
leading: "compact",
className: "whitespace-pre-line text-pretty",
children: value ?? "-"
}
) : /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: value }),
actions && /* @__PURE__ */ jsx("div", { children: actions })
]
}
);
};
const TranslationManagement = ({
id,
slug,
availableLanguages
}) => {
const { t: adminT } = useTranslation("tolgee");
const { t } = useTranslate(id);
const tolgee = useTolgee();
const client = useQueryClient();
const handleLanguageChange = async (lang) => {
if (!tolgee) return;
await tolgee.changeLanguage(lang);
};
const { mutateAsync: syncTranslation, isPending: syncing } = useMutation({
mutationFn: async () => {
await sdk.client.fetch(`/admin/tolgee/translation/${slug}`, {
method: "post"
});
},
onSuccess: () => {
toast.success("Success", { description: "Translations sync successful." });
client.invalidateQueries({ queryKey: ["tolgee-translations", slug] });
tolgee == null ? void 0 : tolgee.addPlugin(InContextTools());
},
onError: (error) => {
console.error(`Failed to sync translations for ${slug}:`, error);
toast.error("Error", { description: "Failed to sync translations." });
},
mutationKey: ["syncTranslation"]
});
const { mutateAsync: addTranslation, isPending: adding } = useMutation({
mutationFn: () => sdk.client.fetch(`/admin/tolgee/translation/${slug}/${id}`, {
method: "post"
}),
onSuccess: () => {
toast.success("Success", { description: "Translation created." });
client.invalidateQueries({ queryKey: ["tolgee-translations", slug, id] });
tolgee == null ? void 0 : tolgee.addPlugin(InContextTools());
},
onError: (error) => {
console.error(`Failed to create translation for ${slug}(${id}):`, error.message);
toast.error("Error", { description: "Failed to create translation." });
},
mutationKey: ["addTranslation"]
});
const { data: { keyNames = [] } = {}, isLoading } = useQuery({
queryFn: async () => await sdk.client.fetch(`/admin/tolgee/translation/${slug}/${id}`),
queryKey: ["tolgee-translations", slug, id]
});
const syncAllAction = {
type: "button",
props: {
children: adminT("widget.syncAll"),
onClick: () => syncTranslation(),
isLoading: syncing,
variant: "secondary"
}
};
const addTranslationAction = {
type: "button",
props: {
children: adminT("widget.add"),
onClick: () => addTranslation(),
isLoading: adding,
variant: "secondary"
}
};
return /* @__PURE__ */ jsxs(Container, { children: [
/* @__PURE__ */ jsx(
Header,
{
title: adminT("widget.title"),
subtitle: (keyNames == null ? void 0 : keyNames.length) > 0 ? adminT("widget.subtitle") : adminT("widget.subtitleEmpty"),
actions: (keyNames == null ? void 0 : keyNames.length) <= 0 ? [addTranslationAction, syncAllAction] : [{
type: "custom",
children: /* @__PURE__ */ jsxs(
Select,
{
onValueChange: handleLanguageChange,
value: tolgee.getLanguage(),
children: [
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: adminT("widget.selectLanguage") }) }),
/* @__PURE__ */ jsx(Select.Content, { children: availableLanguages.map((item) => /* @__PURE__ */ jsx(Select.Item, { value: item.tag, children: item.label }, item.tag)) })
]
}
)
}]
}
),
isLoading ? /* @__PURE__ */ jsx(SectionRow, { title: adminT("loading") }) : keyNames.map(
(keyName) => /* @__PURE__ */ jsx(
SectionRow,
{
title: formatKeyName(keyName),
value: t(keyName, adminT("widget.notTranslated"))
},
keyName
)
)
] });
};
const $schema$1 = "./$schema.json";
const shippingOptionsList$1 = {
title: "Translations",
empty: "No translations present yet."
};
const productOptionsList$1 = {
title: "Options translations",
empty: "No translations present yet."
};
const widget$1 = {
title: "Translations",
subtitle: "To translate, ALT+click on the value.",
subtitleEmpty: "No translations present yet.",
notTranslated: "Not translated (press ALT + click here)",
add: "Add",
syncAll: "Sync all",
selectLanguage: "Select a language"
};
const loading$1 = "Loading...";
const en = {
$schema: $schema$1,
shippingOptionsList: shippingOptionsList$1,
productOptionsList: productOptionsList$1,
widget: widget$1,
loading: loading$1
};
const $schema = "./$schema.json";
const shippingOptionsList = {
title: "Traduzioni",
empty: "Non sono ancora presenti traduzioni."
};
const productOptionsList = {
title: "Traduzioni opzioni",
empty: "Non sono ancora presenti traduzioni."
};
const widget = {
title: "Traduzioni",
subtitle: "Per tradurre, premi ALT+click sul valore.",
subtitleEmpty: "Non sono ancora presenti traduzioni.",
notTranslated: "Non tradotto (premi ALT + click qui)",
add: "Aggiungi",
syncAll: "Sincronizza tutto",
selectLanguage: "Seleziona lingua"
};
const loading = "Caricamento...";
const it = {
$schema,
shippingOptionsList,
productOptionsList,
widget,
loading
};
const translations = {
en,
it
};
const PluginI18n = ({ children }) => {
const { i18n } = useTranslation();
useEffect(() => {
if (!i18n.isInitialized) return;
if (i18n.hasResourceBundle("en", "tolgee")) return;
for (const [lng, resource] of Object.entries(translations))
i18n.addResourceBundle(lng, "tolgee", resource);
}, []);
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
" ",
children,
" "
] });
};
const TranslationWidget = (slug) => ({ data: { id } }) => {
const { options, tolgeeInstance } = useTolgeeFromOptions();
return /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(PluginI18n, { children: options && tolgeeInstance ? /* @__PURE__ */ jsx(TolgeeProvider, { tolgee: tolgeeInstance, fallback: "Loading...", children: /* @__PURE__ */ jsx(
TranslationManagement,
{
id,
slug,
availableLanguages: options.availableLanguages || []
}
) }) : /* @__PURE__ */ jsx("div", { children: "Loading..." }) }) });
};
const useTolgeeFromOptions = () => {
const { data: { options, tolgeeInstance } = {} } = useQuery({
queryFn: () => sdk.client.fetch("/admin/tolgee/options").then((options2) => {
const languages = options2.availableLanguages.map((lang) => lang.tag);
const tolgeeInstance2 = Tolgee().use(FormatSimple()).init({
language: options2.defaultLanguage,
apiUrl: options2.apiUrl,
apiKey: options2.apiKey,
availableLanguages: languages,
observerOptions: {
highlightColor: "rgba(0,0,0,0.7)"
}
});
tolgeeInstance2.addPlugin(InContextTools());
return {
options: options2,
tolgeeInstance: tolgeeInstance2
};
}),
queryKey: ["tolgee-options"]
});
return { options, tolgeeInstance };
};
defineWidgetConfig({
zone: "product_category.details.after"
});
const WidgetComponent0 = TranslationWidget("product_category");
defineWidgetConfig({
zone: "product_collection.details.after"
});
const WidgetComponent1 = TranslationWidget("product_collection");
const ShippingOptionCard = ({
labelKey,
descriptionKey
}) => {
return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: /* @__PURE__ */ jsx("div", { className: "shadow-elevation-card-rest bg-ui-bg-component transition-fg hover:bg-ui-bg-component-hover active:bg-ui-bg-component-pressed group-focus-visible:shadow-borders-interactive-with-active rounded-md px-4 py-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 justify-between", children: [
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-start", children: [
/* @__PURE__ */ jsx(Text, { size: "small", leading: "compact", weight: "plus", children: labelKey }),
/* @__PURE__ */ jsx(
Text,
{
size: "small",
leading: "compact",
className: "text-ui-fg-subtle",
children: descriptionKey
}
)
] }),
/* @__PURE__ */ jsx("div", { className: "flex size-7 items-center justify-center", children: /* @__PURE__ */ jsx(TriangleRightMini, { className: "text-ui-fg-muted" }) })
] }) }) });
};
const ProductOptionWidget = TranslationWidget("product_option");
const ProductOptionValueWidget = TranslationWidget("product_option_value");
const ProductOptionsInProductWidget = ({ data }) => {
const { options } = data;
const { t } = useTranslation("tolgee");
const isEmpty = !(options == null ? void 0 : options.length);
return /* @__PURE__ */ jsx(PluginI18n, { children: /* @__PURE__ */ jsxs(Container, { children: [
/* @__PURE__ */ jsx(Header, { title: t("productOptionsList.title"), subtitle: isEmpty ? t("productOptionsList.empty") : void 0 }),
!isEmpty && OptionsGrid(options, t("widget.title"))
] }) });
};
defineWidgetConfig({
zone: "product.details.side.after"
});
function OptionsGrid(options, title) {
return /* @__PURE__ */ jsx("div", { className: "px-6 py-4 grid gap-4 grid-cols-1 sm:grid-cols-2", children: options == null ? void 0 : options.map((option) => {
var _a;
return /* @__PURE__ */ jsxs(Drawer, { modal: false, children: [
/* @__PURE__ */ jsx(Drawer.Trigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { children: /* @__PURE__ */ jsx(ShippingOptionCard, { labelKey: option.title, descriptionKey: "" }) }) }),
/* @__PURE__ */ jsxs(
Drawer.Content,
{
onInteractOutside: (e) => e.preventDefault(),
className: "bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]",
children: [
/* @__PURE__ */ jsxs("div", { className: "bg-ui-code-bg-base flex items-center justify-between px-6 py-4", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-4", children: [
/* @__PURE__ */ jsx(Drawer.Title, { asChild: true, children: /* @__PURE__ */ jsx(Heading, { className: "text-ui-contrast-fg-primary", children: title }) }),
/* @__PURE__ */ jsx(Drawer.Description, { className: "sr-only", children: `Drawer with translations for product option ${option.title}` })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
/* @__PURE__ */ jsx(Kbd, { className: "bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary", children: "esc" }),
/* @__PURE__ */ jsx(Drawer.Close, { asChild: true, children: /* @__PURE__ */ jsx(
IconButton,
{
size: "small",
variant: "transparent",
className: "text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active",
children: /* @__PURE__ */ jsx(XMarkMini, {})
}
) })
] })
] }),
/* @__PURE__ */ jsx(Drawer.Body, { className: "flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]", children: /* @__PURE__ */ jsx("div", { className: "bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3", children: /* @__PURE__ */ jsxs(
Suspense,
{
fallback: /* @__PURE__ */ jsx("div", { className: "flex size-full flex-col" }),
children: [
/* @__PURE__ */ jsx(Heading, { className: "text-ui-contrast-fg-primary ps-2 pb-4", children: "Option title" }),
/* @__PURE__ */ jsx(ProductOptionWidget, { data: option }, option.id),
/* @__PURE__ */ jsx(Divider, { className: "h-8" }),
/* @__PURE__ */ jsx(Heading, { className: "text-ui-contrast-fg-primary ps-2 pb-4", children: "Option values:" }),
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-y-2", children: (_a = option.values) == null ? void 0 : _a.map((value) => /* @__PURE__ */ jsx(ProductOptionValueWidget, { data: value }, value.id)) })
]
}
) }) })
]
}
)
] }, option.id);
}) });
}
defineWidgetConfig({
zone: "product_tag.details.after"
});
const WidgetComponent3 = TranslationWidget("product_tag");
defineWidgetConfig({
zone: "product_type.details.after"
});
const WidgetComponent4 = TranslationWidget("product_type");
defineWidgetConfig({
zone: "product_variant.details.after"
});
const WidgetComponent5 = TranslationWidget("product_variant");
defineWidgetConfig({
zone: "product.details.after"
});
const WidgetComponent6 = TranslationWidget("product");
const ShippingOptionWidget = TranslationWidget("shipping_option");
const SOWidget = ({ data }) => {
var _a;
const { id } = data;
const { t } = useTranslation("tolgee");
const { data: { stock_location } = {}, isLoading } = useQuery({
queryFn: () => sdk.admin.stockLocation.retrieve(id, { fields: "name,*sales_channels,*address,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile,*fulfillment_providers" }),
queryKey: ["shipping_options"]
});
const shipping_options = ((_a = stock_location == null ? void 0 : stock_location.fulfillment_sets) == null ? void 0 : _a.flatMap((fs) => fs.service_zones.flatMap((sz) => sz.shipping_options.map((option) => ({
fset_type: `${fs.type[0].toUpperCase()}${fs.type.slice(1)}`,
zone_name: sz.name,
option
}))))) ?? [];
const isEmpty = !(shipping_options == null ? void 0 : shipping_options.length);
return /* @__PURE__ */ jsx(PluginI18n, { children: /* @__PURE__ */ jsxs(Container, { children: [
/* @__PURE__ */ jsx(Header, { title: t("shippingOptionsList.title"), subtitle: isEmpty ? t("shippingOptionsList.empty") : void 0 }),
isLoading ? /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: "Loading..." }) : !isEmpty && ShippingOptionsGrid(shipping_options, t("widget.title"))
] }) });
};
defineWidgetConfig({
zone: "location.details.side.after"
});
function ShippingOptionsGrid(shipping_options, title) {
return /* @__PURE__ */ jsx("div", { className: "px-6 py-4 grid gap-4 grid-cols-1 sm:grid-cols-2", children: shipping_options == null ? void 0 : shipping_options.map(({ option, zone_name, fset_type }) => /* @__PURE__ */ jsxs(Drawer, { modal: false, children: [
/* @__PURE__ */ jsx(Drawer.Trigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { children: /* @__PURE__ */ jsx(ShippingOptionCard, { labelKey: option.name, descriptionKey: `${fset_type} - ${zone_name}` }) }) }),
/* @__PURE__ */ jsxs(
Drawer.Content,
{
onInteractOutside: (e) => e.preventDefault(),
className: "bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]",
children: [
/* @__PURE__ */ jsxs("div", { className: "bg-ui-code-bg-base flex items-center justify-between px-6 py-4", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-4", children: [
/* @__PURE__ */ jsx(Drawer.Title, { asChild: true, children: /* @__PURE__ */ jsx(Heading, { className: "text-ui-contrast-fg-primary", children: title }) }),
/* @__PURE__ */ jsx(Drawer.Description, { className: "sr-only", children: `Drawer with translations for shipping option ${option.name}` })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
/* @__PURE__ */ jsx(Kbd, { className: "bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary", children: "esc" }),
/* @__PURE__ */ jsx(Drawer.Close, { asChild: true, children: /* @__PURE__ */ jsx(
IconButton,
{
size: "small",
variant: "transparent",
className: "text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active",
children: /* @__PURE__ */ jsx(XMarkMini, {})
}
) })
] })
] }),
/* @__PURE__ */ jsx(Drawer.Body, { className: "flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]", children: /* @__PURE__ */ jsx("div", { className: "bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3", children: /* @__PURE__ */ jsx(
Suspense,
{
fallback: /* @__PURE__ */ jsx("div", { className: "flex size-full flex-col" }),
children: /* @__PURE__ */ jsx(ShippingOptionWidget, { data: option })
}
) }) })
]
}
)
] }, option.id)) });
}
const widgetModule = { widgets: [
{
Component: WidgetComponent0,
zone: ["product_category.details.after"]
},
{
Component: WidgetComponent1,
zone: ["product_collection.details.after"]
},
{
Component: ProductOptionsInProductWidget,
zone: ["product.details.side.after"]
},
{
Component: WidgetComponent3,
zone: ["product_tag.details.after"]
},
{
Component: WidgetComponent4,
zone: ["product_type.details.after"]
},
{
Component: WidgetComponent5,
zone: ["product_variant.details.after"]
},
{
Component: WidgetComponent6,
zone: ["product.details.after"]
},
{
Component: SOWidget,
zone: ["location.details.side.after"]
}
] };
const routeModule = {
routes: []
};
const menuItemModule = {
menuItems: []
};
const formModule = { customFields: {} };
const displayModule = {
displays: {}
};
const plugin = {
widgetModule,
routeModule,
menuItemModule,
formModule,
displayModule
};
export {
plugin as default
};