docs: docs for next release (#14456)

This commit is contained in:
Shahed Nasser
2026-01-06 17:40:26 +02:00
committed by GitHub
parent a960fb75c9
commit a2210ea5e7
19 changed files with 1434 additions and 64 deletions
@@ -0,0 +1,211 @@
import { CodeTabs, CodeTab, Prerequisites } from "docs-ui"
export const metadata = {
title: `${pageNumber} Localization in API Routes`,
keywords: ["translation api-routes"]
}
# {metadata.title}
In this chapter, you'll learn how to handle localization in API routes of your Medusa application to serve content in different languages.
<Prerequisites
items={[
{
text: "Medusa v2.12.4 or later",
link: "https://github.com/medusajs/medusa/releases/tag/v2.12.4"
},
{
text: "Translation Module Configured",
link: "!resources!/commerce-modules/translation#configure-translation-module",
},
]}
/>
## Overview
Localization in API routes allows you to serve translated content based on the user's preferred language. The Medusa application provides built-in support for handling locale information in API requests and retrieving localized data.
When a locale is specified in a request, you can use it to retrieve translated versions of your data models' fields, providing a seamless multilingual experience for your users.
Learn more about translation, how to manage translations, and how to translate custom data models in the [Translation Module documentation](!resources!/commerce-modules/translation).
---
## Routes with Localization Enabled by Default
The Medusa application automatically supports retrieving localized content from all routes under the `/store` prefix, including both core and custom store API routes.
For example, the following store routes have localization enabled by default:
- `/store/products` -> Get products with translated fields
- `/store/collections` -> Get collections with translated fields
- `/store/categories` -> Get categories with translated fields
Refer to the [Translation Module](!resources!/commerce-modules/translation#supported-module-translations) documentation for a list of supported core data models with localization support.
### Apply Localization to Custom Routes
If you're creating custom API routes outside the `/store` prefix, you must manually apply the `applyLocale` [middleware](../middlewares/page.mdx) to enable localization support.
To apply the `applyLocale` middleware to all HTTP methods for a route, add it to the `src/api/middlewares.ts` file:
export const allMethodsHighlights = [
["7", "applyLocale", "Apply the middleware to the route"],
]
```ts title="src/api/middlewares.ts" highlights={allMethodsHighlights}
import { applyLocale, defineMiddlewares } from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom*",
middlewares: [applyLocale],
},
],
})
```
This applies the `applyLocale` middleware to all routes matching `/custom*`, regardless of the HTTP method.
Alternatively, you can apply the middleware only to specific HTTP methods using the `method` property:
export const specificMethodHighlights = [
["7", "method", "Apply the middleware only to `GET` requests"],
]
```ts title="src/api/middlewares.ts" highlights={specificMethodHighlights}
import { applyLocale, defineMiddlewares } from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom*",
method: ["GET"],
middlewares: [applyLocale],
},
],
})
```
<Note title="Tip">
Learn more about middlewares in the [Middlewares](../middlewares/page.mdx) chapter.
</Note>
---
## How to Pass Locale in API Requests
You can pass the locale in API requests to routes that support localization using either of the following methods:
1. The `locale` query parameter
2. The `x-medusa-locale` request header
The query parameter takes priority over the header if both are provided.
The locale must follow the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1), such as `en-US` for English (United States) or `fr-FR` for French (France).
<Note title="Using the JS SDK?">
Refer to the [JS SDK reference](!resources!/js-sdk#localization-with-js-sdk) for details on how to pass locale.
</Note>
For example:
<CodeTabs group="localization">
<CodeTab label="Query Parameter" value="query-param">
```bash
curl "http://localhost:9000/store/products?locale=fr-FR" \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```
</CodeTab>
<CodeTab label="Header" value="header">
```bash
curl "http://localhost:9000/store/products" \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H 'x-medusa-locale: fr-FR'
```
</CodeTab>
</CodeTabs>
The above examples retrieve products with their fields translated to French (France) if translations are available. If no translations exist for the requested locale, the original content stored in the data model is returned.
<Note>
Store API routes require a publishable API key in the request header. Learn more in the [Store API reference](!api!/store#publishable-api-key).
</Note>
---
## Access Request Locale in API Routes
After applying the `applyLocale` middleware, you can access the request's locale from the `locale` property of the `MedusaRequest` object.
For example:
export const accessLocaleHighlights = [
["10", "req.locale", "Access the request's locale"],
]
```ts title="src/api/custom/route.ts" highlights={accessLocaleHighlights}
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const locale = req.locale
// use locale to retrieve localized data...
}
```
The `req.locale` property contains the locale value from either the query parameter or the request header. If no locale is specified in the request, `req.locale` is `undefined`.
### Retrieve Localized Data with Query
To retrieve data models with translated fields, pass the `locale` option to [Query](/learn/fundamentals/module-links/query) when querying your data.
For example, to retrieve products with translated names and descriptions:
export const queryHighlights = [
["10", "locale", "Pass the request locale to retrieve localized data"],
]
```ts title="src/api/store/products/route.ts" highlights={queryHighlights}
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const query = req.scope.resolve("query")
const { data: products } = await query.graph({
entity: "product",
fields: ["id", "title", "description"],
options: {
locale: req.locale,
},
})
res.json({ products })
}
```
In this example, the products are retrieved with their `title` and `description` fields translated to the locale specified in the request.
Learn more in the [Query](../../module-links/query/page.mdx#retrieve-localized-data) chapter.
### Retrieve Localized Data for Custom Models
You can also retrieve localized data for custom data models. Learn more in the [Translate Custom Data Models](!resources!/commerce-modules/translation/custom-data-models) guide.
@@ -200,43 +200,43 @@ const Post = model.define("post", {
id: model.id().primaryKey(),
title: model.text(),
author: model.belongsTo(() => Author, {
mappedBy: "posts"
})
mappedBy: "posts",
}),
})
const Author = model.define("author", {
id: model.id().primaryKey(),
name: model.text(),
posts: model.hasMany(() => Post, {
mappedBy: "author"
})
mappedBy: "author",
}),
})
```
To create a migration that reflects this relationship in the database, you can create a migration file as follows:
```ts title="src/modules/blog/migrations/Migration202507021200_create_author_and_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
export class Migration20251230112505 extends Migration {
override async up(): Promise<void> {
this.addSql(`create table if not exists "author" ("id" text not null, "name" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "author_pkey" primary key ("id"));`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_author_deleted_at" ON "author" ("deleted_at") WHERE deleted_at IS NULL;`);
this.addSql(`create table if not exists "author" ("id" text not null, "name" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "author_pkey" primary key ("id"));`)
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_author_deleted_at" ON "author" ("deleted_at") WHERE deleted_at IS NULL;`)
this.addSql(`create table if not exists "post" ("id" text not null, "title" text not null, "author_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "post_pkey" primary key ("id"));`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_author_id" ON "post" ("author_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_deleted_at" ON "post" ("deleted_at") WHERE deleted_at IS NULL;`);
this.addSql(`create table if not exists "post" ("id" text not null, "title" text not null, "author_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "post_pkey" primary key ("id"));`)
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_author_id" ON "post" ("author_id") WHERE deleted_at IS NULL;`)
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_deleted_at" ON "post" ("deleted_at") WHERE deleted_at IS NULL;`)
this.addSql(`alter table if exists "post" add constraint "post_author_id_foreign" foreign key ("author_id") references "author" ("id") on update cascade;`);
this.addSql(`alter table if exists "post" add constraint "post_author_id_foreign" foreign key ("author_id") references "author" ("id") on update cascade;`)
}
override async down(): Promise<void> {
this.addSql(`alter table if exists "post" drop constraint if exists "post_author_id_foreign";`);
this.addSql(`alter table if exists "post" drop constraint if exists "post_author_id_foreign";`)
this.addSql(`drop table if exists "author" cascade;`);
this.addSql(`drop table if exists "author" cascade;`)
this.addSql(`drop table if exists "post" cascade;`);
this.addSql(`drop table if exists "post" cascade;`)
}
}
@@ -251,16 +251,16 @@ Consider you have an existing `Post` data model, and you added a new `published_
You can create a migration file to add this new column as follows:
```ts title="src/modules/blog/migrations/Migration202507021300_add_published_at_to_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
export class Migration20251230113025 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "post" add column if not exists "published_at" timestamptz null;`);
this.addSql(`alter table if exists "post" add column if not exists "published_at" timestamptz null;`)
}
override async down(): Promise<void> {
this.addSql(`alter table if exists "post" drop column if exists "published_at";`);
this.addSql(`alter table if exists "post" drop column if exists "published_at";`)
}
}
@@ -275,16 +275,16 @@ Consider you have an existing `Post` data model, and you removed the `published_
You can create a migration file to remove this column as follows:
```ts title="src/modules/blog/migrations/Migration202507021400_remove_published_at_from_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
export class Migration20251230113125 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "post" drop column if exists "published_at";`);
this.addSql(`alter table if exists "post" drop column if exists "published_at";`)
}
override async down(): Promise<void> {
this.addSql(`alter table if exists "post" add column if not exists "published_at" timestamptz null;`);
this.addSql(`alter table if exists "post" add column if not exists "published_at" timestamptz null;`)
}
}
@@ -299,16 +299,16 @@ Consider you have an existing `Post` data model with a `title` column, and you r
You can create a migration file to rename this column as follows:
```ts title="src/modules/blog/migrations/Migration202507021500_rename_title_to_headline_in_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
export class Migration20251230113214 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "post" rename column "title" to "headline";`);
this.addSql(`alter table if exists "post" rename column "title" to "headline";`)
}
override async down(): Promise<void> {
this.addSql(`alter table if exists "post" rename column "headline" to "title";`);
this.addSql(`alter table if exists "post" rename column "headline" to "title";`)
}
}
@@ -332,24 +332,24 @@ export const Post = model.define("post", {
id: model.id().primaryKey(),
headline: model.text().index(),
author: model.belongsTo(() => Author, {
mappedBy: "posts"
})
mappedBy: "posts",
}),
})
```
You can create a migration file to create this index as follows:
```ts title="src/modules/blog/migrations/Migration202507021600_create_index_on_headline_in_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
export class Migration20251230113322 extends Migration {
override async up(): Promise<void> {
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_headline" ON "post" ("headline") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_headline" ON "post" ("headline") WHERE deleted_at IS NULL;`)
}
override async down(): Promise<void> {
this.addSql(`drop index if exists "IDX_post_headline";`);
this.addSql(`drop index if exists "IDX_post_headline";`)
}
}
@@ -364,16 +364,16 @@ Consider you have an existing `Post` data model with an index on the `headline`
You can create a migration file to drop this index as follows:
```ts title="src/modules/blog/migrations/Migration202507021700_drop_index_on_headline_in_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
export class Migration20251230113350 extends Migration {
override async up(): Promise<void> {
this.addSql(`drop index if exists "IDX_post_headline";`);
this.addSql(`drop index if exists "IDX_post_headline";`)
}
override async down(): Promise<void> {
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_headline" ON "post" ("headline") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_headline" ON "post" ("headline") WHERE deleted_at IS NULL;`)
}
}
@@ -23,13 +23,35 @@ Medusa automatically generates TypeScript types for:
## How to Trigger Type Generation?
The Medusa application generates these types automatically when you run the application with the `dev` command:
<Note>
As of [Medusa v2.12.4](https://github.com/medusajs/medusa/releases/tag/v2.12.4), types are generated when you run the `build` command. Prior versions only generated types when running the `dev` command.
</Note>
The Medusa application generates these types automatically when you run the `build` or `dev` commands:
```bash npm2yarn
npm run build
```
So, if you add a new data model or module and you don't find it in auto-completion or type checking, you can run the `build` command to regenerate the types.
### How to Generate Types for Local Plugins?
<Note>
This feature is available as of [Medusa v2.12.4](https://github.com/medusajs/medusa/releases/tag/v2.12.4).
</Note>
Local plugins are plugins installed in your Medusa application with the `plugin:develop` command. To generate types for those plugins, run the `dev` command in the Medusa application:
```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```
So, if you add a new data model or module and you don't find it in auto-completion or type checking, you can run the `dev` command to regenerate the types.
Medusa will copy the generated types under the `.medusa/types` directory of the application to the local plugin's directory.
---
@@ -840,3 +840,36 @@ export const retrieveBrandsWorkflow = createWorkflow(
```
This will retrieve all brands that are linked to at least one product.
### Retrieve Localized Data
<Prerequisites
items={[
{
text: "Medusa v2.12.4 or later",
link: "https://github.com/medusajs/medusa/releases/tag/v2.12.4"
},
{
text: "Translation Module Configured",
link: "!resources!/commerce-modules/translation#configure-translation-module",
},
]}
/>
To retrieve localized data for data models that have translations, pass an `options.locale` property to the first parameter of the `query.index` method:
```ts highlights={[["5", "locale", "Pass the locale to retrieve localized data."]]}
const { data: products } = await query.index({
entity: "product",
fields: ["id", "title", "description"],
options: {
locale: "fr-FR",
},
})
```
The `options.locale` property is a string representing the locale code following the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1).
The returned products will have their `title` and `description` properties in French (`fr-FR`), if translations are available.
Learn more in the [Translation Module](!resources!/commerce-modules/translation) documentation.
@@ -854,6 +854,60 @@ In the example above, you retrieve only deleted posts by enabling the `withDelet
---
## Retrieve Localized Data
<Prerequisites
items={[
{
text: "Medusa v2.12.4 or later",
link: "https://github.com/medusajs/medusa/releases/tag/v2.12.4"
},
{
text: "Translation Module Configured",
link: "!resources!/commerce-modules/translation#configure-translation-module",
},
]}
/>
To retrieve localized data for data models that have translations, pass an `options.locale` property to the first parameter of the `query.graph` method.
<CodeTabs group="query">
<CodeTab label="query.graph" value="query.graph">
```ts highlights={[["5", "locale", "Pass the locale to retrieve localized data."]]}
const { data: products } = await query.graph({
entity: "product",
fields: ["id", "title", "description"],
options: {
locale: "fr-FR",
},
})
```
</CodeTab>
<CodeTab label="useQueryGraphStep" value="useQueryGraphStep">
```ts highlights={[["5", "locale", "Pass the locale to retrieve localized data."]]}
const { data: products } = useQueryGraphStep({
entity: "product",
fields: ["id", "title", "description"],
options: {
locale: "fr-FR",
},
})
```
</CodeTab>
</CodeTabs>
The `options.locale` property is a string representing the locale code following the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1).
The returned products will have their `title` and `description` properties in French (`fr-FR`), if translations are available.
Learn more in the [Translation Module](!resources!/commerce-modules/translation) documentation.
---
## Configure Query to Throw Error
By default, if Query doesn't find records matching your query, it returns an empty array. You can configure Query to throw an error when no records are found.