diff --git a/www/apps/book/app/learn/build/page.mdx b/www/apps/book/app/learn/build/page.mdx index f335559aec..387035f362 100644 --- a/www/apps/book/app/learn/build/page.mdx +++ b/www/apps/book/app/learn/build/page.mdx @@ -86,8 +86,8 @@ module.exports = defineConfig({ cookieOptions: { sameSite: "lax", secure: false, - } - } + }, + }, }) ``` diff --git a/www/apps/book/app/learn/fundamentals/admin/translations/page.mdx b/www/apps/book/app/learn/fundamentals/admin/translations/page.mdx new file mode 100644 index 0000000000..aba1424d4d --- /dev/null +++ b/www/apps/book/app/learn/fundamentals/admin/translations/page.mdx @@ -0,0 +1,464 @@ +import { CodeTabs, CodeTab } from "docs-ui" + +export const metadata = { + title: `${pageNumber} Translate Admin Customizations`, +} + +# {metadata.title} + +In this chapter, you'll learn how to add translations to your Medusa Admin widgets and UI routes. + + + +Translations for admin customizations are available from [Medusa v2.11.1](https://github.com/medusajs/medusa/releases/tag/v2.11.1). + + + +## Translations in the Medusa Admin + +The Medusa Admin dashboard supports [multiple languages](!user-guide!/tips/languages) for its interface. Medusa uses [react-i18next](https://react.i18next.com/) to manage translations in the admin dashboard. + + + +Medusa Admin translations apply to the interface only. Medusa doesn't support translating store content, such as product names or descriptions. To implement localization for store content, [integrate a CMS](!resources!/integrations#cms). + + + +When you create [Widgets](../widgets/page.mdx) or [UI Routes](../ui-routes/page.mdx) to customize the Medusa Admin, you can provide translations for the text content in your customizations, allowing users to view your customizations in their preferred language. + +You can add translations for your admin customizations within your Medusa project or as part of a plugin. + +--- + +## How to Add Translations to Admin Customizations + +### Step 1: Create Translation Files + +Translation files are JSON files containing key-value pairs, where the key identifies a text string and the value is the translated text or a nested object of translations. + +For example, to add English translations, create the file `src/admin/i18n/json/en.json` with the following content: + + + +English is the default language for Medusa Admin, so it's recommended to always include an English translation file. + + + +![Directory structure showing where to place translation files](https://res.cloudinary.com/dza7lstvk/image/upload/v1761555573/Medusa%20Book/translations-json_m2pvet.jpg) + +```json title="src/admin/i18n/json/en.json" +{ + "brands": { + "title": "Brands", + "description": "Manage your product brands" + }, + "done": "Done" +} +``` + +You can create additional translation files for other languages by following the same structure. For example, for Spanish, create `src/admin/i18n/json/es.json`: + +![Directory structure showing where to place translation files](https://res.cloudinary.com/dza7lstvk/image/upload/v1761555573/Medusa%20Book/translations-json-es_s1etna.jpg) + +```json title="src/admin/i18n/json/es.json" +{ + "brands": { + "title": "Marcas", + "description": "Gestiona las marcas de tus productos" + }, + "done": "Hecho" +} +``` + +### Step 2: Load Translation Files + +Next, to load the translation files, create the file `src/admin/i18n/index.ts` with the following content: + +![Directory structure showing i18n index file](https://res.cloudinary.com/dza7lstvk/image/upload/v1761555573/Medusa%20Book/translations-index_cgrj0t.jpg) + +```ts title="src/admin/i18n/index.ts" +import en from "./json/en.json" with { type: "json" } +import es from "./json/es.json" with { type: "json" } + +export default { + en: { + translation: en, + }, + es: { + translation: es, + }, +} +``` + +The `src/admin/i18n/index.ts` file imports the JSON translation files. You must include the `with { type: "json" }` directive to ensure the JSON files load correctly. + +The file exports an object that maps two-character language codes (like `en` and `es`) to their respective translation data. + +### Step 3: Use Translations in Admin Customizations + +Finally, you can use the translations in your admin customizations by using the `useTranslation` hook from `react-i18next`. + + + +The `react-i18next` package is already included in the Medusa Admin's dependencies, so you don't need to install it separately. However, `pnpm` users may need to install it manually due to package resolution issues. + + + +For example, create the file `src/admin/widgets/product-brand.tsx` with the following content: + +export const translationHighlights = [ + ["3", "useTranslation", "Import `useTranslation` hook."], + ["6", "t", "Access translation function."], + ["10", "t", "Get translated text using keys."] +] + +```tsx title="src/admin/widgets/product-brand.tsx" highlights={translationHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +const ProductWidget = () => { + const { t } = useTranslation() + return ( + +
+ {t("brands.title")} +

{t("brands.description")}

+
+
+ +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +In the above example, you retrieve the `t` function from the `useTranslation` hook. You then use this function to get the translated text by providing the appropriate keys defined in your translation JSON files. + +Nested keys are joined using dot notation. For example, `brands.title` refers to the `title` key inside the `brands` object in the translation files. + +### Test Translations + +To test the translations, start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +Then, go to a product details page in the Medusa Admin dashboard. If your default language is set to English, you'll see the widget displaying text in English. + +Next, [change the admin language](!user-guide!/settings/profile#edit-profile-details) to Spanish. The widget will now display the text in Spanish. + +--- + +## How Translations are Loaded + +When you load the translations with the `translation` key in `src/admin/i18n/index.ts`, your custom Medusa Admin translations are merged with the default Medusa Admin translations: + +- Translation keys in your custom translations override the default Medusa Admin translations. +- The default Medusa Admin translations are used as a fallback when a key is not defined in your custom translations. + +For example, consider the following widget and translation file: + + + + +```tsx title="src/admin/widgets/product-brand.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +const ProductWidget = () => { + const { t } = useTranslation() + return ( + +
+ {/* Output: Custom Brands Title */} + {t("brands.title")} + {/* Output: brands.description */} +

{t("brands.description")}

+
+
+ {/* Output: Custom Save */} + + {/* Output: Delete */} + +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +
+ + +```json title="src/admin/i18n/json/en.json" +{ + "brands": { + "title": "Custom Brands Title" + }, + "actions": { + "save": "Custom Save" + } +} +``` + + + + +```ts title="src/admin/i18n/index.ts" +import en from "./json/en.json" with { type: "json" } + +export default { + en: { + translation: en, + }, + // other languages... +} +``` + + +
+ +The widget will render the following for each translation key: + +- `brands.title`: Defined in your custom translation file, so it outputs `Custom Brands Title`. +- `brands.description`: Not defined in your custom translation file or the default Medusa Admin translations, so it outputs the key itself: `brands.description`. +- `actions.save`: Defined in your custom translation file, so it outputs `Custom Save`. +- `actions.delete`: Not defined in your custom translation file, so it falls back to the default Medusa Admin translation and outputs `Delete`. + +### Custom Translation Namespaces + +To avoid potential key conflicts between your custom translations and the default Medusa Admin translations, you can use custom namespaces. This is particularly useful when developing plugins that add admin customizations, as it prevents naming collisions with other plugins or the default Medusa Admin translations. + +To add translations under a custom namespace, change the `[language].translation` key in the `src/admin/i18n/index.ts` file to your desired namespace: + +export const namespacesHighlights = [ + ["6", "brands", "Use `brands` as custom namespace."], + ["9", "brands", "Use `brands` as custom namespace."] +] + +```ts title="src/admin/i18n/index.ts" highlights={namespacesHighlights} +import en from "./json/en.json" with { type: "json" } +import es from "./json/es.json" with { type: "json" } + +export default { + en: { + brands: en, + }, + es: { + brands: es, + }, +} +``` + +The translation files will now be loaded under the `brands` namespace. + +Then, in your admin customizations, specify the namespace when using the `useTranslation` hook: + +```tsx title="src/admin/widgets/product-brand.tsx" highlights={[["7"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +// The widget +const ProductWidget = () => { + const { t } = useTranslation("brands") + return ( + +
+ {t("brands.title")} +

{t("brands.description")}

+
+
+ +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +Translations are now loaded only from the `brands` namespace without conflicting with other translation keys in the Medusa Admin. + +--- + +## Translation Tips + +### Translation Organization + +To keep your translation files organized, especially as they grow, consider grouping related translation keys into nested objects. This helps maintain clarity and structure. + +It's recommended to create a nested object for each domain (for example, `brands`, `products`, etc...) and place related translation keys within those objects. This makes it easier to manage and locate specific translations. + +For example: + +```json title="src/admin/i18n/json/en.json" +{ + "brands": { + "title": "Brands", + "description": "Manage your product brands", + "actions": { + "add": "Add Brand", + "edit": "Edit Brand" + } + } +} +``` + +You can then access these nested translations using dot notation, such as `brands.title` or `brands.actions.add`. + +### Variables in Translations + +Translation values can include variables that are dynamically replaced at runtime. Variables are defined using double curly braces `{{variableName}}` in the translation files. + +For example, in your translation file `src/admin/i18n/json/en.json`, define a translation with a variable: + +```json title="src/admin/i18n/json/en.json" +{ + "welcome_message": "Welcome, {{username}}!" +} +``` + +Then, in your admin customization, pass the variable value in the second object parameter of the `t` function: + +```tsx title="src/admin/widgets/welcome-widget.tsx" +t("welcome_message", { username: "John" }) +``` + +This will output: `Welcome, John!` + +### Pluralization + +The `t` function supports pluralization based on a count value. You can define singular and plural forms in your translation files using the `_one`, `_other`, and `_zero` suffixes. + +For example, in your translation file `src/admin/i18n/json/en.json`, define the following translations: + +```json title="src/admin/i18n/json/en.json" +{ + "item_count_one": "You have {{count}} item.", + "item_count_other": "You have {{count}} items.", + "item_count_zero": "You have no items." +} +``` + +Then, in your admin customization, use the key without the suffix and provide the `count` variable: + +```tsx title="src/admin/widgets/item-count-widget.tsx" +t("item_count", { count: itemCount }) +``` + +This will render one of the following based on the value of `itemCount`: + +1. If `itemCount` is `0`, `item_count_zero` is used: `You have no items.` +2. If `itemCount` is `1`, `item_count_one` is used: `You have 1 item.` +3. If `itemCount` is greater than `1`, `item_count_other` is used: `You have X items.` + +### Element Interpolation + +Your translation strings can include HTML or React element placeholders that are replaced with actual elements at runtime. This is useful for adding links, bold text, or other formatting within translated strings. + +Elements to be interpolated are defined using angle brackets `` in the translation files, where `index` is a zero-based index representing the element's position. + +For example, in your translation file `src/admin/i18n/json/en.json`, define a translation with element placeholders: + +```json title="src/admin/i18n/json/en.json" +{ + "terms_and_conditions": "Please read our <0>Terms and Conditions." +} +``` + +Then, in your admin customization, import the `Trans` component from `react-i18next` that allows you to interpolate elements: + +```tsx title="src/admin/widgets/terms-widget.tsx" +import { Trans } from "react-i18next" +``` + +Finally, use the `Trans` component in the `return` statement to render the translation with the interpolated elements: + +```tsx title="src/admin/widgets/terms-widget.tsx" +, + ]} +/> +``` + +The `components` prop is an array of React elements that correspond to the placeholders defined in the translation string. In this case, the `<0>` placeholder is replaced with the anchor `` element. + +#### Passing Variables with Element Interpolation + +You can also pass translation variables to the `Trans` component as props. For example, to include a username variable: + +```tsx title="src/admin/widgets/welcome-widget.tsx" +, + ]} +/> +``` + +The `username` prop replaces the `{{username}}` variable in the translation string, and the `<0>` placeholder is replaced with the `` element. + +#### Using Namespaces with Element Interpolation + +If you're loading translations from a custom namespace, specify the namespace in the `Trans` component using the `ns` prop: + +```tsx title="src/admin/widgets/product-brand.tsx" +]} +/> +``` + +The `ns` prop indicates that the translation should be loaded from the `brands` namespace. + +#### Multiple Element Interpolation + +You can interpolate multiple elements by defining multiple placeholders in the translation string and providing corresponding elements in the `components` array. + +For example, define the following translation string: + +```json title="src/admin/i18n/json/en.json" +{ + "welcome_message": "Hello, <0>{{username}}! Please read our <1>Terms and Conditions." +} +``` + +Then, in your admin customization, you can use the `Trans` component with multiple elements: + +```tsx title="src/admin/widgets/welcome-widget.tsx" +, + , + ]} +/> +``` + +The first placeholder `<0>` is replaced with the `` element, and the second placeholder `<1>` is replaced with the anchor `` element. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/data-models/properties/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/properties/page.mdx index 13d16f83d3..9862f6c8ca 100644 --- a/www/apps/book/app/learn/fundamentals/data-models/properties/page.mdx +++ b/www/apps/book/app/learn/fundamentals/data-models/properties/page.mdx @@ -79,7 +79,7 @@ const Post = model.define("post", { { name: "limit_name_length", expression: (columns) => `LENGTH(${columns.name}) <= 50`, - } + }, ]) export default Post diff --git a/www/apps/book/app/learn/fundamentals/module-links/index-module/page.mdx b/www/apps/book/app/learn/fundamentals/module-links/index-module/page.mdx index 5e1b47e5b3..2e44a8e6b2 100644 --- a/www/apps/book/app/learn/fundamentals/module-links/index-module/page.mdx +++ b/www/apps/book/app/learn/fundamentals/module-links/index-module/page.mdx @@ -370,8 +370,8 @@ const { data: products } = await query.index({ fields: ["id", "title"], }, { cache: { - enable: true - } + enable: true, + }, }) ``` @@ -443,7 +443,7 @@ const { data: products } = await query.index({ key: "products-123456", // to disable auto invalidation: // autoInvalidate: false, - } + }, }) ``` @@ -467,10 +467,10 @@ const { data: products } = await query.index({ key: async (args, cachingModuleService) => { return await cachingModuleService.computeKey({ ...args, - prefix: "products" + prefix: "products", }) - } - } + }, + }, }) ``` @@ -497,7 +497,7 @@ const { data: products } = await query.index({ cache: { enable: true, tags: ["Product:list:*"], - } + }, }) ``` @@ -525,7 +525,7 @@ const { data: products } = await query.index({ collectionId ? `ProductCollection:${collectionId}` : undefined, ] }, - } + }, }) ``` @@ -549,7 +549,7 @@ const { data: products } = await query.index({ cache: { enable: true, ttl: 100, // 100 seconds - } + }, }) ``` @@ -562,15 +562,15 @@ const { data: products } = await query.index({ entity: "product", fields: ["id", "title"], filters: { - id: "prod_123" - } + id: "prod_123", + }, }, { cache: { enable: true, ttl: (args) => { return args[0].filters.id === "test" ? 10 : 100 - } - } + }, + }, }) ``` @@ -594,7 +594,7 @@ const { data: products } = await query.index({ cache: { enable: true, autoInvalidate: false, - } + }, }) ``` @@ -611,8 +611,8 @@ const { data: products } = await query.index({ enable: true, autoInvalidate: (args) => { return !args[0].fields.includes("custom_field") - } - } + }, + }, }) ``` @@ -641,8 +641,8 @@ const { data: products } = await query.index({ }, { cache: { enable: true, - providers: ["caching-redis", "caching-memcached"] - } + providers: ["caching-redis", "caching-memcached"], + }, }) ``` @@ -657,15 +657,15 @@ const { data: products } = await query.index({ entity: "product", fields: ["id", "title"], filters: { - id: "prod_123" - } + id: "prod_123", + }, }, { cache: { enable: true, providers: (args) => { return args[0].filters.id === "test" ? ["caching-redis"] : ["caching-memcached"] - } - } + }, + }, }) ``` diff --git a/www/apps/book/app/learn/fundamentals/module-links/query/page.mdx b/www/apps/book/app/learn/fundamentals/module-links/query/page.mdx index dc92c48bb3..4bfd1169ef 100644 --- a/www/apps/book/app/learn/fundamentals/module-links/query/page.mdx +++ b/www/apps/book/app/learn/fundamentals/module-links/query/page.mdx @@ -676,7 +676,7 @@ const { ```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]} const { data: posts, - metadata + metadata, } = useQueryGraphStep({ entity: "post", fields: ["id", "title"], @@ -933,7 +933,7 @@ const { data: posts } = useQueryGraphStep({ }, options: { throwIfKeyNotFound: true, - } + }, }) ``` @@ -979,7 +979,7 @@ const { data: posts } = useQueryGraphStep({ }, options: { throwIfKeyNotFound: true, - } + }, }) ``` @@ -1020,8 +1020,8 @@ const { data: products } = await query.graph({ fields: ["id", "title"], }, { cache: { - enable: true - } + enable: true, + }, }) ``` @@ -1034,9 +1034,9 @@ const { data: products } = useQueryGraphStep({ fields: ["id", "title"], options: { cache: { - enable: true - } - } + enable: true, + }, + }, }) ``` @@ -1114,7 +1114,7 @@ const { data: products } = await query.graph({ key: "products-123456", // to disable auto invalidation: // autoInvalidate: false, - } + }, }) ``` @@ -1131,8 +1131,8 @@ const { data: products } = useQueryGraphStep({ key: "products-123456", // to disable auto invalidation: // autoInvalidate: false, - } - } + }, + }, }) ``` @@ -1165,10 +1165,10 @@ const { data: products } = await query.graph({ key: async (args, cachingModuleService) => { return await cachingModuleService.computeKey({ ...args, - prefix: "products" + prefix: "products", }) - } - } + }, + }, }) ``` @@ -1198,7 +1198,7 @@ const { data: products } = await query.graph({ cache: { enable: true, tags: ["Product:list:*"], - } + }, }) ``` @@ -1215,8 +1215,8 @@ const { data: products } = useQueryGraphStep({ tags: ["Product:list:*"], // to disable auto invalidation: // autoInvalidate: false, - } - } + }, + }, }) ``` @@ -1253,7 +1253,7 @@ const { data: products } = await query.graph({ collectionId ? `ProductCollection:${collectionId}` : undefined, ] }, - } + }, }) ``` @@ -1280,7 +1280,7 @@ const { data: products } = await query.graph({ cache: { enable: true, ttl: 100, // 100 seconds - } + }, }) ``` @@ -1295,8 +1295,8 @@ const { data: products } = useQueryGraphStep({ cache: { enable: true, ttl: 100, // 100 seconds - } - } + }, + }, }) ``` @@ -1318,15 +1318,15 @@ const { data: products } = await query.graph({ entity: "product", fields: ["id", "title"], filters: { - id: "prod_123" - } + id: "prod_123", + }, }, { cache: { enable: true, ttl: (args) => { return args[0].filters.id === "test" ? 10 : 100 - } - } + }, + }, }) ``` @@ -1353,7 +1353,7 @@ const { data: products } = await query.graph({ cache: { enable: true, autoInvalidate: false, - } + }, }) ``` @@ -1368,8 +1368,8 @@ const { data: products } = useQueryGraphStep({ cache: { enable: true, autoInvalidate: false, - } - } + }, + }, }) ``` @@ -1395,8 +1395,8 @@ const { data: products } = await query.graph({ enable: true, autoInvalidate: (args) => { return !args[0].fields.includes("custom_field") - } - } + }, + }, }) ``` @@ -1428,8 +1428,8 @@ const { data: products } = await query.graph({ }, { cache: { enable: true, - providers: ["caching-redis", "caching-memcached"] - } + providers: ["caching-redis", "caching-memcached"], + }, }) ``` @@ -1443,9 +1443,9 @@ const { data: products } = useQueryGraphStep({ options: { cache: { enable: true, - providers: ["caching-redis", "caching-memcached"] - } - } + providers: ["caching-redis", "caching-memcached"], + }, + }, }) ``` @@ -1469,15 +1469,15 @@ const { data: products } = await query.graph({ entity: "product", fields: ["id", "title"], filters: { - id: "prod_123" - } + id: "prod_123", + }, }, { cache: { enable: true, providers: (args) => { return args[0].filters.id === "test" ? ["caching-redis"] : ["caching-memcached"] - } - } + }, + }, }) ``` diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 22fbf82beb..28dea8034c 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -65,7 +65,7 @@ export const generatedEditDates = { "app/learn/fundamentals/module-links/custom-columns/page.mdx": "2025-09-29T16:09:36.116Z", "app/learn/fundamentals/module-links/directions/page.mdx": "2025-03-17T12:52:06.161Z", "app/learn/fundamentals/module-links/page.mdx": "2025-04-17T08:50:17.036Z", - "app/learn/fundamentals/module-links/query/page.mdx": "2025-08-15T12:06:30.572Z", + "app/learn/fundamentals/module-links/query/page.mdx": "2025-10-27T09:30:26.957Z", "app/learn/fundamentals/modules/db-operations/page.mdx": "2025-10-09T11:43:28.746Z", "app/learn/fundamentals/modules/multiple-services/page.mdx": "2025-03-18T15:11:44.632Z", "app/learn/fundamentals/modules/page.mdx": "2025-10-09T11:41:57.515Z", @@ -93,7 +93,7 @@ export const generatedEditDates = { "app/learn/fundamentals/data-models/infer-type/page.mdx": "2025-03-18T07:41:01.936Z", "app/learn/fundamentals/custom-cli-scripts/seed-data/page.mdx": "2025-09-15T16:02:51.362Z", "app/learn/fundamentals/environment-variables/page.mdx": "2025-05-26T15:06:07.800Z", - "app/learn/build/page.mdx": "2025-10-17T14:48:44.767Z", + "app/learn/build/page.mdx": "2025-10-27T09:30:26.957Z", "app/learn/deployment/general/page.mdx": "2025-10-21T07:39:08.998Z", "app/learn/fundamentals/workflows/multiple-step-usage/page.mdx": "2025-08-01T14:59:59.501Z", "app/learn/installation/page.mdx": "2025-10-24T09:22:44.583Z", @@ -115,13 +115,13 @@ export const generatedEditDates = { "app/learn/configurations/ts-aliases/page.mdx": "2025-07-23T15:32:18.008Z", "app/learn/production/worker-mode/page.mdx": "2025-10-13T10:33:27.403Z", "app/learn/fundamentals/module-links/read-only/page.mdx": "2025-10-15T15:42:22.610Z", - "app/learn/fundamentals/data-models/properties/page.mdx": "2025-10-15T05:36:40.576Z", + "app/learn/fundamentals/data-models/properties/page.mdx": "2025-10-27T09:30:26.957Z", "app/learn/fundamentals/framework/page.mdx": "2025-06-26T14:26:22.120Z", "app/learn/fundamentals/api-routes/retrieve-custom-links/page.mdx": "2025-07-14T10:24:32.582Z", "app/learn/fundamentals/workflows/errors/page.mdx": "2025-04-25T14:26:25.000Z", "app/learn/fundamentals/api-routes/override/page.mdx": "2025-05-09T08:01:24.493Z", "app/learn/fundamentals/module-links/index/page.mdx": "2025-05-23T07:57:58.958Z", - "app/learn/fundamentals/module-links/index-module/page.mdx": "2025-10-01T06:07:40.436Z", + "app/learn/fundamentals/module-links/index-module/page.mdx": "2025-10-27T09:30:26.957Z", "app/learn/introduction/build-with-llms-ai/page.mdx": "2025-10-02T15:10:49.394Z", "app/learn/installation/docker/page.mdx": "2025-10-24T08:53:46.445Z", "app/learn/fundamentals/generated-types/page.mdx": "2025-07-25T13:17:35.319Z", @@ -134,5 +134,6 @@ export const generatedEditDates = { "app/learn/debugging-and-testing/feature-flags/page.mdx": "2025-09-02T08:36:12.714Z", "app/learn/fundamentals/workflows/locks/page.mdx": "2025-09-15T09:37:00.808Z", "app/learn/codemods/page.mdx": "2025-09-29T15:40:03.620Z", - "app/learn/codemods/replace-imports/page.mdx": "2025-10-09T11:37:44.754Z" + "app/learn/codemods/replace-imports/page.mdx": "2025-10-09T11:37:44.754Z", + "app/learn/fundamentals/admin/translations/page.mdx": "2025-10-27T09:29:59.965Z" } \ No newline at end of file diff --git a/www/apps/book/generated/sidebar.mjs b/www/apps/book/generated/sidebar.mjs index d07fb5d9fc..2c58de34f8 100644 --- a/www/apps/book/generated/sidebar.mjs +++ b/www/apps/book/generated/sidebar.mjs @@ -1053,6 +1053,16 @@ export const generatedSidebars = [ "chapterTitle": "4.5. Routing Customizations", "number": "4.5." }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/fundamentals/admin/translations", + "title": "Translations", + "children": [], + "chapterTitle": "4.6. Translations", + "number": "4.6." + }, { "loaded": true, "isPathHref": true, @@ -1060,8 +1070,8 @@ export const generatedSidebars = [ "path": "/learn/fundamentals/admin/constraints", "title": "Constraints", "children": [], - "chapterTitle": "4.6. Constraints", - "number": "4.6." + "chapterTitle": "4.7. Constraints", + "number": "4.7." }, { "loaded": true, @@ -1070,8 +1080,8 @@ export const generatedSidebars = [ "path": "/learn/fundamentals/admin/tips", "title": "Tips", "children": [], - "chapterTitle": "4.7. Tips", - "number": "4.7." + "chapterTitle": "4.8. Tips", + "number": "4.8." } ], "chapterTitle": "4. Admin Development", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index 2688d37516..dc5c6dc65b 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -72,8 +72,8 @@ module.exports = defineConfig({ cookieOptions: { sameSite: "lax", secure: false, - } - } + }, + }, }) ``` @@ -7413,6 +7413,433 @@ The Medusa Admin dashboard can be displayed in languages other than English, whi Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). +# Translate Admin Customizations + +In this chapter, you'll learn how to add translations to your Medusa Admin widgets and UI routes. + +Translations for admin customizations are available from [Medusa v2.11.1](https://github.com/medusajs/medusa/releases/tag/v2.11.1). + +## Translations in the Medusa Admin + +The Medusa Admin dashboard supports [multiple languages](https://docs.medusajs.com/user-guide/tips/languages/index.html.md) for its interface. Medusa uses [react-i18next](https://react.i18next.com/) to manage translations in the admin dashboard. + +Medusa Admin translations apply to the interface only. Medusa doesn't support translating store content, such as product names or descriptions. To implement localization for store content, [integrate a CMS](https://docs.medusajs.com/resources/integrations#cms/index.html.md). + +When you create [Widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) or [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) to customize the Medusa Admin, you can provide translations for the text content in your customizations, allowing users to view your customizations in their preferred language. + +You can add translations for your admin customizations within your Medusa project or as part of a plugin. + +*** + +## How to Add Translations to Admin Customizations + +### Step 1: Create Translation Files + +Translation files are JSON files containing key-value pairs, where the key identifies a text string and the value is the translated text or a nested object of translations. + +For example, to add English translations, create the file `src/admin/i18n/json/en.json` with the following content: + +English is the default language for Medusa Admin, so it's recommended to always include an English translation file. + +![Directory structure showing where to place translation files](https://res.cloudinary.com/dza7lstvk/image/upload/v1761555573/Medusa%20Book/translations-json_m2pvet.jpg) + +```json title="src/admin/i18n/json/en.json" +{ + "brands": { + "title": "Brands", + "description": "Manage your product brands" + }, + "done": "Done" +} +``` + +You can create additional translation files for other languages by following the same structure. For example, for Spanish, create `src/admin/i18n/json/es.json`: + +![Directory structure showing where to place translation files](https://res.cloudinary.com/dza7lstvk/image/upload/v1761555573/Medusa%20Book/translations-json-es_s1etna.jpg) + +```json title="src/admin/i18n/json/es.json" +{ + "brands": { + "title": "Marcas", + "description": "Gestiona las marcas de tus productos" + }, + "done": "Hecho" +} +``` + +### Step 2: Load Translation Files + +Next, to load the translation files, create the file `src/admin/i18n/index.ts` with the following content: + +![Directory structure showing i18n index file](https://res.cloudinary.com/dza7lstvk/image/upload/v1761555573/Medusa%20Book/translations-index_cgrj0t.jpg) + +```ts title="src/admin/i18n/index.ts" +import en from "./json/en.json" with { type: "json" } +import es from "./json/es.json" with { type: "json" } + +export default { + en: { + translation: en, + }, + es: { + translation: es, + }, +} +``` + +The `src/admin/i18n/index.ts` file imports the JSON translation files. You must include the `with { type: "json" }` directive to ensure the JSON files load correctly. + +The file exports an object that maps two-character language codes (like `en` and `es`) to their respective translation data. + +### Step 3: Use Translations in Admin Customizations + +Finally, you can use the translations in your admin customizations by using the `useTranslation` hook from `react-i18next`. + +The `react-i18next` package is already included in the Medusa Admin's dependencies, so you don't need to install it separately. However, `pnpm` users may need to install it manually due to package resolution issues. + +For example, create the file `src/admin/widgets/product-brand.tsx` with the following content: + +```tsx title="src/admin/widgets/product-brand.tsx" highlights={translationHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +const ProductWidget = () => { + const { t } = useTranslation() + return ( + +
+ {t("brands.title")} +

{t("brands.description")}

+
+
+ +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +In the above example, you retrieve the `t` function from the `useTranslation` hook. You then use this function to get the translated text by providing the appropriate keys defined in your translation JSON files. + +Nested keys are joined using dot notation. For example, `brands.title` refers to the `title` key inside the `brands` object in the translation files. + +### Test Translations + +To test the translations, start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +Then, go to a product details page in the Medusa Admin dashboard. If your default language is set to English, you'll see the widget displaying text in English. + +Next, [change the admin language](https://docs.medusajs.com/user-guide/settings/profile#edit-profile-details/index.html.md) to Spanish. The widget will now display the text in Spanish. + +*** + +## How Translations are Loaded + +When you load the translations with the `translation` key in `src/admin/i18n/index.ts`, your custom Medusa Admin translations are merged with the default Medusa Admin translations: + +- Translation keys in your custom translations override the default Medusa Admin translations. +- The default Medusa Admin translations are used as a fallback when a key is not defined in your custom translations. + +For example, consider the following widget and translation file: + +### Widget + +```tsx title="src/admin/widgets/product-brand.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +const ProductWidget = () => { + const { t } = useTranslation() + return ( + +
+ {/* Output: Custom Brands Title */} + {t("brands.title")} + {/* Output: brands.description */} +

{t("brands.description")}

+
+
+ {/* Output: Custom Save */} + + {/* Output: Delete */} + +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +### Translation File + +```json title="src/admin/i18n/json/en.json" +{ + "brands": { + "title": "Custom Brands Title" + }, + "actions": { + "save": "Custom Save" + } +} +``` + +### Loaded Translations + +```ts title="src/admin/i18n/index.ts" +import en from "./json/en.json" with { type: "json" } + +export default { + en: { + translation: en, + }, + // other languages... +} +``` + +The widget will render the following for each translation key: + +- `brands.title`: Defined in your custom translation file, so it outputs `Custom Brands Title`. +- `brands.description`: Not defined in your custom translation file or the default Medusa Admin translations, so it outputs the key itself: `brands.description`. +- `actions.save`: Defined in your custom translation file, so it outputs `Custom Save`. +- `actions.delete`: Not defined in your custom translation file, so it falls back to the default Medusa Admin translation and outputs `Delete`. + +### Custom Translation Namespaces + +To avoid potential key conflicts between your custom translations and the default Medusa Admin translations, you can use custom namespaces. This is particularly useful when developing plugins that add admin customizations, as it prevents naming collisions with other plugins or the default Medusa Admin translations. + +To add translations under a custom namespace, change the `[language].translation` key in the `src/admin/i18n/index.ts` file to your desired namespace: + +```ts title="src/admin/i18n/index.ts" highlights={namespacesHighlights} +import en from "./json/en.json" with { type: "json" } +import es from "./json/es.json" with { type: "json" } + +export default { + en: { + brands: en, + }, + es: { + brands: es, + }, +} +``` + +The translation files will now be loaded under the `brands` namespace. + +Then, in your admin customizations, specify the namespace when using the `useTranslation` hook: + +```tsx title="src/admin/widgets/product-brand.tsx" highlights={[["7"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +// The widget +const ProductWidget = () => { + const { t } = useTranslation("brands") + return ( + +
+ {t("brands.title")} +

{t("brands.description")}

+
+
+ +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +Translations are now loaded only from the `brands` namespace without conflicting with other translation keys in the Medusa Admin. + +*** + +## Translation Tips + +### Translation Organization + +To keep your translation files organized, especially as they grow, consider grouping related translation keys into nested objects. This helps maintain clarity and structure. + +It's recommended to create a nested object for each domain (for example, `brands`, `products`, etc...) and place related translation keys within those objects. This makes it easier to manage and locate specific translations. + +For example: + +```json title="src/admin/i18n/json/en.json" +{ + "brands": { + "title": "Brands", + "description": "Manage your product brands", + "actions": { + "add": "Add Brand", + "edit": "Edit Brand" + } + } +} +``` + +You can then access these nested translations using dot notation, such as `brands.title` or `brands.actions.add`. + +### Variables in Translations + +Translation values can include variables that are dynamically replaced at runtime. Variables are defined using double curly braces `{{variableName}}` in the translation files. + +For example, in your translation file `src/admin/i18n/json/en.json`, define a translation with a variable: + +```json title="src/admin/i18n/json/en.json" +{ + "welcome_message": "Welcome, {{username}}!" +} +``` + +Then, in your admin customization, pass the variable value in the second object parameter of the `t` function: + +```tsx title="src/admin/widgets/welcome-widget.tsx" +t("welcome_message", { username: "John" }) +``` + +This will output: `Welcome, John!` + +### Pluralization + +The `t` function supports pluralization based on a count value. You can define singular and plural forms in your translation files using the `_one`, `_other`, and `_zero` suffixes. + +For example, in your translation file `src/admin/i18n/json/en.json`, define the following translations: + +```json title="src/admin/i18n/json/en.json" +{ + "item_count_one": "You have {{count}} item.", + "item_count_other": "You have {{count}} items.", + "item_count_zero": "You have no items." +} +``` + +Then, in your admin customization, use the key without the suffix and provide the `count` variable: + +```tsx title="src/admin/widgets/item-count-widget.tsx" +t("item_count", { count: itemCount }) +``` + +This will render one of the following based on the value of `itemCount`: + +1. If `itemCount` is `0`, `item_count_zero` is used: `You have no items.` +2. If `itemCount` is `1`, `item_count_one` is used: `You have 1 item.` +3. If `itemCount` is greater than `1`, `item_count_other` is used: `You have X items.` + +### Element Interpolation + +Your translation strings can include HTML or React element placeholders that are replaced with actual elements at runtime. This is useful for adding links, bold text, or other formatting within translated strings. + +Elements to be interpolated are defined using angle brackets `` in the translation files, where `index` is a zero-based index representing the element's position. + +For example, in your translation file `src/admin/i18n/json/en.json`, define a translation with element placeholders: + +```json title="src/admin/i18n/json/en.json" +{ + "terms_and_conditions": "Please read our <0>Terms and Conditions." +} +``` + +Then, in your admin customization, import the `Trans` component from `react-i18next` that allows you to interpolate elements: + +```tsx title="src/admin/widgets/terms-widget.tsx" +import { Trans } from "react-i18next" +``` + +Finally, use the `Trans` component in the `return` statement to render the translation with the interpolated elements: + +```tsx title="src/admin/widgets/terms-widget.tsx" +, + ]} +/> +``` + +The `components` prop is an array of React elements that correspond to the placeholders defined in the translation string. In this case, the `<0>` placeholder is replaced with the anchor `
` element. + +#### Passing Variables with Element Interpolation + +You can also pass translation variables to the `Trans` component as props. For example, to include a username variable: + +```tsx title="src/admin/widgets/welcome-widget.tsx" +, + ]} +/> +``` + +The `username` prop replaces the `{{username}}` variable in the translation string, and the `<0>` placeholder is replaced with the `` element. + +#### Using Namespaces with Element Interpolation + +If you're loading translations from a custom namespace, specify the namespace in the `Trans` component using the `ns` prop: + +```tsx title="src/admin/widgets/product-brand.tsx" +]} +/> +``` + +The `ns` prop indicates that the translation should be loaded from the `brands` namespace. + +#### Multiple Element Interpolation + +You can interpolate multiple elements by defining multiple placeholders in the translation string and providing corresponding elements in the `components` array. + +For example, define the following translation string: + +```json title="src/admin/i18n/json/en.json" +{ + "welcome_message": "Hello, <0>{{username}}! Please read our <1>Terms and Conditions." +} +``` + +Then, in your admin customization, you can use the `Trans` component with multiple elements: + +```tsx title="src/admin/widgets/welcome-widget.tsx" +, + , + ]} +/> +``` + +The first placeholder `<0>` is replaced with the `` element, and the second placeholder `<1>` is replaced with the anchor `` element. + + # Admin UI Routes In this chapter, you’ll learn how to create a UI route in the admin dashboard. @@ -11237,7 +11664,7 @@ const Post = model.define("post", { { name: "limit_name_length", expression: (columns) => `LENGTH(${columns.name}) <= 50`, - } + }, ]) export default Post @@ -14025,8 +14452,8 @@ const { data: products } = await query.index({ fields: ["id", "title"], }, { cache: { - enable: true - } + enable: true, + }, }) ``` @@ -14079,7 +14506,7 @@ const { data: products } = await query.index({ key: "products-123456", // to disable auto invalidation: // autoInvalidate: false, - } + }, }) ``` @@ -14099,10 +14526,10 @@ const { data: products } = await query.index({ key: async (args, cachingModuleService) => { return await cachingModuleService.computeKey({ ...args, - prefix: "products" + prefix: "products", }) - } - } + }, + }, }) ``` @@ -14129,7 +14556,7 @@ const { data: products } = await query.index({ cache: { enable: true, tags: ["Product:list:*"], - } + }, }) ``` @@ -14153,7 +14580,7 @@ const { data: products } = await query.index({ collectionId ? `ProductCollection:${collectionId}` : undefined, ] }, - } + }, }) ``` @@ -14177,7 +14604,7 @@ const { data: products } = await query.index({ cache: { enable: true, ttl: 100, // 100 seconds - } + }, }) ``` @@ -14190,15 +14617,15 @@ const { data: products } = await query.index({ entity: "product", fields: ["id", "title"], filters: { - id: "prod_123" - } + id: "prod_123", + }, }, { cache: { enable: true, ttl: (args) => { return args[0].filters.id === "test" ? 10 : 100 - } - } + }, + }, }) ``` @@ -14222,7 +14649,7 @@ const { data: products } = await query.index({ cache: { enable: true, autoInvalidate: false, - } + }, }) ``` @@ -14239,8 +14666,8 @@ const { data: products } = await query.index({ enable: true, autoInvalidate: (args) => { return !args[0].fields.includes("custom_field") - } - } + }, + }, }) ``` @@ -14265,8 +14692,8 @@ const { data: products } = await query.index({ }, { cache: { enable: true, - providers: ["caching-redis", "caching-memcached"] - } + providers: ["caching-redis", "caching-memcached"], + }, }) ``` @@ -14281,15 +14708,15 @@ const { data: products } = await query.index({ entity: "product", fields: ["id", "title"], filters: { - id: "prod_123" - } + id: "prod_123", + }, }, { cache: { enable: true, providers: (args) => { return args[0].filters.id === "test" ? ["caching-redis"] : ["caching-memcached"] - } - } + }, + }, }) ``` @@ -15691,7 +16118,7 @@ const { ```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]} const { data: posts, - metadata + metadata, } = useQueryGraphStep({ entity: "post", fields: ["id", "title"], @@ -15896,7 +16323,7 @@ const { data: posts } = useQueryGraphStep({ }, options: { throwIfKeyNotFound: true, - } + }, }) ``` @@ -15937,7 +16364,7 @@ const { data: posts } = useQueryGraphStep({ }, options: { throwIfKeyNotFound: true, - } + }, }) ``` @@ -15965,8 +16392,8 @@ const { data: products } = await query.graph({ fields: ["id", "title"], }, { cache: { - enable: true - } + enable: true, + }, }) ``` @@ -15978,9 +16405,9 @@ const { data: products } = useQueryGraphStep({ fields: ["id", "title"], options: { cache: { - enable: true - } - } + enable: true, + }, + }, }) ``` @@ -16035,7 +16462,7 @@ const { data: products } = await query.graph({ key: "products-123456", // to disable auto invalidation: // autoInvalidate: false, - } + }, }) ``` @@ -16051,8 +16478,8 @@ const { data: products } = useQueryGraphStep({ key: "products-123456", // to disable auto invalidation: // autoInvalidate: false, - } - } + }, + }, }) ``` @@ -16074,10 +16501,10 @@ const { data: products } = await query.graph({ key: async (args, cachingModuleService) => { return await cachingModuleService.computeKey({ ...args, - prefix: "products" + prefix: "products", }) - } - } + }, + }, }) ``` @@ -16106,7 +16533,7 @@ const { data: products } = await query.graph({ cache: { enable: true, tags: ["Product:list:*"], - } + }, }) ``` @@ -16122,8 +16549,8 @@ const { data: products } = useQueryGraphStep({ tags: ["Product:list:*"], // to disable auto invalidation: // autoInvalidate: false, - } - } + }, + }, }) ``` @@ -16149,7 +16576,7 @@ const { data: products } = await query.graph({ collectionId ? `ProductCollection:${collectionId}` : undefined, ] }, - } + }, }) ``` @@ -16175,7 +16602,7 @@ const { data: products } = await query.graph({ cache: { enable: true, ttl: 100, // 100 seconds - } + }, }) ``` @@ -16189,8 +16616,8 @@ const { data: products } = useQueryGraphStep({ cache: { enable: true, ttl: 100, // 100 seconds - } - } + }, + }, }) ``` @@ -16205,15 +16632,15 @@ const { data: products } = await query.graph({ entity: "product", fields: ["id", "title"], filters: { - id: "prod_123" - } + id: "prod_123", + }, }, { cache: { enable: true, ttl: (args) => { return args[0].filters.id === "test" ? 10 : 100 - } - } + }, + }, }) ``` @@ -16239,7 +16666,7 @@ const { data: products } = await query.graph({ cache: { enable: true, autoInvalidate: false, - } + }, }) ``` @@ -16253,8 +16680,8 @@ const { data: products } = useQueryGraphStep({ cache: { enable: true, autoInvalidate: false, - } - } + }, + }, }) ``` @@ -16273,8 +16700,8 @@ const { data: products } = await query.graph({ enable: true, autoInvalidate: (args) => { return !args[0].fields.includes("custom_field") - } - } + }, + }, }) ``` @@ -16301,8 +16728,8 @@ const { data: products } = await query.graph({ }, { cache: { enable: true, - providers: ["caching-redis", "caching-memcached"] - } + providers: ["caching-redis", "caching-memcached"], + }, }) ``` @@ -16315,9 +16742,9 @@ const { data: products } = useQueryGraphStep({ options: { cache: { enable: true, - providers: ["caching-redis", "caching-memcached"] - } - } + providers: ["caching-redis", "caching-memcached"], + }, + }, }) ``` @@ -16334,15 +16761,15 @@ const { data: products } = await query.graph({ entity: "product", fields: ["id", "title"], filters: { - id: "prod_123" - } + id: "prod_123", + }, }, { cache: { enable: true, providers: (args) => { return args[0].filters.id === "test" ? ["caching-redis"] : ["caching-memcached"] - } - } + }, + }, }) ``` @@ -27234,7 +27661,7 @@ export async function POST( Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by: -1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). +1. Obtaining a token using the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). 2. Passing the token in the bearer header of the request to this route. In the API route, you create the manager using the workflow from the previous section and return it in the response. @@ -27562,7 +27989,7 @@ Learn more about why modules are isolated in [this documentation](https://docs.m - [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials. - [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md). - [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types. -- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors. +- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providers. *** @@ -28460,7 +28887,7 @@ In this document, you’ll learn about the main concepts related to carts in Med A cart is the selection of product variants that a customer intends to purchase. It is represented by the [Cart data model](https://docs.medusajs.com/references/cart/models/Cart/index.html.md). -A cart holds informations about: +A cart holds information about: - The items the customer wants to buy. - The customer's shipping and billing addresses. @@ -34094,7 +34521,7 @@ import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" // ... -const { result: paymentSesion } = await createPaymentSessionsWorkflow(container) +const { result: paymentSession } = await createPaymentSessionsWorkflow(container) .run({ input: { payment_collection_id: "paycol_123", @@ -35379,7 +35806,7 @@ Each rule of a price is represented by the [PriceRule data model](https://docs.m The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. -For exmaple, you create a price restricted to `10557` zip codes. +For example, you create a price restricted to `10557` zip codes. ![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) @@ -42379,9 +42806,9 @@ class MemcachedCachingProviderService implements ICachingProviderService { const tagResult = tagResults[i] if (tagResult.value) { - const currrentTag = tagResult.value.toString() + const currentTag = tagResult.value.toString() // If the namespace has changed since the key was stored, it's invalid - if (currrentTag !== storedTags[tag]) { + if (currentTag !== storedTags[tag]) { return false } } else { @@ -45055,7 +45482,7 @@ Refer to the [Events Reference](https://docs.medusajs.com/references/events/inde You can also send an email using SendGrid in any [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). This allows you to send emails within your custom flows. -You can use the [sendNotifcationStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow to send an email using SendGrid. +You can use the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow to send an email using SendGrid. For example: @@ -45195,7 +45622,7 @@ The Locking Module uses module providers that implement the underlying logic of ## Notification Module -The Notification Module handles sending notifications to users or customers, such as reset password instructions or newsletters. Refer to the [Notifcation Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) to learn more about it. +The Notification Module handles sending notifications to users or customers, such as reset password instructions or newsletters. Refer to the [Notification Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) to learn more about it. The Notification Module has module providers that implement the underlying logic of sending notifications, typically through integrating a third-party service. The following modules are provided by Medusa. You can also create a custom provider as explained in the [Create Notification Module Provider guide](https://docs.medusajs.com/references/notification-provider-module/index.html.md). @@ -45212,7 +45639,7 @@ The Notification Module has module providers that implement the underlying logic ## Workflow Engine Module -A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. Learn more about it in the [Worklow Engine Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md). +A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. Learn more about it in the [Workflow Engine Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md). The following Workflow Engine modules are provided by Medusa. @@ -48004,7 +48431,7 @@ medusa new [ []] |Option|Description| |---|---|---| -|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| +|\`-y\`|Skip all prompts, such as database prompts. A database might not be created if default PostgreSQL credentials don't work.| |\`--skip-db\`|Skip database creation.| |\`--skip-env\`|Skip populating | |\`--db-user \\`|The database user to use for database setup.| @@ -48410,7 +48837,7 @@ medusa new [ []] |Option|Description| |---|---|---| -|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| +|\`-y\`|Skip all prompts, such as database prompts. A database might not be created if default PostgreSQL credentials don't work.| |\`--skip-db\`|Skip database creation.| |\`--skip-env\`|Skip populating | |\`--db-user \\`|The database user to use for database setup.| @@ -48710,7 +49137,7 @@ export const sdk = new Medusa({ auth: { type: "jwt", jwtTokenStorageMethod: "custom", - storge: AsyncStorage, + storage: AsyncStorage, }, }) ``` @@ -50719,7 +51146,7 @@ It accepts as a second parameter a constructor function, which is the workflow's In the workflow's constructor function, you use `useQueryGraphStep` to retrieve the cart and customer details using the IDs passed as an input to the workflow. -`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), whic allows you to retrieve data across modules. For example, in the above snippet you're retrieving the cart's promotions, which are managed in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md), by passing `promotions.code` to the `fields` array. +`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which allows you to retrieve data across modules. For example, in the above snippet you're retrieving the cart's promotions, which are managed in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md), by passing `promotions.code` to the `fields` array. Next, you want to create the draft order for the quote. Replace the `TODO` in the workflow with the following: @@ -51496,7 +51923,7 @@ export type AdminQuoteResponse = { You define the following types: - `AdminQuote`: Represents a quote. -- `QuoteQueryParams`: Represents the query parameters that can be passed when retrieving qoutes. +- `QuoteQueryParams`: Represents the query parameters that can be passed when retrieving quotes. - `AdminQuotesResponse`: Represents the response when retrieving a list of quotes. - `AdminQuoteResponse`: Represents the response when retrieving a single quote, which you'll implement later in this guide. @@ -51588,9 +52015,9 @@ export const config = defineRouteConfig({ export default Quotes ``` -The route file must export a React component that implements the content of the page. To show a link to the route in the sidebar, you can also export a configuation object created with `defineRouteConfig` that specifies the label and icon of the route in the Medusa Admin sidebar. +The route file must export a React component that implements the content of the page. To show a link to the route in the sidebar, you can also export a configuration object created with `defineRouteConfig` that specifies the label and icon of the route in the Medusa Admin sidebar. -In the `Quotes` component, you'll show a table of quotes using the [DataTable component](https://docs.medusajs.com/ui/components/data-table/index.html.md) from Medusa UI. This componet requires you first define the columns of the table. +In the `Quotes` component, you'll show a table of quotes using the [DataTable component](https://docs.medusajs.com/ui/components/data-table/index.html.md) from Medusa UI. This component requires you first define the columns of the table. To define the table's columns, add in the same file and before the `Quotes` component the following: @@ -51714,7 +52141,7 @@ In the component, you use the `useQuotes` hook to fetch the quotes from the Medu Next, you use the `useDataTable` hook to create a table instance with the columns you defined. You pass the fetched quotes to the `DataTable` component, along with configurations related to pagination and loading. -Notice that as part of the `useDataTable` configurations you naviagte to the `/quotes/:id` UI route when a row is clicked. You'll create that route in a later step. +Notice that as part of the `useDataTable` configurations you navigate to the `/quotes/:id` UI route when a row is clicked. You'll create that route in a later step. Finally, you render the `DataTable` component to display the quotes in a table. @@ -56514,7 +56941,7 @@ const { transaction } = await myLongRunningWorkflow(req.scope) .run() ``` -2. In an API route, workflow, or other resource, change a step's status to successful using the [Worfklow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md): +2. In an API route, workflow, or other resource, change a step's status to successful using the [Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md): ```ts highlights={stepSuccessHighlights} const workflowEngineService = container.resolve( @@ -56954,7 +57381,7 @@ Where the first parameter of `registerAdd` is the name to register the resource You can customize the Medusa Admin to inject widgets in existing pages, or create new pages using UI routes. -For a list of components to use in the admin dashboard, refere to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/index.html.md). +For a list of components to use in the admin dashboard, refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/index.html.md). ### Create Widget @@ -65975,7 +66402,7 @@ In this step, you'll create a subscriber that listens to the `order.placed` even Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more. -Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the fle `src/subscribers/order-placed.ts` with the following content: +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the file `src/subscribers/order-placed.ts` with the following content: ```ts title="src/subscribers/order-placed.ts" import type { @@ -66656,7 +67083,7 @@ The workflow will have the following steps: - [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. - [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions to remove the loyalty promotion. - [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to remove the loyalty promotion ID from the metadata. -- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactive the loyalty promotion. +- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactivate the loyalty promotion. - [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again. Since you already have all the steps, you can create the workflow. @@ -66939,7 +67366,7 @@ completeCartWorkflow.hooks.validate( ) ``` -Workflows have a special `hooks` property that includes all the hooks tht you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property. +Workflows have a special `hooks` property that includes all the hooks that you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property. Since the hook is essentially a step function, it accepts the following parameters: @@ -75699,7 +76126,7 @@ You display a separate section for each custom field, complementary product, and When a customer chooses complementary and addon products, the price shown on the product page should reflect that selection. So, you need to modify the pricing component to accept the builder configuration, and update the displayed price accordingly. -In `src/modules/products/components/product-price/index.tsx`, add the followng imports at the top of the file: +In `src/modules/products/components/product-price/index.tsx`, add the following imports at the top of the file: ```tsx title="src/modules/products/components/product-price/index.tsx" badgeLabel="Storefront" badgeColor="blue" import { BuilderConfiguration } from "../../../../types/global" @@ -76069,7 +76496,7 @@ npm run dev You should see the custom fields, complementary products, and addons on the product's page. -![Product details page with builder configuations](https://res.cloudinary.com/dza7lstvk/image/upload/v1755101580/Medusa%20Resources/CleanShot_2025-08-13_at_19.12.48_2x_tf4goa.png) +![Product details page with builder configurations](https://res.cloudinary.com/dza7lstvk/image/upload/v1755101580/Medusa%20Resources/CleanShot_2025-08-13_at_19.12.48_2x_tf4goa.png) While you can enter custom values and select variants, you still can't add the product variant with its builder configurations to the cart. You'll support that in the next step. @@ -76942,7 +77369,7 @@ You changed the `text-ui-fg-muted` class in the wrapper `div` to `text-ui-fg-sub #### Update Item Component -Next, you'll update the component showing a line item row. This component is used in mutliple places, including the cart and checkout pages. +Next, you'll update the component showing a line item row. This component is used in multiple places, including the cart and checkout pages. You'll update the component to ignore addon products. Instead, you'll show them as part of the main product line item. You'll also display the custom field values of the main product. @@ -79936,7 +80363,7 @@ Finally, you return the reviews with pagination fields and the average rating of ### Apply Query Configurations Middleware -The last step is to add a middleware that validates the query parameters passed to the request, and sets the default Query configuations. +The last step is to add a middleware that validates the query parameters passed to the request, and sets the default Query configurations. In `src/api/middlewares.ts`, add a new middleware: @@ -81902,7 +82329,7 @@ A module has a container that holds all resources registered in that module, and #### Index Data Method -The first method you need to add to the servie is a method that receives an array of data to add or update in Algolia's index. +The first method you need to add to the service is a method that receives an array of data to add or update in Algolia's index. Add the following methods to the `AlgoliaModuleService` class: @@ -82205,7 +82632,7 @@ In the compensation function, you resolve the Algolia Module's service from the ### Add Sync Products Workflow -You can now create the worklow that syncs the products to Algolia. +You can now create the workflow that syncs the products to Algolia. To create the workflow, create the file `src/workflows/sync-products.ts` with the following content: @@ -86610,7 +87037,7 @@ Then, you format the retrieved products to: - Pass the product's ID in the `product_id` property. This is essential to map a product in Medusa to its entry in Contentful. - Remove the circular references in the product's variants, options, and values to avoid infinite loops. -To paginate the retrieved products, implemet a `listAndCount` method as explained in the [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context#using-pagination-with-query/index.html.md) documentation. +To paginate the retrieved products, implement a `listAndCount` method as explained in the [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context#using-pagination-with-query/index.html.md) documentation. ### Retrieve Product Details for Locale API Route @@ -87204,7 +87631,7 @@ In the workflow, you: - Use a `when` condition to check if the entry type is a `productVariant`, and if so, update the product variant using the `updateProductVariantsWorkflow`. - Use a `when` condition to check if the entry type is a `productOption`, and if so, update the product option using the `updateProductOptionsWorkflow`. -You can't perform data manipulation in a workflow's constructor function. Instead, the Workflows SDK includes utility functions like `when` to perform typical operations that requires accessing data values. Learn more about workflow constraints in the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documetation. +You can't perform data manipulation in a workflow's constructor function. Instead, the Workflows SDK includes utility functions like `when` to perform typical operations that requires accessing data values. Learn more about workflow constraints in the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. ### Add the Webhook Listener API Route @@ -88028,7 +88455,7 @@ export const migrateProductsFromMagentoWorkflow = createWorkflow( You create a workflow using `createWorkflow` from the Workflows SDK. It accepts two parameters: 1. An object with the workflow's configuration, including the name and whether to store the workflow's executions. You enable storing the workflow execution so that you can view it later in the Medusa Admin dashboard. -2. A worflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters. +2. A workflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters. In the workflow constructor function, you use the `getMagentoProductsStep` step to retrieve the products from Magento, passing it the pagination parameters from the workflow's input. @@ -88060,7 +88487,7 @@ const { data: shippingProfiles } = useQueryGraphStep({ You use the `useQueryGraphStep` step to retrieve the store details and shipping profiles. `useQueryGraphStep` is a Medusa step that wraps [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), allowing you to use it in a workflow. Query is a tool that retrieves data across modules. -Whe retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile. +When retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile. Next, you'll retrieve products that were previously migrated from Magento to determine which products to create or update. Replace the `TODO` with the following: @@ -95268,7 +95695,7 @@ A subscriber file exports: - An asynchronous function that's executed whenever the associated event is emitted, which is the `order.placed` event. - A configuration object with an `event` property indicating the event the subscriber is listening to. -The subscriber function accepts the event's details as a first paramter which has a `data` property that holds the data payload of the event. For example, Medusa emits the `order.placed` event with the order's ID in the data payload. The function also accepts as a second parameter the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +The subscriber function accepts the event's details as a first parameter which has a `data` property that holds the data payload of the event. For example, Medusa emits the `order.placed` event with the order's ID in the data payload. The function also accepts as a second parameter the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). In the function, you execute the `sendOrderConfirmationWorkflow` by invoking it, passing it the `container`, then using its `run` method. The `run` method accepts an object having an `input` property, which is the input to pass to the workflow. You pass the ID of the placed order as received in the event's data payload. @@ -95839,7 +96266,7 @@ Where: - Enter a name for the API token, choose "Editor" for the permissions, then click Save. -![In the Token form, enter the name and choose "Editor" for permisions.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091811/Medusa%20Resources/Screenshot_2024-11-20_at_10.36.25_AM_nqxa5y.png) +![In the Token form, enter the name and choose "Editor" for permissions.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091811/Medusa%20Resources/Screenshot_2024-11-20_at_10.36.25_AM_nqxa5y.png) - `SANITY_PROJECT_ID`: The ID of the project, which you can find at the top section of your Sanity project's dashboard. @@ -97021,7 +97448,7 @@ export const useSanitySyncs = ( The `useTriggerSanitySync` hook creates a mutation that, when executed, sends a request to the trigger sync API route you created earlier to sync all products. -The `useSanitySyncs` hook sends a request to the retrieve sync executions API route that you created earlier to retrieve the workflow's exections. +The `useSanitySyncs` hook sends a request to the retrieve sync executions API route that you created earlier to retrieve the workflow's executions. Finally, to create the UI route, create the file `src/admin/routes/sanity/page.tsx` with the following content: @@ -98383,7 +98810,7 @@ export class ShipStationClient { if (typeof resp !== "string" && resp.errors?.length) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `An error occured while sending a request to ShipStation: ${ + `An error occurred while sending a request to ShipStation: ${ resp.errors.map((error) => error.message) }` ) @@ -98689,7 +99116,7 @@ export class ShipStationClient { if (resp.rate_response.errors?.length) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `An error occured while retrieving rates from ShipStation: ${ + `An error occurred while retrieving rates from ShipStation: ${ resp.rate_response.errors.map((error) => error.message) }` ) @@ -99293,7 +99720,7 @@ Where `SHIPSTATION_API_KEY` is the ShipStation API key, which you can retrieve o ![The sidebar has an Account expandable. When you click on it, more items will show. Click on the API Settings](https://res.cloudinary.com/dza7lstvk/image/upload/v1734352145/Medusa%20Resources/Screenshot_2024-12-16_at_2.28.32_PM_wwfc1s.png). -- On the API Settings page, make sure V2 API is selected for "Select API Verion" field, then click the "Generate API Key" button. +- On the API Settings page, make sure V2 API is selected for "Select API Version" field, then click the "Generate API Key" button. ![Make sure V2 API is selected in the Select API Version dropdown, then click on the "Generate API Key" button.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734352261/Medusa%20Resources/Screenshot_2024-12-16_at_2.30.31_PM_vbkz4i.png) @@ -100645,7 +101072,7 @@ The second step in the workflow will create a wishlist for the customer. To crea ![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467998/Medusa%20Resources/wishlist-10_xex4d0.jpg) -```ts title="src/workflows/steps/create-wishlist.ts" highlights={createWishlistStepHiglights} +```ts title="src/workflows/steps/create-wishlist.ts" highlights={createWishlistStepHighlights} import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" import { WISHLIST_MODULE } from "../../modules/wishlist" import WishlistModuleService from "../../modules/wishlist/service" @@ -101840,7 +102267,7 @@ export default class WishlistModuleService extends MedusaService({ To perform queries on the database in a method, add the `@InjectManager` decorator to the method. This will inject a [forked MikroORM entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager) that you can use in your method. -Methods with the `@InjectManager` decorator accept as a last parameter a context object that has the `@MedusaContext` decorator. The entity manager is injected into the `manager` property of this paramter. +Methods with the `@InjectManager` decorator accept as a last parameter a context object that has the `@MedusaContext` decorator. The entity manager is injected into the `manager` property of this parameter. The method accepts an array of variant IDs as a parameter. In the method, you use the `createQueryBuilder` to construct a query, passing it the name of the `WishlistItem`'s table. You then select distinct `wishlist_id`s where the `product_variant_id` of the wishlist item is in the array of variant IDs. @@ -119657,7 +120084,7 @@ Now that you have a working bundled product feature, you can customize it furthe - Add API routes to update the bundled product and its items in the cart. - Add more CRUD management features to the Bundled Products page in the Medusa Admin. -- Customize the Next.js Starter Storefront to show the bundled products together in the cart, rather than seperately. +- Customize the Next.js Starter Storefront to show the bundled products together in the cart, rather than separately. - Use custom logic to set the price of the bundled product. If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. @@ -120797,7 +121224,7 @@ export default RestockModuleService To perform queries on the database in a method, add the `@InjectManager` decorator to the method. This will inject a [forked MikroORM entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager) that you can use in your method. -Methods with the `@InjectManager` decorator accept as a last parameter a context object that has the `@MedusaContext` decorator. The entity manager is injected into the `manager` property of this paramter. +Methods with the `@InjectManager` decorator accept as a last parameter a context object that has the `@MedusaContext` decorator. The entity manager is injected into the `manager` property of this parameter. In the method, you use the `createQueryBuilder` to construct a query, passing it the name of the `RestockSubscription`'s table. You then select distinct `variant_id` and `sales_channel` pairings, and execute and return the query's result. @@ -124339,7 +124766,7 @@ Our documentation also provides a step-by-step guides to deploy your Medusa appl Along with the extensive ecommerce features, Medusa also provides the architecture and Framework to customize your application and build custom features that are tailored for your business use case. -To learn how to develop customziations with Medusa, refer to the [Get Started](https://docs.medusajs.com/docs/learn/index.html.md) documentation. +To learn how to develop customizations with Medusa, refer to the [Get Started](https://docs.medusajs.com/docs/learn/index.html.md) documentation. # Integrate Odoo with Medusa @@ -129713,7 +130140,7 @@ In the workflow function, you run the following steps: 1. `createVendorStep` to create the vendor. 2. `createVendorAdminStep` to create the vendor admin. - - Notice that you use `transform` from the Workflows SDK to prepare the data you pass into the step. Medusa doesn't allow direct manipulation of variables within the worflow's constructor function. Learn more in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md). + - Notice that you use `transform` from the Workflows SDK to prepare the data you pass into the step. Medusa doesn't allow direct manipulation of variables within the workflow's constructor function. Learn more in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md). 3. `setAuthAppMetadataStep` to associate the vendor admin with its auth identity of actor type `vendor`. This will allow the vendor admin to send authenticated requests afterwards. 4. `useQueryGraphStep` to retrieve the created vendor with its admins using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). Query allows you to retrieve data across modules. @@ -130466,7 +130893,7 @@ try { ) } catch (e) { return StepResponse.permanentFailure( - `An error occured while creating vendor orders: ${e}`, + `An error occurred while creating vendor orders: ${e}`, { created_orders: createdOrders, } @@ -134084,7 +134511,7 @@ You create a `getLatestPaymentMethod` function that receives an array of payment Then, you create the `getPaymentMethodStep` that receives the customer's data and account holder as an input. -Next, you'll add the implemenation of the step. Replace `getPaymentMethodStep` with the following: +Next, you'll add the implementation of the step. Replace `getPaymentMethodStep` with the following: ```tsx title="src/workflows/create-subscription-order/steps/get-payment-method.ts" export const getPaymentMethodStep = createStep( diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index a485a42451..3114788b00 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -554,6 +554,11 @@ export const sidebars = [ path: "/learn/fundamentals/admin/routing", title: "Routing Customizations", }, + { + type: "link", + path: "/learn/fundamentals/admin/translations", + title: "Translations", + }, { type: "link", path: "/learn/fundamentals/admin/constraints",