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
This commit is contained in:
@@ -44,6 +44,10 @@ medusaIntegrationTestRunner({
|
||||
|
||||
beforeEach(async () => {
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
const translationModule = appContainer.resolve(Modules.TRANSLATION)
|
||||
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
|
||||
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ medusaIntegrationTestRunner({
|
||||
|
||||
beforeEach(async () => {
|
||||
appContainer = getContainer()
|
||||
const translationModule = appContainer.resolve(Modules.TRANSLATION)
|
||||
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
const taxStructure = await setupTaxStructure(
|
||||
|
||||
@@ -33,6 +33,9 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
const translationModule = appContainer.resolve(Modules.TRANSLATION)
|
||||
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
|
||||
|
||||
salesChannel = (
|
||||
await api.post(
|
||||
"/admin/sales-channels",
|
||||
|
||||
@@ -47,6 +47,10 @@ medusaIntegrationTestRunner({
|
||||
appContainer.resolve(Modules.TAX)
|
||||
)
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
const translationModule = appContainer.resolve(Modules.TRANSLATION)
|
||||
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
|
||||
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ medusaIntegrationTestRunner({
|
||||
appContainer.resolve(Modules.TAX)
|
||||
)
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
const translationModule = appContainer.resolve(Modules.TRANSLATION)
|
||||
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ medusaIntegrationTestRunner({
|
||||
appContainer.resolve(Modules.TAX)
|
||||
)
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
const translationModule = appContainer.resolve(Modules.TRANSLATION)
|
||||
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
|
||||
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ medusaIntegrationTestRunner({
|
||||
const container = getContainer()
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
|
||||
const translationModule = container.resolve(Modules.TRANSLATION)
|
||||
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
|
||||
|
||||
// Set up supported locales in the store
|
||||
const storeModule = container.resolve(Modules.STORE)
|
||||
const [defaultStore] = await storeModule.listStores(
|
||||
|
||||
@@ -41,6 +41,9 @@ medusaIntegrationTestRunner({
|
||||
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
const translationModule = appContainer.resolve(Modules.TRANSLATION)
|
||||
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
|
||||
|
||||
// Set up store locales
|
||||
const storeModule = appContainer.resolve(Modules.STORE)
|
||||
const [defaultStore] = await storeModule.listStores(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import { Modules } from "@medusajs/utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
@@ -13,6 +14,10 @@ medusaIntegrationTestRunner({
|
||||
describe("Admin Locale API", () => {
|
||||
beforeEach(async () => {
|
||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||
|
||||
const appContainer = getContainer()
|
||||
const translationModule = appContainer.resolve(Modules.TRANSLATION)
|
||||
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import { DmlEntity, 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 Settings Batch API", () => {
|
||||
let mockGetTranslatableEntities: jest.SpyInstance
|
||||
|
||||
beforeEach(async () => {
|
||||
mockGetTranslatableEntities = jest.spyOn(
|
||||
DmlEntity,
|
||||
"getTranslatableEntities"
|
||||
)
|
||||
mockGetTranslatableEntities.mockReturnValue([
|
||||
{ entity: "ProductVariant", fields: ["title", "material"] },
|
||||
{ entity: "ProductCategory", fields: ["name", "description"] },
|
||||
{ entity: "ProductCollection", fields: ["title"] },
|
||||
])
|
||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||
|
||||
const appContainer = getContainer()
|
||||
|
||||
const translationModuleService = appContainer.resolve(
|
||||
Modules.TRANSLATION
|
||||
)
|
||||
await translationModuleService.__hooks
|
||||
?.onApplicationStart?.()
|
||||
.catch(() => {})
|
||||
|
||||
// Delete all translation settings to be able to test the create operation
|
||||
const settings =
|
||||
await translationModuleService.listTranslationSettings()
|
||||
await translationModuleService.deleteTranslationSettings(
|
||||
settings.map((s) => s.id)
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
delete process.env.MEDUSA_FF_TRANSLATION
|
||||
mockGetTranslatableEntities.mockRestore()
|
||||
})
|
||||
|
||||
describe("POST /admin/translations/settings/batch", () => {
|
||||
describe("create", () => {
|
||||
it("should create a single translation setting", async () => {
|
||||
const response = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.created).toHaveLength(1)
|
||||
expect(response.data.created[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
is_active: true,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should create multiple translation settings", async () => {
|
||||
const response = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
entity_type: "product_category",
|
||||
fields: ["name", "description"],
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
entity_type: "product_collection",
|
||||
fields: ["title"],
|
||||
is_active: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.created).toHaveLength(3)
|
||||
expect(response.data.created).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
is_active: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
entity_type: "product_category",
|
||||
fields: ["name", "description"],
|
||||
is_active: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
entity_type: "product_collection",
|
||||
fields: ["title"],
|
||||
is_active: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("should update an existing translation setting", async () => {
|
||||
const createResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const settingId = createResponse.data.created[0].id
|
||||
|
||||
const updateResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
update: [
|
||||
{
|
||||
id: settingId,
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(updateResponse.status).toEqual(200)
|
||||
expect(updateResponse.data.updated).toHaveLength(1)
|
||||
expect(updateResponse.data.updated[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: settingId,
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
is_active: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should update multiple translation settings", async () => {
|
||||
const createResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
entity_type: "product_category",
|
||||
fields: ["name"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const [settingId1, settingId2] = createResponse.data.created.map(
|
||||
(s) => s.id
|
||||
)
|
||||
|
||||
const updateResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
update: [
|
||||
{
|
||||
id: settingId1,
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
},
|
||||
{
|
||||
id: settingId2,
|
||||
entity_type: "product_category",
|
||||
is_active: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(updateResponse.status).toEqual(200)
|
||||
expect(updateResponse.data.updated).toHaveLength(2)
|
||||
expect(updateResponse.data.updated).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
entity_type: "product_category",
|
||||
is_active: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
it("should delete a translation setting", async () => {
|
||||
const createResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const settingId = createResponse.data.created[0].id
|
||||
|
||||
const deleteResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
delete: [settingId],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(deleteResponse.status).toEqual(200)
|
||||
expect(deleteResponse.data.deleted).toEqual({
|
||||
ids: [settingId],
|
||||
object: "translation_settings",
|
||||
deleted: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should delete multiple translation settings", async () => {
|
||||
const createResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
entity_type: "product_category",
|
||||
fields: ["name"],
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
entity_type: "product_collection",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const ids = createResponse.data.created.map((s) => s.id)
|
||||
|
||||
const deleteResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
delete: ids,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(deleteResponse.status).toEqual(200)
|
||||
expect(deleteResponse.data.deleted).toEqual({
|
||||
ids: expect.arrayContaining(ids),
|
||||
object: "translation_settings",
|
||||
deleted: true,
|
||||
})
|
||||
expect(deleteResponse.data.deleted.ids).toHaveLength(3)
|
||||
})
|
||||
|
||||
it("should handle deleting with empty array", async () => {
|
||||
const response = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
delete: [],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.deleted).toEqual({
|
||||
ids: [],
|
||||
object: "translation_settings",
|
||||
deleted: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("combined operations", () => {
|
||||
it("should handle create, update, and delete in a single batch", async () => {
|
||||
const setupResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
entity_type: "product_category",
|
||||
fields: ["name"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const [settingId1, settingId2] = setupResponse.data.created.map(
|
||||
(s) => s.id
|
||||
)
|
||||
|
||||
const batchResponse = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_collection",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
update: [
|
||||
{
|
||||
id: settingId1,
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
is_active: false,
|
||||
},
|
||||
],
|
||||
delete: [settingId2],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(batchResponse.status).toEqual(200)
|
||||
expect(batchResponse.data.created).toHaveLength(1)
|
||||
expect(batchResponse.data.updated).toHaveLength(1)
|
||||
expect(batchResponse.data.deleted.ids).toContain(settingId2)
|
||||
|
||||
expect(batchResponse.data.created[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
entity_type: "product_collection",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
})
|
||||
)
|
||||
|
||||
expect(batchResponse.data.updated[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: settingId1,
|
||||
fields: ["title", "material"],
|
||||
is_active: false,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle empty batch request", async () => {
|
||||
const response = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.created).toEqual([])
|
||||
expect(response.data.updated).toEqual([])
|
||||
expect(response.data.deleted.ids).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("validation", () => {
|
||||
it("should reject non-translatable entity types", async () => {
|
||||
const error = await api
|
||||
.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "NonExistentEntity",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data.message).toContain(
|
||||
"NonExistentEntity is not a translatable entity"
|
||||
)
|
||||
})
|
||||
|
||||
it("should reject invalid fields for translatable entities", async () => {
|
||||
const error = await api
|
||||
.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "invalid_field", "another_invalid"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data.message).toContain("product_variant")
|
||||
expect(error.response.data.message).toContain("invalid_field")
|
||||
expect(error.response.data.message).toContain("another_invalid")
|
||||
})
|
||||
|
||||
it("should reject multiple invalid settings in a single batch", async () => {
|
||||
const error = await api
|
||||
.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "NonExistentEntity",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "invalid_field"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data.message).toContain(
|
||||
"NonExistentEntity is not a translatable entity"
|
||||
)
|
||||
expect(error.response.data.message).toContain("product_variant")
|
||||
expect(error.response.data.message).toContain("invalid_field")
|
||||
})
|
||||
|
||||
it("should accept valid fields for translatable entities", async () => {
|
||||
const response = await api.post(
|
||||
"/admin/translations/settings/batch",
|
||||
{
|
||||
create: [
|
||||
{
|
||||
entity_type: "product_variant",
|
||||
fields: ["title", "material"],
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
entity_type: "product_category",
|
||||
fields: ["name", "description"],
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
entity_type: "product_collection",
|
||||
fields: ["title"],
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.created).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -22,6 +22,9 @@ medusaIntegrationTestRunner({
|
||||
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(
|
||||
{},
|
||||
|
||||
Reference in New Issue
Block a user