Files
medusa-store/integration-tests/http/__tests__/translation/admin/translation.spec.ts
Nicolas Gorga d60ea7268a feat(translation,fulfillment,customer,product,region,tax,core-flows,medusa,types): Implement dynamic translation settings management (#14536)
* Add is_active field to translation_settings model

* Types

* Workflows

* Api layer

* Tests

* Add changeset

* Add comment

* Hook to create or deactivate translatable entities on startup

* Cleanup old code

* Configure translatable option for core entities

* Validation step and snake case correction

* Cleanup

* Tests

* Comment in PR

* Update changeset

* Mock DmlEntity.getTranslatableEntities

* Move validation to module service layer

* Remove validation from remaining workflow

* Return object directly

* Type improvements

* Remove .only from tests

* Apply snakeCase

* Fix tests

* Fix tests

* Remove unnecessary map and use set instead

* Fix tests

* Comments

* Include translatable product properties

* Avoid race condition in translations tests

* Update test
2026-01-14 07:09:49 -03:00

1039 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { MedusaContainer } from "@medusajs/types"
import { Modules } from "@medusajs/utils"
import {
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
jest.setTimeout(100000)
process.env.MEDUSA_FF_TRANSLATION = "true"
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
describe("Admin Translation API", () => {
let appContainer: MedusaContainer
beforeAll(async () => {
appContainer = getContainer()
})
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
const storeModule = appContainer.resolve(Modules.STORE)
const [defaultStore] = await storeModule.listStores(
{},
{
select: ["id"],
take: 1,
}
)
await storeModule.updateStores(defaultStore.id, {
supported_locales: [
{ locale_code: "en-US" },
{ locale_code: "fr-FR" },
{ locale_code: "de-DE" },
],
})
})
afterAll(async () => {
delete process.env.MEDUSA_FF_TRANSLATION
})
describe("GET /admin/translations", () => {
it("should list translations (empty initially)", async () => {
const response = await api.get("/admin/translations", adminHeaders)
expect(response.status).toEqual(200)
expect(response.data).toEqual(
expect.objectContaining({
translations: [],
count: 0,
offset: 0,
limit: 20,
})
)
})
it("should list translations after creating some", async () => {
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_123",
reference: "product",
locale_code: "fr-FR",
translations: {
title: "Titre du produit",
description: "Description en français",
},
},
{
reference_id: "prod_123",
reference: "product",
locale_code: "de-DE",
translations: {
title: "Produkttitel",
description: "Beschreibung auf Deutsch",
},
},
],
},
adminHeaders
)
const response = await api.get("/admin/translations", adminHeaders)
expect(response.status).toEqual(200)
expect(response.data.translations).toHaveLength(2)
expect(response.data.count).toEqual(2)
expect(response.data.translations).toEqual(
expect.arrayContaining([
expect.objectContaining({
reference_id: "prod_123",
reference: "product",
locale_code: "fr-FR",
translations: {
title: "Titre du produit",
description: "Description en français",
},
}),
expect.objectContaining({
reference_id: "prod_123",
reference: "product",
locale_code: "de-DE",
translations: {
title: "Produkttitel",
description: "Beschreibung auf Deutsch",
},
}),
])
)
})
it("should filter translations by reference_id", async () => {
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_1",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Produit Un" },
},
{
reference_id: "prod_2",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Produit Deux" },
},
],
},
adminHeaders
)
const response = await api.get(
"/admin/translations?reference_id=prod_1",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.translations).toHaveLength(1)
expect(response.data.translations[0].reference_id).toEqual("prod_1")
})
it("should filter translations by reference", async () => {
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_1",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Produit" },
},
{
reference_id: "cat_1",
reference: "product_category",
locale_code: "fr-FR",
translations: { name: "Catégorie" },
},
],
},
adminHeaders
)
const response = await api.get(
"/admin/translations?reference=product_category",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.translations).toHaveLength(1)
expect(response.data.translations[0].reference).toEqual(
"product_category"
)
})
it("should filter translations by locale_code", async () => {
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_1",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Titre français" },
},
{
reference_id: "prod_1",
reference: "product",
locale_code: "de-DE",
translations: { title: "Deutscher Titel" },
},
],
},
adminHeaders
)
const response = await api.get(
"/admin/translations?locale_code=de-DE",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.translations).toHaveLength(1)
expect(response.data.translations[0].locale_code).toEqual("de-DE")
})
it("should filter translations by multiple criteria", async () => {
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_1",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Produit Un FR" },
},
{
reference_id: "prod_1",
reference: "product",
locale_code: "de-DE",
translations: { title: "Produkt Eins DE" },
},
{
reference_id: "prod_2",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Produit Deux FR" },
},
],
},
adminHeaders
)
const response = await api.get(
"/admin/translations?reference_id=prod_1&locale_code=fr-FR",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.translations).toHaveLength(1)
expect(response.data.translations[0]).toEqual(
expect.objectContaining({
reference_id: "prod_1",
locale_code: "fr-FR",
translations: { title: "Produit Un FR" },
})
)
})
it("should support pagination", async () => {
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_1",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Produit 1" },
},
{
reference_id: "prod_2",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Produit 2" },
},
{
reference_id: "prod_3",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Produit 3" },
},
],
},
adminHeaders
)
const response = await api.get(
"/admin/translations?limit=2&offset=0",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.translations).toHaveLength(2)
expect(response.data.count).toEqual(3)
expect(response.data.limit).toEqual(2)
expect(response.data.offset).toEqual(0)
const response2 = await api.get(
"/admin/translations?limit=2&offset=2",
adminHeaders
)
expect(response2.status).toEqual(200)
expect(response2.data.translations).toHaveLength(1)
expect(response2.data.offset).toEqual(2)
})
it("should filter translations using q parameter (JSONB search)", async () => {
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_1",
reference: "product",
locale_code: "fr-FR",
translations: {
title: "Chaussures de sport",
description: "Des chaussures confortables",
},
},
{
reference_id: "prod_2",
reference: "product",
locale_code: "fr-FR",
translations: {
title: "T-shirt de sport",
description: "Un t-shirt léger",
},
},
],
},
adminHeaders
)
const response = await api.get(
"/admin/translations?q=chaussures",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.translations).toHaveLength(1)
expect(response.data.translations[0].reference_id).toEqual("prod_1")
})
})
describe("POST /admin/translations/batch", () => {
describe("create", () => {
it("should create a single translation", async () => {
const response = await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_123",
reference: "product",
locale_code: "fr-FR",
translations: {
title: "Titre du produit",
},
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.created).toHaveLength(1)
expect(response.data.created[0]).toEqual(
expect.objectContaining({
id: expect.stringMatching(/^trans_/),
reference_id: "prod_123",
reference: "product",
locale_code: "fr-FR",
translations: {
title: "Titre du produit",
},
})
)
})
it("should create multiple translations", async () => {
const response = await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_123",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Titre FR" },
},
{
reference_id: "prod_123",
reference: "product",
locale_code: "de-DE",
translations: { title: "Titel DE" },
},
{
reference_id: "var_456",
reference: "product_variant",
locale_code: "fr-FR",
translations: { title: "Variante FR" },
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.created).toHaveLength(3)
})
it("should create translations for different entity types", async () => {
const response = await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_123",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Produit" },
},
{
reference_id: "cat_456",
reference: "product_category",
locale_code: "fr-FR",
translations: { name: "Catégorie" },
},
{
reference_id: "col_789",
reference: "product_collection",
locale_code: "fr-FR",
translations: { title: "Collection" },
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.created).toHaveLength(3)
expect(response.data.created).toEqual(
expect.arrayContaining([
expect.objectContaining({ reference: "product" }),
expect.objectContaining({ reference: "product_category" }),
expect.objectContaining({ reference: "product_collection" }),
])
)
})
})
describe("update", () => {
it("should update an existing translation", async () => {
const createResponse = await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_123",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Ancien titre" },
},
],
},
adminHeaders
)
const translationId = createResponse.data.created[0].id
const updateResponse = await api.post(
"/admin/translations/batch",
{
update: [
{
id: translationId,
translations: { title: "Nouveau titre" },
},
],
},
adminHeaders
)
expect(updateResponse.status).toEqual(200)
expect(updateResponse.data.updated).toHaveLength(1)
expect(updateResponse.data.updated[0]).toEqual(
expect.objectContaining({
id: translationId,
translations: { title: "Nouveau titre" },
})
)
})
it("should update multiple translations", async () => {
const createResponse = await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_1",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Titre 1" },
},
{
reference_id: "prod_2",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Titre 2" },
},
],
},
adminHeaders
)
const [trans1, trans2] = createResponse.data.created
const updateResponse = await api.post(
"/admin/translations/batch",
{
update: [
{ id: trans1.id, translations: { title: "Nouveau 1" } },
{ id: trans2.id, translations: { title: "Nouveau 2" } },
],
},
adminHeaders
)
expect(updateResponse.status).toEqual(200)
expect(updateResponse.data.updated).toHaveLength(2)
})
})
describe("delete", () => {
it("should delete a translation", async () => {
const createResponse = await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_123",
reference: "product",
locale_code: "fr-FR",
translations: { title: "À supprimer" },
},
],
},
adminHeaders
)
const translationId = createResponse.data.created[0].id
const deleteResponse = await api.post(
"/admin/translations/batch",
{
delete: [translationId],
},
adminHeaders
)
expect(deleteResponse.status).toEqual(200)
expect(deleteResponse.data.deleted).toEqual({
ids: [translationId],
object: "translation",
deleted: true,
})
const listResponse = await api.get(
`/admin/translations?reference_id=prod_123`,
adminHeaders
)
expect(listResponse.data.translations).toHaveLength(0)
})
it("should delete multiple translations", async () => {
const createResponse = await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_1",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Supprimer 1" },
},
{
reference_id: "prod_2",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Supprimer 2" },
},
],
},
adminHeaders
)
const ids = createResponse.data.created.map((t) => t.id)
const deleteResponse = await api.post(
"/admin/translations/batch",
{
delete: ids,
},
adminHeaders
)
expect(deleteResponse.status).toEqual(200)
expect(deleteResponse.data.deleted.ids).toHaveLength(2)
})
})
describe("combined operations", () => {
it("should handle create, update, and delete in a single batch", async () => {
const createResponse = await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_existing",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Existant" },
},
{
reference_id: "prod_to_delete",
reference: "product",
locale_code: "fr-FR",
translations: { title: "À supprimer" },
},
],
},
adminHeaders
)
const existingId = createResponse.data.created[0].id
const toDeleteId = createResponse.data.created[1].id
const batchResponse = await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: "prod_new",
reference: "product",
locale_code: "fr-FR",
translations: { title: "Nouveau" },
},
],
update: [
{
id: existingId,
translations: { title: "Mis à jour" },
},
],
delete: [toDeleteId],
},
adminHeaders
)
expect(batchResponse.status).toEqual(200)
expect(batchResponse.data.created).toHaveLength(1)
expect(batchResponse.data.updated).toHaveLength(1)
expect(batchResponse.data.deleted.ids).toContain(toDeleteId)
expect(batchResponse.data.created[0].translations.title).toEqual(
"Nouveau"
)
expect(batchResponse.data.updated[0].translations.title).toEqual(
"Mis à jour"
)
})
})
})
describe("GET /admin/translations/statistics", () => {
it("should return statistics for entity types with no translations", async () => {
const productModule = appContainer.resolve(Modules.PRODUCT)
await productModule.createProducts([
{ title: "Product 1" },
{ title: "Product 2" },
])
const response = await api.get(
"/admin/translations/statistics?locales=en-US&locales=fr-FR&entity_types=product",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.statistics).toBeDefined()
expect(response.data.statistics.product).toEqual({
// 2 products × 4 translatable fields × 2 locales = 16 expected
expected: 16,
translated: 0,
missing: 16,
by_locale: {
"en-US": { expected: 8, translated: 0, missing: 8 },
"fr-FR": { expected: 8, translated: 0, missing: 8 },
},
})
})
it("should return statistics with partial translations", async () => {
const productModule = appContainer.resolve(Modules.PRODUCT)
const [product1, product2] = await productModule.createProducts([
{ title: "Product 1" },
{ title: "Product 2" },
])
// Create translations for product1 with partial fields
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: product1.id,
reference: "product",
locale_code: "fr-FR",
translations: {
title: "Produit 1",
description: "Description du produit 1",
},
},
{
reference_id: product2.id,
reference: "product",
locale_code: "fr-FR",
translations: {
title: "Produit 2",
},
},
],
},
adminHeaders
)
const response = await api.get(
"/admin/translations/statistics?locales=fr-FR&entity_types=product",
adminHeaders
)
expect(response.status).toEqual(200)
// 2 products × 4 fields × 1 locale = 8 expected
// product1 has 2 fields, product2 has 1 field = 3 translated
expect(response.data.statistics.product).toEqual({
expected: 8,
translated: 3,
missing: 5,
by_locale: {
"fr-FR": { expected: 8, translated: 3, missing: 5 },
},
})
})
it("should return statistics for multiple entity types", async () => {
const productModule = appContainer.resolve(Modules.PRODUCT)
const [product] = await productModule.createProducts([
{
title: "Product with variant",
variants: [{ title: "Variant 1" }, { title: "Variant 2" }],
},
])
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: product.id,
reference: "product",
locale_code: "fr-FR",
translations: {
title: "Produit",
description: "Description",
subtitle: "Sous-titre",
material: "Matériau",
},
},
{
reference_id: product.variants[0].id,
reference: "product_variant",
locale_code: "fr-FR",
translations: {
title: "Variante 1",
},
},
],
},
adminHeaders
)
const response = await api.get(
"/admin/translations/statistics?locales=fr-FR&entity_types=product&entity_types=product_variant",
adminHeaders
)
expect(response.status).toEqual(200)
// Product: 1 × 4 fields × 1 locale = 4, all translated
expect(response.data.statistics.product).toEqual({
expected: 4,
translated: 4,
missing: 0,
by_locale: {
"fr-FR": { expected: 4, translated: 4, missing: 0 },
},
})
// Variant: 2 × 2 fields × 1 locale = 4, 1 translated
expect(response.data.statistics.product_variant).toEqual({
expected: 4,
translated: 1,
missing: 3,
by_locale: {
"fr-FR": { expected: 4, translated: 1, missing: 3 },
},
})
})
it("should return statistics for multiple locales", async () => {
const productModule = appContainer.resolve(Modules.PRODUCT)
const [product] = await productModule.createProducts([
{ title: "Product" },
])
await api.post(
"/admin/translations/batch",
{
create: [
{
reference_id: product.id,
reference: "product",
locale_code: "fr-FR",
translations: {
title: "Produit",
description: "Description",
},
},
{
reference_id: product.id,
reference: "product",
locale_code: "de-DE",
translations: { title: "Produkt" },
},
],
},
adminHeaders
)
const response = await api.get(
"/admin/translations/statistics?locales=fr-FR&locales=de-DE&entity_types=product",
adminHeaders
)
expect(response.status).toEqual(200)
// 1 product × 4 fields × 2 locales = 8 expected
// fr-FR: 2 translated, de-DE: 1 translated = 3 total
expect(response.data.statistics.product.expected).toEqual(8)
expect(response.data.statistics.product.translated).toEqual(3)
expect(response.data.statistics.product.missing).toEqual(5)
expect(response.data.statistics.product.by_locale["fr-FR"]).toEqual({
expected: 4,
translated: 2,
missing: 2,
})
expect(response.data.statistics.product.by_locale["de-DE"]).toEqual({
expected: 4,
translated: 1,
missing: 3,
})
})
it("should return zeros for unknown entity types", async () => {
const response = await api.get(
"/admin/translations/statistics?locales=fr-FR&entity_types=unknown_entity",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.statistics.unknown_entity).toEqual({
expected: 0,
translated: 0,
missing: 0,
by_locale: {
"fr-FR": { expected: 0, translated: 0, missing: 0 },
},
})
})
it("should validate required fields", async () => {
const response1 = await api
.get(
"/admin/translations/statistics?entity_types=product",
adminHeaders
)
.catch((e) => e.response)
expect(response1.status).toEqual(400)
const response2 = await api
.get("/admin/translations/statistics?locales=fr-FR", adminHeaders)
.catch((e) => e.response)
expect(response2.status).toEqual(400)
const response3 = await api
.get("/admin/translations/statistics", adminHeaders)
.catch((e) => e.response)
expect(response3.status).toEqual(400)
})
})
describe("GET /admin/translations/entities", () => {
it("should return entities with only translatable fields", async () => {
const productModule = appContainer.resolve(Modules.PRODUCT)
await productModule.createProducts([
{
title: "Product 1",
description: "Description 1",
handle: "product-1",
status: "published",
},
{
title: "Product 2",
description: "Description 2",
handle: "product-2",
status: "draft",
},
])
const response = await api.get(
"/admin/translations/entities?type=product",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.data).toHaveLength(2)
expect(response.data.count).toEqual(2)
expect(response.data.offset).toEqual(0)
expect(response.data.limit).toEqual(20)
response.data.data.forEach((entity: Record<string, unknown>) => {
expect(entity).toHaveProperty("id")
expect(entity.title).toBeDefined()
expect(entity.description).toBeDefined()
expect(entity.material).toBeDefined()
expect(entity.status).not.toBeDefined()
})
})
it("should return empty array for unknown entity type", async () => {
const response = await api.get(
"/admin/translations/entities?type=unknown_entity",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.data).toEqual([])
expect(response.data.count).toEqual(0)
})
it("should support pagination", async () => {
const productModule = appContainer.resolve(Modules.PRODUCT)
await productModule.createProducts([
{ title: "Product 1" },
{ title: "Product 2" },
{ title: "Product 3" },
{ title: "Product 4" },
{ title: "Product 5" },
])
const response = await api.get(
"/admin/translations/entities?type=product&limit=2&offset=0",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.data).toHaveLength(2)
expect(response.data.count).toEqual(5)
expect(response.data.limit).toEqual(2)
expect(response.data.offset).toEqual(0)
const response2 = await api.get(
"/admin/translations/entities?type=product&limit=2&offset=2",
adminHeaders
)
expect(response2.status).toEqual(200)
expect(response2.data.data).toHaveLength(2)
expect(response2.data.offset).toEqual(2)
})
it("should return product variants with their translatable fields", async () => {
const productModule = appContainer.resolve(Modules.PRODUCT)
await productModule.createProducts([
{
title: "Product with variants",
variants: [
{ title: "Variant 1", manage_inventory: false },
{ title: "Variant 2", manage_inventory: false },
],
},
])
const response = await api.get(
"/admin/translations/entities?type=product_variant",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.data).toHaveLength(2)
expect(response.data.count).toEqual(2)
response.data.data.forEach((variant: Record<string, unknown>) => {
expect(variant).toHaveProperty("id")
expect(variant.title).toBeDefined()
expect(variant.manage_inventory).not.toBeDefined()
})
})
it("should validate required type parameter", async () => {
const response = await api
.get("/admin/translations/entities", adminHeaders)
.catch((e) => e.response)
expect(response.status).toEqual(400)
})
})
})
},
})