docs: documented admin translations (#13864)

This commit is contained in:
Shahed Nasser
2025-10-27 12:04:31 +02:00
committed by GitHub
parent ec44432876
commit a9dbd035a5
9 changed files with 1091 additions and 184 deletions

View File

@@ -86,8 +86,8 @@ module.exports = defineConfig({
cookieOptions: {
sameSite: "lax",
secure: false,
}
}
},
},
})
```

View File

@@ -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.
<Note>
Translations for admin customizations are available from [Medusa v2.11.1](https://github.com/medusajs/medusa/releases/tag/v2.11.1).
</Note>
## 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.
<Note>
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).
</Note>
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:
<Note>
English is the default language for Medusa Admin, so it's recommended to always include an English translation file.
</Note>
![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`.
<Note>
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.
</Note>
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 (
<Container className="p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("brands.title")}</Heading>
<p>{t("brands.description")}</p>
</div>
<div className="flex justify-end px-6 py-4">
<Button variant="primary">{t("done")}</Button>
</div>
</Container>
)
}
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:
<CodeTabs group="widget-translation">
<CodeTab label="Widget" value="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 (
<Container className="p-0">
<div className="flex items-center justify-between px-6 py-4">
{/* Output: Custom Brands Title */}
<Heading level="h2">{t("brands.title")}</Heading>
{/* Output: brands.description */}
<p>{t("brands.description")}</p>
</div>
<div className="flex justify-end px-6 py-4">
{/* Output: Custom Save */}
<Button variant="primary">{t("actions.save")}</Button>
{/* Output: Delete */}
<Button variant="primary">{t("actions.delete")}</Button>
</div>
</Container>
)
}
export const config = defineWidgetConfig({
zone: "product.details.before",
})
export default ProductWidget
```
</CodeTab>
<CodeTab label="Translation File" value="translation-file">
```json title="src/admin/i18n/json/en.json"
{
"brands": {
"title": "Custom Brands Title"
},
"actions": {
"save": "Custom Save"
}
}
```
</CodeTab>
<CodeTab label="Loaded Translations" value="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...
}
```
</CodeTab>
</CodeTabs>
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 (
<Container className="p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("brands.title")}</Heading>
<p>{t("brands.description")}</p>
</div>
<div className="flex justify-end px-6 py-4">
<Button variant="primary">{t("done")}</Button>
</div>
</Container>
)
}
// 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 `<index></index>` 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</0>."
}
```
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"
<Trans
i18nKey="terms_and_conditions"
components={[
<a href="https://example.com/terms" className="text-blue-600 underline" />,
]}
/>
```
The `components` prop is an array of React elements that correspond to the placeholders defined in the translation string. In this case, the `<0></0>` placeholder is replaced with the anchor `<a>` 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"
<Trans
i18nKey="welcome_message"
username="John"
components={[
<strong />,
]}
/>
```
The `username` prop replaces the `{{username}}` variable in the translation string, and the `<0></0>` placeholder is replaced with the `<strong>` 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"
<Trans
i18nKey="brands.count"
ns="brands"
count={5}
components={[<strong />]}
/>
```
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}}</0>! Please read our <1>Terms and Conditions</1>."
}
```
Then, in your admin customization, you can use the `Trans` component with multiple elements:
```tsx title="src/admin/widgets/welcome-widget.tsx"
<Trans
i18nKey="welcome_message"
username="John"
components={[
<strong />,
<a href="https://example.com/terms" className="text-blue-600 underline" />,
]}
/>
```
The first placeholder `<0></0>` is replaced with the `<strong>` element, and the second placeholder `<1></1>` is replaced with the anchor `<a>` element.

View File

@@ -79,7 +79,7 @@ const Post = model.define("post", {
{
name: "limit_name_length",
expression: (columns) => `LENGTH(${columns.name}) <= 50`,
}
},
])
export default Post

View File

@@ -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"]
}
}
},
},
})
```

View File

@@ -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"]
}
}
},
},
})
```

View File

@@ -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"
}

View File

@@ -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",

File diff suppressed because it is too large Load Diff

View File

@@ -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",