feat: Translations UI (#14217)

* Add Translations route and guard it with feature flag. Empty TranslationsList main component to test route.

* Translation list component

* Add translations namespace to js-sdk

* Translations hook

* Avoid incorrectly throwing when updating and locale not included

* Translations bulk editor component v1

* Add batch method to translations namespace in js-sdk

* Protect translations edit route with feature flag

* Handle reference_id search param

* Replace entity_type entity_id for reference reference_id

* Manage translations from product detail page

* Dynamically resolve base hook for retrieving translations

* Fix navigation from outside settings/translations

* Navigation to bulk editor from product list

* Add Translations to various product module types

* Type useVariants hook

* Handle product module entities translations in bulk editor

* Fix categories issue in datagrid due to column clash

* Translations bulk navigation from remaining entities detail pages

* Add remaining bulk editor navigation for list components. Fix invalidation query for variants

* Expandable text cell v1

* Popover approach

* Add *supported_locales.locale to default fields in stores list endpoint

* Make popover more aligned to excell approach

* Correctly tie the focused cell anchor to popover

* Rework translations main component UI

* Fix link def export

* Swap axis for translations datagrid

* Add original column to translations data grid

* Remove is_default store locale from backend

* Remove ldefault locale from ui

* Type

* Add changeset

* Comments

* Remove unused import

* Add translations to admin product categories endpoint allowed fields

* Default locale removal

* Lazy loading with infinite scroll data grid

* Infinite list hook and implementation for products and variants

* Translation bulk editor lazy loaded datagrid

* Prevent scroll when forcing focus, to avoid scrollTop reset on infinite loading

* Confgiure placeholder data

* Cleanup logs and refactor

* Infinite query hooks for translatable entities

* Batch requests for translation batch endpoint

* Clean up

* Update icon

* Add query param validator in settings endpoint

* Settings endpoint param type

* JS sdk methods for translation settings and statistics

* Retrieve translatable fields and entities dynamically. Remove hardcoded information from tranlations list

* Resolve translation aggregate completion dynamically

* Format label

* Resolve bulk editor header label dynamically

* Include type and collection in translations config

* Avoid showing product option and option values in translatable entities list

* Translations

* Make translations bulk editor content columns wider

* Disable hiding Original column in translations bulk editor

* Adjust translations completion styles

* Fix translations config screen

* Locale selector switcher with conditional locale column rendering

* Batch one locale at a time

* Hooks save actions to footer buttons

* Reset dirty state on save

* Dynamic row heights for translations bulk editor. Replace expandable cell for text cell, with additional isMultiLine config

* Make columns take as much responsive width as possible and divide equally

* more padding to avoid unnecessary horizontal scrollbar

* Update statistics graphs

* Translations

* Statistics graphs translations

* Translation, text sizes and weight in stat graphs

* Conditionally show/hide column visibility dropdown in datagrid

* Allow to pass component to place in DataGrid header and use it in translations bulk editor

* Center text regardless of multiLine config

* Apply full height to datagrid cell regardles of multiSelect config

* Colors and fonts

* Handle key down for text area in text cell

* MultilineCell with special keydown handling

* Rework form schema to match new single locale edit flow

* Update created translations to include id, to avoid duplication issue on subsequent calls

* Handle space key for text cells

* Finish hooking up multiline cell with key and mouse events

* Disable remaining buttons when batch is ongoing

* Style updates

* Update style

* Refactor to make form updates and sync/comparison with server data more comprehensive and robust

* Update styles

* Bars and labels alignment

* Add languages tooltip

* Styles and translation

* Navigation update

* Disable edit translations button when no reference count

* Invert colors

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
Co-authored-by: Adrien de Peretti <adrien.deperetti@gmail.com>
This commit is contained in:
Nicolas Gorga
2025-12-17 09:36:50 -03:00
committed by GitHub
parent c1a5390fc6
commit 3d1330ebb9
69 changed files with 3595 additions and 112 deletions

View File

@@ -61,7 +61,7 @@ export const validateTranslationsStep = createStep(
const unsupportedLocales = normalizedInput
.filter((translation) => Boolean(translation.locale_code))
.map((translation) => translation.locale_code)
.filter((locale) => !enabledLocales.includes(locale ?? ""))
.filter((locale) => !enabledLocales.includes(locale))
if (unsupportedLocales.length) {
throw new MedusaError(

View File

@@ -46,6 +46,7 @@ import { Views } from "./views"
import { WorkflowExecution } from "./workflow-execution"
import { ShippingOptionType } from "./shipping-option-type"
import { Locale } from "./locale"
import { Translation } from "./translation"
export class Admin {
/**
@@ -225,6 +226,10 @@ export class Admin {
* @tags tax
*/
public taxProvider: TaxProvider
/**
* @tags translations
*/
public translation: Translation
/**
* @tags promotion
*/
@@ -268,6 +273,7 @@ export class Admin {
this.claim = new Claim(client)
this.taxRate = new TaxRate(client)
this.taxRegion = new TaxRegion(client)
this.translation = new Translation(client)
this.store = new Store(client)
this.productTag = new ProductTag(client)
this.user = new User(client)

View File

@@ -0,0 +1,206 @@
import { HttpTypes } from "@medusajs/types"
import { Client } from "../client"
import { ClientHeaders } from "../types"
export class Translation {
/**
* @ignore
*/
private client: Client
/**
* @ignore
*/
constructor(client: Client) {
this.client = client
}
/**
* This method retrieves a paginated list of translations. It sends a request to the
* [List Translations](https://docs.medusajs.com/api/admin#translations_gettranslations)
* API route.
*
* @param query - Filters and pagination configurations.
* @param headers - Headers to pass in the request.
* @returns The paginated list of translations.
*
* @example
* To retrieve the list of translations:
*
* ```ts
* sdk.admin.translation.list()
* .then(({ translations, count, limit, offset }) => {
* console.log(translations)
* })
* ```
*
* To configure the pagination, pass the `limit` and `offset` query parameters.
*
* For example, to retrieve only 10 items and skip 10 items:
*
* ```ts
* sdk.admin.translation.list({
* limit: 10,
* offset: 10
* })
* .then(({ translations, count, limit, offset }) => {
* console.log(translations)
* })
* ```
*
* Using the `fields` query parameter, you can specify the fields and relations to retrieve
* in each translation:
*
* ```ts
* sdk.admin.translation.list({
* fields: "id,name"
* })
* .then(({ translations, count, limit, offset }) => {
* console.log(translations)
* })
* ```
*
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
*/
async list(
query?: HttpTypes.AdminTranslationsListParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminTranslationsListResponse>(
`/admin/translations`,
{
headers,
query,
}
)
}
/**
* This method allows bulk operations on translations. It sends a request to the
* [Manage Translations](https://docs.medusajs.com/api/admin#translations_posttranslationsbatch)
* API route.
*
* @param payload - The translations to create, update, or delete.
* @param headers - Headers to pass in the request.
* @returns The translations' details.
*
* @example
* sdk.admin.translation.batch({
* create: [
* {
* reference_id: "prod_123",
* reference: "product",
* locale_code: "en-US",
* translations: { title: "Shirt" }
* }
* ],
* update: [
* {
* id: "trans_123",
* translations: { title: "Pants" }
* }
* ],
* delete: ["trans_321"]
* })
* .then(({ created, updated, deleted }) => {
* console.log(created, updated, deleted)
* })
* ```
*/
async batch(body: HttpTypes.AdminBatchTranslations, headers?: ClientHeaders) {
return await this.client.fetch<HttpTypes.AdminTranslationsBatchResponse>(
`/admin/translations/batch`,
{
method: "POST",
headers,
body,
}
)
}
/**
* This method retrieves the settings for the translations for a given entity type or all entity types if no entity type is provided.
* It sends a request to the
* [Get Translation Settings](https://docs.medusajs.com/api/admin#translations_gettranslationssettings) API route.
*
* @param query - The query parameters which can optionally include the entity type to get the settings for.
* @param headers - Headers to pass in the request.
* @returns The translation settings.
*
* @example
* To retrieve the settings for the translations for a given entity type:
*
* ```ts
* sdk.admin.translation.settings({
* entity_type: "product"
* })
* .then(({ translatable_fields }) => {
* console.log(translatable_fields)
* })
* ```
*
* To retrieve the settings for all entity types:
*
* ```ts
* sdk.admin.translation.settings()
* .then(({ translatable_fields }) => {
* console.log(translatable_fields)
* })
* ```
*/
async settings(
query?: HttpTypes.AdminTranslationSettingsParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminTranslationSettingsResponse>(
`/admin/translations/settings`,
{
headers,
query,
}
)
}
/**
* This method retrieves the statistics for the translations for a given entity type or all entity types if no entity type is provided.
* It sends a request to the
* [Get Translation Statistics](https://docs.medusajs.com/api/admin#translations_gettranslationsstatistics) API route.
*
* @param query - The query parameters which can optionally include the entity type to get the statistics for.
* @param headers - Headers to pass in the request.
* @returns The translation statistics.
*
* @example
* To retrieve the statistics for the translations for a given entity type:
*
* ```ts
* sdk.admin.translation.statistics({
* entity_type: "product"
* })
* .then(({ statistics }) => {
* console.log(statistics)
* })
* ```
*
* To retrieve the statistics for all entity types:
*
* ```ts
* sdk.admin.translation.statistics()
* .then(({ statistics }) => {
* console.log(statistics)
* })
* ```
*/
async statistics(
query?: HttpTypes.AdminTranslationStatisticsParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminTranslationStatisticsResponse>(
`/admin/translations/statistics`,
{
headers,
query,
}
)
}
}

View File

@@ -1,3 +1,9 @@
import { BaseCollection } from "../common"
import { AdminTranslation } from "../../translations"
export interface AdminCollection extends BaseCollection {}
export interface AdminCollection extends BaseCollection {
/**
* The collection's translations.
*/
translations?: AdminTranslation[] | null
}

View File

@@ -1,4 +1,5 @@
import { AdminProduct } from "../../product"
import { AdminTranslation } from "../../translations"
import { BaseProductCategory } from "../common"
export interface AdminProductCategory
@@ -18,4 +19,8 @@ export interface AdminProductCategory
* The products that belong to this category.
*/
products?: AdminProduct[]
/**
* The category's translations.
*/
translations?: AdminTranslation[] | null
}

View File

@@ -1,3 +1,9 @@
import { BaseProductTag } from "../common"
import { AdminTranslation } from "../../translations"
export interface AdminProductTag extends BaseProductTag {}
export interface AdminProductTag extends BaseProductTag {
/**
* The tag's translations.
*/
translations?: AdminTranslation[] | null
}

View File

@@ -1,3 +1,9 @@
import { AdminTranslation } from "../../translations"
import { BaseProductType } from "../common"
export interface AdminProductType extends BaseProductType {}
export interface AdminProductType extends BaseProductType {
/**
* The product type's translations.
*/
translations?: AdminTranslation[] | null
}

View File

@@ -6,6 +6,7 @@ import { AdminProductTag } from "../../product-tag"
import { AdminProductType } from "../../product-type"
import { AdminSalesChannel } from "../../sales-channel"
import { AdminShippingProfile } from "../../shipping-profile"
import { AdminTranslation } from "../../translations"
import {
BaseProduct,
BaseProductImage,
@@ -55,6 +56,10 @@ export interface AdminProductVariant extends BaseProductVariant {
* The product that this variant belongs to.
*/
product?: AdminProduct | null
/**
* The product variant's translations.
*/
translations?: AdminTranslation[] | null
/**
* The variant's inventory items.
*/
@@ -108,6 +113,10 @@ export interface AdminProduct
* The product's variants.
*/
variants: AdminProductVariant[] | null
/**
* The product's translations.
*/
translations?: AdminTranslation[] | null
/**
* The product's type.
*/

View File

@@ -1,3 +1,4 @@
export * from "./queries"
export * from "./responses"
export * from "./entities"
export * from "./payloads"

View File

@@ -0,0 +1,44 @@
interface AdminCreateTranslation {
/**
* The ID of the entity being translated.
*/
reference_id: string
/**
* The type of entity being translated (e.g., "product", "product_variant").
*/
reference: string
/**
* The BCP 47 language tag code for this translation (e.g., "en-US", "fr-FR").
*/
locale_code: string
/**
* The translated fields as key-value pairs.
*/
translations: Record<string, unknown>
}
interface AdminUpdateTranslation {
/**
* The ID of the translation.
*/
id: string
/**
* The translated fields as key-value pairs.
*/
translations: Record<string, unknown>
}
export interface AdminBatchTranslations {
/**
* The translations to create.
*/
create?: AdminCreateTranslation[]
/**
* The translations to update.
*/
update?: AdminUpdateTranslation[]
/**
* The translations to delete.
*/
delete?: string[]
}

View File

@@ -51,3 +51,13 @@ export interface AdminTranslationStatisticsParams {
*/
entity_types: string[]
}
/**
* Query parameters for translation settings endpoint.
*/
export interface AdminTranslationSettingsParams {
/**
* The entity type to get the settings for (e.g., "product").
*/
entity_type?: string
}