diff --git a/.changeset/fine-wings-stick.md b/.changeset/fine-wings-stick.md new file mode 100644 index 0000000000..c8683697c9 --- /dev/null +++ b/.changeset/fine-wings-stick.md @@ -0,0 +1,13 @@ +--- +"@medusajs/fulfillment": patch +"@medusajs/translation": patch +"@medusajs/customer": patch +"@medusajs/core-flows": patch +"@medusajs/product": patch +"@medusajs/region": patch +"@medusajs/tax": patch +"@medusajs/types": patch +"@medusajs/medusa": patch +--- + +feat(translation,fulfillment,customer,product,region,tax,core-flows,medusa,types): Implement dynamic translation settings management diff --git a/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts b/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts index 428b7afa26..7f7143f40a 100644 --- a/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts @@ -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 }) diff --git a/integration-tests/http/__tests__/claims/claims-translations.spec.ts b/integration-tests/http/__tests__/claims/claims-translations.spec.ts index 8584c715c8..c36a57b4a4 100644 --- a/integration-tests/http/__tests__/claims/claims-translations.spec.ts +++ b/integration-tests/http/__tests__/claims/claims-translations.spec.ts @@ -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( diff --git a/integration-tests/http/__tests__/draft-order/admin/draft-order-translation.spec.ts b/integration-tests/http/__tests__/draft-order/admin/draft-order-translation.spec.ts index 5a2cd4c8a2..54cf7c3661 100644 --- a/integration-tests/http/__tests__/draft-order/admin/draft-order-translation.spec.ts +++ b/integration-tests/http/__tests__/draft-order/admin/draft-order-translation.spec.ts @@ -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", diff --git a/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts b/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts index 87cab294a0..da4c60690c 100644 --- a/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts +++ b/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts @@ -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 }) diff --git a/integration-tests/http/__tests__/order-edits/order-edit-translation.spec.ts b/integration-tests/http/__tests__/order-edits/order-edit-translation.spec.ts index a1a35a4e69..088fccfaae 100644 --- a/integration-tests/http/__tests__/order-edits/order-edit-translation.spec.ts +++ b/integration-tests/http/__tests__/order-edits/order-edit-translation.spec.ts @@ -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 }) diff --git a/integration-tests/http/__tests__/order/admin/order-translation.spec.ts b/integration-tests/http/__tests__/order/admin/order-translation.spec.ts index 260f8dcd8f..8435778f36 100644 --- a/integration-tests/http/__tests__/order/admin/order-translation.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order-translation.spec.ts @@ -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 }) diff --git a/integration-tests/http/__tests__/returns/return-translation.spec.ts b/integration-tests/http/__tests__/returns/return-translation.spec.ts index 746c1d2512..9b1545bb4d 100644 --- a/integration-tests/http/__tests__/returns/return-translation.spec.ts +++ b/integration-tests/http/__tests__/returns/return-translation.spec.ts @@ -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( diff --git a/integration-tests/http/__tests__/shipping-option/store/shipping-option-translation.spec.ts b/integration-tests/http/__tests__/shipping-option/store/shipping-option-translation.spec.ts index cebc8f14d2..e0260405cf 100644 --- a/integration-tests/http/__tests__/shipping-option/store/shipping-option-translation.spec.ts +++ b/integration-tests/http/__tests__/shipping-option/store/shipping-option-translation.spec.ts @@ -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( diff --git a/integration-tests/http/__tests__/translation/admin/locale.spec.ts b/integration-tests/http/__tests__/translation/admin/locale.spec.ts index 4d7a0a5d6b..db5e8c66d5 100644 --- a/integration-tests/http/__tests__/translation/admin/locale.spec.ts +++ b/integration-tests/http/__tests__/translation/admin/locale.spec.ts @@ -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 () => { diff --git a/integration-tests/http/__tests__/translation/admin/translation-settings.spec.ts b/integration-tests/http/__tests__/translation/admin/translation-settings.spec.ts new file mode 100644 index 0000000000..7098147cf6 --- /dev/null +++ b/integration-tests/http/__tests__/translation/admin/translation-settings.spec.ts @@ -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) + }) + }) + }) + }) + }, +}) diff --git a/integration-tests/http/__tests__/translation/admin/translation.spec.ts b/integration-tests/http/__tests__/translation/admin/translation.spec.ts index db3a199f56..9ad14ccf1f 100644 --- a/integration-tests/http/__tests__/translation/admin/translation.spec.ts +++ b/integration-tests/http/__tests__/translation/admin/translation.spec.ts @@ -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( {}, diff --git a/packages/core/core-flows/src/translation/steps/create-translation-settings.ts b/packages/core/core-flows/src/translation/steps/create-translation-settings.ts new file mode 100644 index 0000000000..a6fea5a843 --- /dev/null +++ b/packages/core/core-flows/src/translation/steps/create-translation-settings.ts @@ -0,0 +1,34 @@ +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { CreateTranslationSettingsDTO } from "@medusajs/types" + +export const createTranslationSettingsStepId = "create-translation-settings" + +export type CreateTranslationSettingsStepInput = + | CreateTranslationSettingsDTO + | CreateTranslationSettingsDTO[] + +export const createTranslationSettingsStep = createStep( + createTranslationSettingsStepId, + async (data: CreateTranslationSettingsStepInput, { container }) => { + const service = container.resolve(Modules.TRANSLATION) + + const normalizedInput = Array.isArray(data) ? data : [data] + + const created = await service.createTranslationSettings(normalizedInput) + + return new StepResponse( + created, + created.map((translationSettings) => translationSettings.id) + ) + }, + async (createdIds, { container }) => { + if (!createdIds?.length) { + return + } + + const service = container.resolve(Modules.TRANSLATION) + + await service.deleteTranslationSettings(createdIds) + } +) diff --git a/packages/core/core-flows/src/translation/steps/delete-translation-settings.ts b/packages/core/core-flows/src/translation/steps/delete-translation-settings.ts new file mode 100644 index 0000000000..04810a4ff3 --- /dev/null +++ b/packages/core/core-flows/src/translation/steps/delete-translation-settings.ts @@ -0,0 +1,28 @@ +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" + +export const deleteTranslationSettingsStepId = "delete-translation-settings" + +export const deleteTranslationSettingsStep = createStep( + deleteTranslationSettingsStepId, + async (data: string[], { container }) => { + const service = container.resolve(Modules.TRANSLATION) + + const previous = await service.listTranslationSettings({ + id: data, + }) + + await service.deleteTranslationSettings(data) + + return new StepResponse(void 0, previous) + }, + async (previous, { container }) => { + if (!previous?.length) { + return + } + + const service = container.resolve(Modules.TRANSLATION) + + await service.createTranslationSettings(previous) + } +) diff --git a/packages/core/core-flows/src/translation/steps/index.ts b/packages/core/core-flows/src/translation/steps/index.ts index cc9acf5555..16e19dc05f 100644 --- a/packages/core/core-flows/src/translation/steps/index.ts +++ b/packages/core/core-flows/src/translation/steps/index.ts @@ -2,3 +2,6 @@ export * from "./create-translations" export * from "./delete-translations" export * from "./update-translations" export * from "./validate-translations" +export * from "./create-translation-settings" +export * from "./update-translation-settings" +export * from "./delete-translation-settings" diff --git a/packages/core/core-flows/src/translation/steps/update-translation-settings.ts b/packages/core/core-flows/src/translation/steps/update-translation-settings.ts new file mode 100644 index 0000000000..cfa50abdc0 --- /dev/null +++ b/packages/core/core-flows/src/translation/steps/update-translation-settings.ts @@ -0,0 +1,35 @@ +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { UpdateTranslationSettingsDTO } from "@medusajs/types" + +export const updateTranslationSettingsStepId = "update-translation-settings" + +export type UpdateTranslationSettingsStepInput = + | UpdateTranslationSettingsDTO + | UpdateTranslationSettingsDTO[] + +export const updateTranslationSettingsStep = createStep( + updateTranslationSettingsStepId, + async (data: UpdateTranslationSettingsStepInput, { container }) => { + const service = container.resolve(Modules.TRANSLATION) + + const normalizedInput = Array.isArray(data) ? data : [data] + + const previous = await service.listTranslationSettings({ + id: normalizedInput.map((d) => d.id), + }) + + const updated = await service.updateTranslationSettings(normalizedInput) + + return new StepResponse(updated, previous) + }, + async (previous, { container }) => { + if (!previous?.length) { + return + } + + const service = container.resolve(Modules.TRANSLATION) + + await service.updateTranslationSettings(previous) + } +) diff --git a/packages/core/core-flows/src/translation/workflows/batch-translation-settings.ts b/packages/core/core-flows/src/translation/workflows/batch-translation-settings.ts new file mode 100644 index 0000000000..544763b163 --- /dev/null +++ b/packages/core/core-flows/src/translation/workflows/batch-translation-settings.ts @@ -0,0 +1,35 @@ +import { + createWorkflow, + parallelize, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + UpdateTranslationSettingsDTO, + CreateTranslationSettingsDTO, +} from "@medusajs/types" +import { + createTranslationSettingsStep, + deleteTranslationSettingsStep, + updateTranslationSettingsStep, +} from "../steps" + +export const batchTranslationSettingsWorkflowId = "batch-translation-settings" + +export interface BatchTranslationSettingsWorkflowInput { + create: CreateTranslationSettingsDTO[] + update: UpdateTranslationSettingsDTO[] + delete: string[] +} + +export const batchTranslationSettingsWorkflow = createWorkflow( + batchTranslationSettingsWorkflowId, + (input: BatchTranslationSettingsWorkflowInput) => { + const [created, updated, deleted] = parallelize( + createTranslationSettingsStep(input.create), + updateTranslationSettingsStep(input.update), + deleteTranslationSettingsStep(input.delete) + ) + + return new WorkflowResponse({ created, updated, deleted }) + } +) diff --git a/packages/core/core-flows/src/translation/workflows/create-translations.ts b/packages/core/core-flows/src/translation/workflows/create-translations.ts index cd9417bae9..47d9893178 100644 --- a/packages/core/core-flows/src/translation/workflows/create-translations.ts +++ b/packages/core/core-flows/src/translation/workflows/create-translations.ts @@ -7,7 +7,6 @@ import { } from "@medusajs/framework/workflows-sdk" import { emitEventStep } from "../../common/steps/emit-event" import { createTranslationsStep } from "../steps" -import { validateTranslationsStep } from "../steps" import { TranslationWorkflowEvents } from "@medusajs/framework/utils" /** @@ -27,7 +26,7 @@ export const createTranslationsWorkflowId = "create-translations" * * You can use this workflow within your own customizations or custom workflows, allowing you * to create translations in your custom flows. - * + * * @since 2.12.3 * @featureFlag translation * @@ -55,7 +54,6 @@ export const createTranslationsWorkflow = createWorkflow( ( input: WorkflowData ): WorkflowResponse => { - validateTranslationsStep(input.translations) const translations = createTranslationsStep(input.translations) const translationIdEvents = transform( diff --git a/packages/core/core-flows/src/translation/workflows/index.ts b/packages/core/core-flows/src/translation/workflows/index.ts index c2c3830095..fede24b36f 100644 --- a/packages/core/core-flows/src/translation/workflows/index.ts +++ b/packages/core/core-flows/src/translation/workflows/index.ts @@ -2,3 +2,4 @@ export * from "./create-translations" export * from "./delete-translations" export * from "./update-translations" export * from "./batch-translations" +export * from "./batch-translation-settings" diff --git a/packages/core/core-flows/src/translation/workflows/update-translations.ts b/packages/core/core-flows/src/translation/workflows/update-translations.ts index 343c9c298a..380a013e90 100644 --- a/packages/core/core-flows/src/translation/workflows/update-translations.ts +++ b/packages/core/core-flows/src/translation/workflows/update-translations.ts @@ -7,7 +7,6 @@ import { } from "@medusajs/framework/workflows-sdk" import { emitEventStep } from "../../common/steps/emit-event" import { updateTranslationsStep, UpdateTranslationsStepInput } from "../steps" -import { validateTranslationsStep } from "../steps" import { TranslationWorkflowEvents } from "@medusajs/framework/utils" /** @@ -22,13 +21,13 @@ export const updateTranslationsWorkflowId = "update-translations" * * You can use this workflow within your own customizations or custom workflows, allowing you * to update translations in your custom flows. - * + * * @since 2.12.3 * @featureFlag translation * * @example * To update translations by their IDs: - * + * * ```ts * const { result } = await updateTranslationsWorkflow(container) * .run({ @@ -61,11 +60,6 @@ export const updateTranslationsWorkflow = createWorkflow( ( input: WorkflowData ): WorkflowResponse => { - const validateInput = transform(input, (input) => { - return "translations" in input ? input.translations : [input.update] - }) - validateTranslationsStep(validateInput) - const translations = updateTranslationsStep(input) const translationIdEvents = transform( diff --git a/packages/core/types/src/http/translations/admin/entities.ts b/packages/core/types/src/http/translations/admin/entities.ts index 944d08cccc..541f41f616 100644 --- a/packages/core/types/src/http/translations/admin/entities.ts +++ b/packages/core/types/src/http/translations/admin/entities.ts @@ -6,7 +6,7 @@ export interface AdminTranslation { /** * The ID of the entity being translated. - * + * * @example * "prod_123" */ @@ -14,7 +14,7 @@ export interface AdminTranslation { /** * The name of the table that the translation belongs to. - * + * * @example * "product" */ @@ -22,7 +22,7 @@ export interface AdminTranslation { /** * The BCP 47 language tag code for this translation. - * + * * @example * "en-US" */ @@ -31,7 +31,7 @@ export interface AdminTranslation { /** * The translations of the resource. * The object's keys are the field names of the data model, and its value is the translated value. - * + * * @example * { * "title": "Product Title", @@ -55,3 +55,34 @@ export interface AdminTranslation { */ deleted_at: Date | string | null } + +export interface AdminTranslationSettings { + /** + * The ID of the settings. + */ + id: string + /** + * The date and time the settings were created. + */ + created_at: Date | string + /** + * The date and time the settings were last updated. + */ + updated_at: Date | string + /** + * The date and time the settings were deleted. + */ + deleted_at: Date | string | null + /** + * The entity type. + */ + entity_type: string + /** + * The translatable fields. + */ + fields: string[] + /** + * Whether the entity translatable status is enabled. + */ + is_active: boolean +} diff --git a/packages/core/types/src/http/translations/admin/responses.ts b/packages/core/types/src/http/translations/admin/responses.ts index 6466aed215..28f4186811 100644 --- a/packages/core/types/src/http/translations/admin/responses.ts +++ b/packages/core/types/src/http/translations/admin/responses.ts @@ -1,5 +1,5 @@ import { PaginatedResponse } from "../../common" -import { AdminTranslation } from "./entities" +import { AdminTranslation, AdminTranslationSettings } from "./entities" export interface AdminTranslationsResponse { /** @@ -100,6 +100,25 @@ export interface AdminTranslationSettingsResponse { translatable_fields: Record } +export interface AdminBatchTranslationSettingsResponse { + /** + * The created settings. + */ + created: AdminTranslationSettings[] + /** + * The updated settings. + */ + updated: AdminTranslationSettings[] + /** + * The deleted settings. + */ + deleted: { + ids: string[] + object: "translation_settings" + deleted: boolean + } +} + /** * Response for translation entities endpoint. * Returns paginated entities with only their translatable fields and all their translations. diff --git a/packages/core/types/src/translation/common.ts b/packages/core/types/src/translation/common.ts index aabb00306e..b79b1cce42 100644 --- a/packages/core/types/src/translation/common.ts +++ b/packages/core/types/src/translation/common.ts @@ -99,6 +99,11 @@ export interface TranslationSettingsDTO { */ fields: string[] + /** + * Whether the entity translatable status is enabled. + */ + is_active: boolean + /** * The date and time the settings were created. */ @@ -168,13 +173,30 @@ export interface FilterableTranslationProps locale_code?: string | string[] | OperatorMap } +export interface FilterableTranslationSettingsProps + extends BaseFilterable { + /** + * The IDs to filter the translation settings by. + */ + id?: string[] | string | OperatorMap + + /** + * Filter translation settings by entity type. + */ + entity_type?: string | string[] | OperatorMap + /** + * Filter translation settings by active status. + */ + is_active?: boolean | OperatorMap +} + /** * Input for getStatistics method. */ export interface TranslationStatisticsInput { /** * Locales to check translations for. - * + * * @example * ["en-US", "fr-FR"] */ @@ -183,15 +205,18 @@ export interface TranslationStatisticsInput { /** * Key-value pairs of entity types and their configurations. */ - entities: Record + entities: Record< + string, + { + /** + * Total number of records for the entity type. + * For example, total number of products. + * + * This is necessary to compute expected translation counts. + */ + count: number + } + > } /** diff --git a/packages/core/types/src/translation/mutations.ts b/packages/core/types/src/translation/mutations.ts index c8bed54477..5facec20f1 100644 --- a/packages/core/types/src/translation/mutations.ts +++ b/packages/core/types/src/translation/mutations.ts @@ -9,7 +9,7 @@ export interface CreateLocaleDTO { /** * The BCP 47 language tag code of the locale. - * + * * @example * "en-US" */ @@ -17,7 +17,7 @@ export interface CreateLocaleDTO { /** * The human-readable name of the locale. - * + * * @example * "English (United States)" */ @@ -30,7 +30,7 @@ export interface CreateLocaleDTO { export interface UpdateLocaleDataDTO { /** * The BCP 47 language tag code of the locale. - * + * * @example * "en-US" */ @@ -38,7 +38,7 @@ export interface UpdateLocaleDataDTO { /** * The human-readable name of the locale. - * + * * @example * "English (United States)" */ @@ -66,7 +66,7 @@ export interface UpsertLocaleDTO { /** * The BCP 47 language tag code of the locale. - * + * * @example * "en-US" */ @@ -74,7 +74,7 @@ export interface UpsertLocaleDTO { /** * The human-readable name of the locale. - * + * * @example * "English (United States)" */ @@ -87,7 +87,7 @@ export interface UpsertLocaleDTO { export interface CreateTranslationDTO { /** * The ID of the data model being translated. - * + * * @example * "prod_123" */ @@ -95,7 +95,7 @@ export interface CreateTranslationDTO { /** * The name of the table that the translation belongs to. - * + * * @example * "product" */ @@ -103,7 +103,7 @@ export interface CreateTranslationDTO { /** * The BCP 47 language tag code for this translation. - * + * * @example * "en-US" */ @@ -111,7 +111,7 @@ export interface CreateTranslationDTO { /** * The translated fields as key-value pairs. - * + * * @example * { * "title": "Product Title", @@ -127,7 +127,7 @@ export interface CreateTranslationDTO { export interface UpdateTranslationDataDTO { /** * The ID of the data model being translated. - * + * * @example * "prod_123" */ @@ -135,7 +135,7 @@ export interface UpdateTranslationDataDTO { /** * The name of the table that the translation belongs to. - * + * * @example * "product" */ @@ -143,7 +143,7 @@ export interface UpdateTranslationDataDTO { /** * The BCP 47 language tag code for this translation. - * + * * @example * "en-US" */ @@ -151,7 +151,7 @@ export interface UpdateTranslationDataDTO { /** * The translated fields as key-value pairs. - * + * * @example * { * "title": "Product Title", @@ -182,7 +182,7 @@ export interface UpsertTranslationDTO { /** * The ID of the data model being translated. - * + * * @example * "prod_123" */ @@ -190,7 +190,7 @@ export interface UpsertTranslationDTO { /** * The name of the table that the translation belongs to. - * + * * @example * "product" */ @@ -198,7 +198,7 @@ export interface UpsertTranslationDTO { /** * The BCP 47 language tag code for this translation. - * + * * @example * "en-US" */ @@ -206,7 +206,7 @@ export interface UpsertTranslationDTO { /** * The translated fields as key-value pairs. - * + * * @example * { * "title": "Product Title", @@ -215,3 +215,52 @@ export interface UpsertTranslationDTO { */ translations?: Record } + +export interface CreateTranslationSettingsDTO { + /** + * The entity type. + * + * @example + * "product" + */ + entity_type: string + /** + * The translatable fields. + * + * @example + * ["title", "description", "material"] + */ + fields: string[] + /** + * Whether the entity translatable status is enabled. + */ + is_active?: boolean +} + +/** + * The translation settings to be created or updated. + */ +export interface UpdateTranslationSettingsDTO { + /** + * The ID of the translation settings to update. + */ + id: string + /** + * The entity type. + * + * @example + * "product" + */ + entity_type?: string + /** + * The translatable fields. + * + * @example + * ["title", "description", "material"] + */ + fields?: string[] + /** + * Whether the entity translatable status is enabled. + */ + is_active?: boolean +} diff --git a/packages/core/types/src/translation/service.ts b/packages/core/types/src/translation/service.ts index acd19e9ab7..f63a5865b3 100644 --- a/packages/core/types/src/translation/service.ts +++ b/packages/core/types/src/translation/service.ts @@ -5,23 +5,27 @@ import { Context } from "../shared-context" import { FilterableLocaleProps, FilterableTranslationProps, + FilterableTranslationSettingsProps, LocaleDTO, TranslationDTO, + TranslationSettingsDTO, TranslationStatisticsInput, TranslationStatisticsOutput, } from "./common" import { CreateLocaleDTO, CreateTranslationDTO, + CreateTranslationSettingsDTO, UpdateLocaleDTO, UpdateLocaleDataDTO, UpdateTranslationDTO, UpdateTranslationDataDTO, + UpdateTranslationSettingsDTO, } from "./mutations" /** * The main service interface for the Translation Module. - * + * * @privateRemarks * Method signatures match what MedusaService generates. */ @@ -43,12 +47,12 @@ export interface ITranslationModuleService extends IModuleService { * ``` * * To specify relations that should be retrieved: - * + * * :::note - * + * * You can only retrieve data models defined in the same module. To retrieve linked data models * from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead. - * + * * ::: * * ```ts @@ -82,12 +86,12 @@ export interface ITranslationModuleService extends IModuleService { * ``` * * To specify relations that should be retrieved within the locales: - * + * * :::note - * + * * You can only retrieve data models defined in the same module. To retrieve linked data models * from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead. - * + * * ::: * * ```ts @@ -141,12 +145,12 @@ export interface ITranslationModuleService extends IModuleService { * ``` * * To specify relations that should be retrieved within the locales: - * + * * :::note - * + * * You can only retrieve data models defined in the same module. To retrieve linked data models * from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead. - * + * * ::: * * ```ts @@ -250,7 +254,7 @@ export interface ITranslationModuleService extends IModuleService { * * @example * To update locales by their IDs: - * + * * ```ts * const locales = await translationModuleService.updateLocales([ * { @@ -265,7 +269,7 @@ export interface ITranslationModuleService extends IModuleService { * ``` * * To update locales by a selector: - * + * * ```ts * const locales = await translationModuleService.updateLocales({ * selector: { @@ -299,7 +303,7 @@ export interface ITranslationModuleService extends IModuleService { * @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects with IDs identifying the locales to delete. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the locales are deleted. - * + * * @example * await translationModuleService.deleteLocales(["loc_123", "loc_321"]) */ @@ -336,7 +340,7 @@ export interface ITranslationModuleService extends IModuleService { * @returns {Promise | void>} An object that includes the IDs of related records that were restored. * * If there are no related records restored, the promise resolves to `void`. - * + * * @example * await translationModuleService.restoreLocales(["loc_123", "loc_321"]) */ @@ -362,12 +366,12 @@ export interface ITranslationModuleService extends IModuleService { * ``` * * To specify relations that should be retrieved: - * + * * :::note - * + * * You can only retrieve data models defined in the same module. To retrieve linked data models * from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead. - * + * * ::: * * ```ts @@ -401,12 +405,12 @@ export interface ITranslationModuleService extends IModuleService { * ``` * * To specify relations that should be retrieved within the translations: - * + * * :::note - * + * * You can only retrieve data models defined in the same module. To retrieve linked data models * from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead. - * + * * ::: * * ```ts @@ -419,7 +423,7 @@ export interface ITranslationModuleService extends IModuleService { * } * ) * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: * * ```ts @@ -460,12 +464,12 @@ export interface ITranslationModuleService extends IModuleService { * ``` * * To specify relations that should be retrieved within the translations: - * + * * :::note - * + * * You can only retrieve data models defined in the same module. To retrieve linked data models * from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead. - * + * * ::: * * ```ts @@ -478,7 +482,7 @@ export interface ITranslationModuleService extends IModuleService { * } * ) * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: * * ```ts @@ -618,7 +622,7 @@ export interface ITranslationModuleService extends IModuleService { * @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects with IDs identifying the translations to delete. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the translations are deleted. - * + * * @example * await translationModuleService.deleteTranslations("tra_123") */ @@ -635,7 +639,7 @@ export interface ITranslationModuleService extends IModuleService { * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise | void>} An object that includes the IDs of related records that were also soft deleted. * If there are no related records, the promise resolves to `void`. - * + * * @example * await translationModuleService.softDeleteTranslations(["tra_123", "tra_321"]) */ @@ -667,7 +671,7 @@ export interface ITranslationModuleService extends IModuleService { /** * This method retrieves translation statistics for the specified entities and locales. * It's useful to understand the translation coverage of different entities across various locales. - * + * * You can use this method to get insights into how many fields are translated, missing translations, * and the expected number of translations based on the entities and locales provided. * @@ -731,4 +735,189 @@ export interface ITranslationModuleService extends IModuleService { entityType?: string, sharedContext?: Context ): Promise> + + /** + * This method creates a translation setting. + * + * @param {CreateTranslationSettingsDTO} data - The translation setting to be created. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The created translation setting. + * + * @example + * const translationSetting = await translationModuleService.createTranslationSettings({ + * entity_type: "product", + * fields: ["title", "description"], + * is_active: true, + * }) + */ + createTranslationSettings( + data: CreateTranslationSettingsDTO, + sharedContext?: Context + ): Promise + + /** + * + * @param data - The translation settings to be created. + * @param sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The created translation settings. + * + * @example + * const translationSettings = await translationModuleService.createTranslationSettings([ + * { + * entity_type: "product", + * fields: ["title", "description"], + * is_active: true, + * }, + * ]) + */ + createTranslationSettings( + data: CreateTranslationSettingsDTO[], + sharedContext?: Context + ): Promise + + /** + * This method updates an existent translation setting. The ID should be included in the data object. + } + * @param {UpdateTranslationSettingsDTO} data - The attributes to update in the translation setting (including id). + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated translation setting. + * + * @example + * const translationSettings = await translationModuleService.updateTranslationSettings([ + * { + * id: "ts_123", + * entity_type: "product_collection", + * fields: ["title"], + * is_active: true, + * }, + * ]) + */ + updateTranslationSettings( + data: UpdateTranslationSettingsDTO, + sharedContext?: Context + ): Promise + + /** + * This method updates one or more existent translation settings. + * @param {UpdateTranslationSettingsDTO[]} data - The translation settings to update. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated translation settings. + * + * @example + * const translationSettings = await translationModuleService.updateTranslationSettings([ + * { + * id: "ts_123", + * entity_type: "product_collection", + * fields: ["title"], + * is_active: true, + * }, + * ]) + */ + updateTranslationSettings( + data: UpdateTranslationSettingsDTO[], + sharedContext?: Context + ): Promise + + /** + * This method deletes one or more translation settings. + * + * @param {string[]} input - The IDs of the translation settings to delete. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the translation settings are deleted. + * + * @example + * await translationModuleService.deleteTranslationSettings([ + * "ts_123", + * "ts_321", + * ]) + */ + deleteTranslationSettings( + input: string[], + sharedContext?: Context + ): Promise + + /** + * This method retrieves a paginated list of translation settings based on optional filters and configuration. + * + * @param {FilterableTranslationSettingsProps} filters - The filters to apply on the retrieved translation settings. + * @param {FindConfig} config - The configurations determining how the translation settings are retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a translation settings. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The list of translation settings. + * + * @example + * const translationSettings = await translationModuleService.listTranslationSettings({ + * entity_type: "product", + * is_active: true, + * }) + * // Returns: [ + * // { + * // id: "ts_123", + * // entity_type: "product", + * // fields: ["title", "description"], + * // is_active: true, + * // }, + * // ] + */ + listTranslationSettings( + filters?: FilterableTranslationSettingsProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + /** + * This method retrieves a paginated list of translation settings based on optional filters and configuration, along with the total count. + * + * @param {FilterableTranslationSettingsProps} filters - The filters to apply on the retrieved translation settings. + * @param {FindConfig} config - The configurations determining how the translation settings are retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a translation settings. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise<[TranslationSettingsDTO[], number]>} The list of translation settings along with their total count. + * + * @example + * const [translationSettings, count] = await translationModuleService.listAndCountTranslationSettings({ + * entity_type: "product", + * is_active: true, + * }) + * // Returns: [ + * // [ + * // { + * // id: "ts_123", + * // entity_type: "product", + * // fields: ["title", "description"], + * // is_active: true, + * // }, + * // ], + * // 1, + * // ] + */ + listAndCountTranslationSettings( + filters?: FilterableTranslationSettingsProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[TranslationSettingsDTO[], number]> + + /** + * This method retrieves a translation setting by its ID. + * + * @param {string} id - The ID of the translation setting to retrieve. + * @param {FindConfig} config - The configurations determining how the translation setting is retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a translation settings. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The retrieved translation setting. + * + * @example + * const translationSetting = await translationModuleService.retrieveTranslationSettings("ts_123") + * // Returns: { + * // id: "ts_123", + * // entity_type: "product", + * // fields: ["title", "description"], + * // is_active: true, + * // } + */ + retrieveTranslationSettings( + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise } diff --git a/packages/medusa/src/api/admin/translations/middlewares.ts b/packages/medusa/src/api/admin/translations/middlewares.ts index 42fdf22556..aaf4e25066 100644 --- a/packages/medusa/src/api/admin/translations/middlewares.ts +++ b/packages/medusa/src/api/admin/translations/middlewares.ts @@ -5,6 +5,7 @@ import { } from "@medusajs/framework" import { AdminBatchTranslations, + AdminBatchTranslationSettings, AdminGetTranslationsParams, AdminTranslationEntitiesParams, AdminTranslationSettingsParams, @@ -44,6 +45,11 @@ export const adminTranslationsRoutesMiddlewares: MiddlewareRoute[] = [ validateAndTransformQuery(AdminTranslationSettingsParams, {}), ], }, + { + method: ["POST"], + matcher: "/admin/translations/settings/batch", + middlewares: [validateAndTransformBody(AdminBatchTranslationSettings)], + }, { method: ["GET"], matcher: "/admin/translations/entities", diff --git a/packages/medusa/src/api/admin/translations/settings/batch/route.ts b/packages/medusa/src/api/admin/translations/settings/batch/route.ts new file mode 100644 index 0000000000..8b67b59fda --- /dev/null +++ b/packages/medusa/src/api/admin/translations/settings/batch/route.ts @@ -0,0 +1,39 @@ +import { batchTranslationSettingsWorkflow } from "@medusajs/core-flows" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { defineFileConfig, FeatureFlag } from "@medusajs/framework/utils" +import { HttpTypes } from "@medusajs/types" +import TranslationFeatureFlag from "../../../../../feature-flags/translation" +import { AdminBatchTranslationSettingsType } from "../../validators" + +/** + * @since 2.12.5 + * @featureFlag translation + */ +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { create = [], update = [], delete: deleteIds = [] } = req.validatedBody + + const { result } = await batchTranslationSettingsWorkflow(req.scope).run({ + input: { + create, + update, + delete: deleteIds, + }, + }) + + return res.status(200).json({ + created: result.created, + updated: result.updated, + deleted: { + ids: deleteIds, + object: "translation_settings", + deleted: true, + }, + }) +} + +defineFileConfig({ + isDisabled: () => !FeatureFlag.isFeatureEnabled(TranslationFeatureFlag.key), +}) diff --git a/packages/medusa/src/api/admin/translations/validators.ts b/packages/medusa/src/api/admin/translations/validators.ts index 3aaac9de6c..50d68d5b53 100644 --- a/packages/medusa/src/api/admin/translations/validators.ts +++ b/packages/medusa/src/api/admin/translations/validators.ts @@ -72,6 +72,27 @@ export const AdminTranslationSettingsParams = z.object({ entity_type: z.string().optional(), }) +const AdminUpdateTranslationSettings = z.object({ + id: z.string(), + entity_type: z.string().optional(), + fields: z.array(z.string()).optional(), + is_active: z.boolean().optional(), +}) + +const AdminCreateTranslationSettings = z.object({ + entity_type: z.string(), + fields: z.array(z.string()), + is_active: z.boolean().optional(), +}) + +export type AdminBatchTranslationSettingsType = z.infer< + typeof AdminBatchTranslationSettings +> +export const AdminBatchTranslationSettings = createBatchBody( + AdminCreateTranslationSettings, + AdminUpdateTranslationSettings +) + export type AdminTranslationEntitiesParamsType = z.infer< typeof AdminTranslationEntitiesParams > diff --git a/packages/modules/customer/src/models/customer-group.ts b/packages/modules/customer/src/models/customer-group.ts index a43389672e..adfec998c1 100644 --- a/packages/modules/customer/src/models/customer-group.ts +++ b/packages/modules/customer/src/models/customer-group.ts @@ -5,7 +5,7 @@ import { CustomerGroupCustomer } from "@models" const CustomerGroup = model .define("CustomerGroup", { id: model.id({ prefix: "cusgroup" }).primaryKey(), - name: model.text().searchable(), + name: model.text().searchable().translatable(), metadata: model.json().nullable(), created_by: model.text().nullable(), customers: model.manyToMany(() => Customer, { diff --git a/packages/modules/fulfillment/src/models/shipping-option-type.ts b/packages/modules/fulfillment/src/models/shipping-option-type.ts index 31c74b3bbb..ed62cc2bbc 100644 --- a/packages/modules/fulfillment/src/models/shipping-option-type.ts +++ b/packages/modules/fulfillment/src/models/shipping-option-type.ts @@ -4,8 +4,8 @@ import { ShippingOption } from "./shipping-option" export const ShippingOptionType = model.define("shipping_option_type", { id: model.id({ prefix: "sotype" }).primaryKey(), - label: model.text().searchable(), - description: model.text().searchable().nullable(), + label: model.text().searchable().translatable(), + description: model.text().searchable().translatable().nullable(), code: model.text().searchable(), shipping_options: model.hasMany(() => ShippingOption, { mappedBy: "type", diff --git a/packages/modules/fulfillment/src/models/shipping-option.ts b/packages/modules/fulfillment/src/models/shipping-option.ts index a43c6227f7..b160d6faef 100644 --- a/packages/modules/fulfillment/src/models/shipping-option.ts +++ b/packages/modules/fulfillment/src/models/shipping-option.ts @@ -10,7 +10,7 @@ import { ShippingProfile } from "./shipping-profile" export const ShippingOption = model .define("shipping_option", { id: model.id({ prefix: "so" }).primaryKey(), - name: model.text().searchable(), + name: model.text().searchable().translatable(), price_type: model .enum(ShippingOptionPriceType) .default(ShippingOptionPriceType.FLAT), diff --git a/packages/modules/product/src/models/product-category.ts b/packages/modules/product/src/models/product-category.ts index a80badde66..a63aca811b 100644 --- a/packages/modules/product/src/models/product-category.ts +++ b/packages/modules/product/src/models/product-category.ts @@ -4,8 +4,8 @@ import Product from "./product" const ProductCategory = model .define("ProductCategory", { id: model.id({ prefix: "pcat" }).primaryKey(), - name: model.text().searchable(), - description: model.text().searchable().default(""), + name: model.text().searchable().translatable(), + description: model.text().searchable().translatable().default(""), handle: model.text().searchable(), mpath: model.text(), is_active: model.boolean().default(false), diff --git a/packages/modules/product/src/models/product-collection.ts b/packages/modules/product/src/models/product-collection.ts index 36905e64b7..bcd3ea607f 100644 --- a/packages/modules/product/src/models/product-collection.ts +++ b/packages/modules/product/src/models/product-collection.ts @@ -4,7 +4,7 @@ import Product from "./product" const ProductCollection = model .define("ProductCollection", { id: model.id({ prefix: "pcol" }).primaryKey(), - title: model.text().searchable(), + title: model.text().searchable().translatable(), handle: model.text(), metadata: model.json().nullable(), products: model.hasMany(() => Product, { diff --git a/packages/modules/product/src/models/product-option-value.ts b/packages/modules/product/src/models/product-option-value.ts index 21e0aa7461..40a06d2bea 100644 --- a/packages/modules/product/src/models/product-option-value.ts +++ b/packages/modules/product/src/models/product-option-value.ts @@ -4,7 +4,7 @@ import { ProductOption, ProductVariant } from "./index" const ProductOptionValue = model .define("ProductOptionValue", { id: model.id({ prefix: "optval" }).primaryKey(), - value: model.text(), + value: model.text().translatable(), metadata: model.json().nullable(), option: model .belongsTo(() => ProductOption, { diff --git a/packages/modules/product/src/models/product-option.ts b/packages/modules/product/src/models/product-option.ts index 553209193a..ab1e1adde3 100644 --- a/packages/modules/product/src/models/product-option.ts +++ b/packages/modules/product/src/models/product-option.ts @@ -5,7 +5,7 @@ import ProductOptionValue from "./product-option-value" const ProductOption = model .define("ProductOption", { id: model.id({ prefix: "opt" }).primaryKey(), - title: model.text().searchable(), + title: model.text().searchable().translatable(), metadata: model.json().nullable(), product: model.belongsTo(() => Product, { mappedBy: "options", diff --git a/packages/modules/product/src/models/product-tag.ts b/packages/modules/product/src/models/product-tag.ts index e3a7389a9a..e7f39d6b04 100644 --- a/packages/modules/product/src/models/product-tag.ts +++ b/packages/modules/product/src/models/product-tag.ts @@ -6,7 +6,7 @@ const ProductTag = model { tableName: "product_tag", name: "ProductTag" }, { id: model.id({ prefix: "ptag" }).primaryKey(), - value: model.text().searchable(), + value: model.text().searchable().translatable(), metadata: model.json().nullable(), products: model.manyToMany(() => Product, { mappedBy: "tags", diff --git a/packages/modules/product/src/models/product-type.ts b/packages/modules/product/src/models/product-type.ts index 789b62091e..991fa0bd46 100644 --- a/packages/modules/product/src/models/product-type.ts +++ b/packages/modules/product/src/models/product-type.ts @@ -4,7 +4,7 @@ import { Product } from "@models" const ProductType = model .define("ProductType", { id: model.id({ prefix: "ptyp" }).primaryKey(), - value: model.text().searchable(), + value: model.text().searchable().translatable(), metadata: model.json().nullable(), products: model.hasMany(() => Product, { mappedBy: "type", diff --git a/packages/modules/product/src/models/product-variant.ts b/packages/modules/product/src/models/product-variant.ts index 75bbb89361..26f87176ef 100644 --- a/packages/modules/product/src/models/product-variant.ts +++ b/packages/modules/product/src/models/product-variant.ts @@ -5,7 +5,7 @@ import ProductVariantProductImage from "./product-variant-product-image" const ProductVariant = model .define("ProductVariant", { id: model.id({ prefix: "variant" }).primaryKey(), - title: model.text().searchable(), + title: model.text().searchable().translatable(), sku: model.text().searchable().nullable(), barcode: model.text().searchable().nullable(), ean: model.text().searchable().nullable(), @@ -15,7 +15,7 @@ const ProductVariant = model hs_code: model.text().nullable(), origin_country: model.text().nullable(), mid_code: model.text().nullable(), - material: model.text().nullable(), + material: model.text().translatable().nullable(), weight: model.number().nullable(), length: model.number().nullable(), height: model.number().nullable(), diff --git a/packages/modules/product/src/models/product.ts b/packages/modules/product/src/models/product.ts index a9daced7c0..403cad95d2 100644 --- a/packages/modules/product/src/models/product.ts +++ b/packages/modules/product/src/models/product.ts @@ -11,10 +11,10 @@ import ProductVariant from "./product-variant" const Product = model .define("Product", { id: model.id({ prefix: "prod" }).primaryKey(), - title: model.text().searchable(), + title: model.text().searchable().translatable(), handle: model.text(), - subtitle: model.text().searchable().nullable(), - description: model.text().searchable().nullable(), + subtitle: model.text().searchable().translatable().nullable(), + description: model.text().searchable().translatable().nullable(), is_giftcard: model.boolean().default(false), status: model .enum(ProductUtils.ProductStatus) @@ -27,7 +27,7 @@ const Product = model origin_country: model.text().nullable(), hs_code: model.text().nullable(), mid_code: model.text().nullable(), - material: model.text().nullable(), + material: model.text().translatable().nullable(), discountable: model.boolean().default(true), external_id: model.text().nullable(), metadata: model.json().nullable(), diff --git a/packages/modules/region/src/models/region.ts b/packages/modules/region/src/models/region.ts index cfc64a04ba..bfa70eca96 100644 --- a/packages/modules/region/src/models/region.ts +++ b/packages/modules/region/src/models/region.ts @@ -3,7 +3,7 @@ import RegionCountry from "./country" export default model.define("region", { id: model.id({ prefix: "reg" }).primaryKey(), - name: model.text().searchable(), + name: model.text().searchable().translatable(), currency_code: model.text().searchable(), automatic_taxes: model.boolean().default(true), countries: model.hasMany(() => RegionCountry), diff --git a/packages/modules/tax/src/models/tax-rate.ts b/packages/modules/tax/src/models/tax-rate.ts index 1b57fbc5a4..966e9b10d3 100644 --- a/packages/modules/tax/src/models/tax-rate.ts +++ b/packages/modules/tax/src/models/tax-rate.ts @@ -7,7 +7,7 @@ const TaxRate = model id: model.id({ prefix: "txr" }).primaryKey(), rate: model.float().nullable(), code: model.text().searchable(), - name: model.text().searchable(), + name: model.text().searchable().translatable(), is_default: model.boolean().default(false), is_combinable: model.boolean().default(false), tax_region: model.belongsTo(() => TaxRegion, { diff --git a/packages/modules/translation/integration-tests/__tests__/translation-module-service.spec.ts b/packages/modules/translation/integration-tests/__tests__/translation-module-service.spec.ts index bcb54628e3..0172ed1f44 100644 --- a/packages/modules/translation/integration-tests/__tests__/translation-module-service.spec.ts +++ b/packages/modules/translation/integration-tests/__tests__/translation-module-service.spec.ts @@ -1,15 +1,42 @@ import { ITranslationModuleService } from "@medusajs/framework/types" -import { Module, Modules } from "@medusajs/framework/utils" +import { DmlEntity, Module, Modules } from "@medusajs/framework/utils" import { moduleIntegrationTestRunner } from "@medusajs/test-utils" import TranslationModuleService from "@services/translation-module" import { createLocaleFixture, createTranslationFixture } from "../__fixtures__" jest.setTimeout(100000) +// Set up the mock before module initialization +let mockGetTranslatableEntities: jest.SpyInstance + moduleIntegrationTestRunner({ moduleName: Modules.TRANSLATION, + hooks: { + beforeModuleInit: async () => { + mockGetTranslatableEntities = jest.spyOn( + DmlEntity, + "getTranslatableEntities" + ) + mockGetTranslatableEntities.mockReturnValue([ + { + entity: "Product", + fields: ["title", "description", "subtitle", "material"], + }, + { entity: "ProductVariant", fields: ["title", "material"] }, + { entity: "ProductCategory", fields: ["name"] }, + ]) + }, + }, testSuite: ({ service }) => { describe("Translation Module Service", () => { + beforeEach(async () => { + await service.__hooks?.onApplicationStart?.().catch(() => {}) + }) + afterAll(() => { + // Restore the mock after all tests complete + mockGetTranslatableEntities.mockRestore() + }) + it(`should export the appropriate linkable configuration`, () => { const linkable = Module(Modules.TRANSLATION, { service: TranslationModuleService, diff --git a/packages/modules/translation/src/index.ts b/packages/modules/translation/src/index.ts index b99064384e..75ac055d4c 100644 --- a/packages/modules/translation/src/index.ts +++ b/packages/modules/translation/src/index.ts @@ -1,12 +1,11 @@ import "./types" import { Module } from "@medusajs/framework/utils" import TranslationModuleService from "@services/translation-module" -import loadConfig from "./loaders/config" import loadDefaults from "./loaders/defaults" export const TRANSLATION_MODULE = "translation" export default Module(TRANSLATION_MODULE, { service: TranslationModuleService, - loaders: [loadDefaults, loadConfig], + loaders: [loadDefaults], }) diff --git a/packages/modules/translation/src/loaders/config.ts b/packages/modules/translation/src/loaders/config.ts deleted file mode 100644 index d76f91aee4..0000000000 --- a/packages/modules/translation/src/loaders/config.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - LoaderOptions, - Logger, - ModulesSdkTypes, -} from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -import { TRANSLATABLE_FIELDS_CONFIG_KEY } from "@utils/constants" -import { asValue } from "awilix" -import { translatableFieldsConfig } from "../utils/translatable-fields" -import Settings from "@models/settings" -import type { TranslationModuleOptions } from "../types" - -export default async ({ - container, - options, -}: LoaderOptions): Promise => { - const logger = - container.resolve(ContainerRegistrationKeys.LOGGER) ?? console - const settingsService: ModulesSdkTypes.IMedusaInternalService< - typeof Settings - > = container.resolve("translationSettingsService") - - const mergedConfig: Record = translatableFieldsConfig - - const userProvidedFields = options?.entities ?? [] - for (const field of userProvidedFields) { - mergedConfig[field.type] ??= [] - mergedConfig[field.type] = Array.from( - new Set([...(mergedConfig[field.type] ?? []), ...field.fields]) - ) - } - - try { - const existingSettings = await settingsService.list( - {}, - { select: ["id", "entity_type"] } - ) - const existingByEntityType = new Map( - existingSettings.map((s) => [s.entity_type, s.id]) - ) - - const settingsToUpsert = Object.entries(mergedConfig).map( - ([entityType, fields]) => { - const existingId = existingByEntityType.get(entityType) - return existingId - ? { id: existingId, entity_type: entityType, fields } - : { entity_type: entityType, fields } - } - ) - - const resp = await settingsService.upsert(settingsToUpsert) - logger.debug(`Loaded ${resp.length} translation settings`) - } catch (error) { - logger.warn( - `Failed to load translation settings, skipping loader. Original error: ${error.message}` - ) - } - - container.register(TRANSLATABLE_FIELDS_CONFIG_KEY, asValue(mergedConfig)) -} diff --git a/packages/modules/translation/src/migrations/.snapshot-medusa-translation.json b/packages/modules/translation/src/migrations/.snapshot-medusa-translation.json index 9fb74f51b3..2302ec26e8 100644 --- a/packages/modules/translation/src/migrations/.snapshot-medusa-translation.json +++ b/packages/modules/translation/src/migrations/.snapshot-medusa-translation.json @@ -293,6 +293,16 @@ "nullable": false, "mappedType": "json" }, + "is_active": { + "name": "is_active", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "true", + "mappedType": "boolean" + }, "created_at": { "name": "created_at", "type": "timestamptz", diff --git a/packages/modules/translation/src/migrations/Migration20260108122757.ts b/packages/modules/translation/src/migrations/Migration20260108122757.ts new file mode 100644 index 0000000000..21763285b3 --- /dev/null +++ b/packages/modules/translation/src/migrations/Migration20260108122757.ts @@ -0,0 +1,13 @@ +import { Migration } from "@medusajs/framework/mikro-orm/migrations"; + +export class Migration20260108122757 extends Migration { + + override async up(): Promise { + this.addSql(`alter table if exists "translation_settings" add column if not exists "is_active" boolean not null default true;`); + } + + override async down(): Promise { + this.addSql(`alter table if exists "translation_settings" drop column if exists "is_active";`); + } + +} diff --git a/packages/modules/translation/src/models/settings.ts b/packages/modules/translation/src/models/settings.ts index f3e06d9e35..b84588510d 100644 --- a/packages/modules/translation/src/models/settings.ts +++ b/packages/modules/translation/src/models/settings.ts @@ -18,6 +18,10 @@ const Settings = model * ["title", "description", "material"] */ fields: model.json(), + /** + * Wether the entity translatable status is enabled. + */ + is_active: model.boolean().default(true), }) .indexes([ { diff --git a/packages/modules/translation/src/services/translation-module.ts b/packages/modules/translation/src/services/translation-module.ts index fb7a4afa6d..1372f16ad0 100644 --- a/packages/modules/translation/src/services/translation-module.ts +++ b/packages/modules/translation/src/services/translation-module.ts @@ -2,6 +2,7 @@ import { raw } from "@medusajs/framework/mikro-orm/core" import { Context, CreateTranslationDTO, + CreateTranslationSettingsDTO, DAL, FilterableTranslationProps, FindConfig, @@ -9,21 +10,25 @@ import { LocaleDTO, ModulesSdkTypes, TranslationTypes, + UpdateTranslationSettingsDTO, } from "@medusajs/framework/types" import { SqlEntityManager } from "@medusajs/framework/mikro-orm/postgresql" import { + arrayDifference, + DmlEntity, EmitEvents, InjectManager, MedusaContext, MedusaError, + MedusaErrorTypes, MedusaService, normalizeLocale, + toSnakeCase, } from "@medusajs/framework/utils" import Locale from "@models/locale" import Translation from "@models/translation" import Settings from "@models/settings" import { computeTranslatedFieldCount } from "@utils/compute-translated-field-count" -import { TRANSLATABLE_FIELDS_CONFIG_KEY } from "@utils/constants" import { filterTranslationFields } from "@utils/filter-translation-fields" type InjectedDependencies = { @@ -33,7 +38,6 @@ type InjectedDependencies = { translationSettingsService: ModulesSdkTypes.IMedusaInternalService< typeof Settings > - [TRANSLATABLE_FIELDS_CONFIG_KEY]: Record } export default class TranslationModuleService @@ -78,6 +82,55 @@ export default class TranslationModuleService this.settingsService_ = translationSettingsService } + __hooks = { + onApplicationStart: async () => { + return this.onApplicationStart_() + }, + } + + protected async onApplicationStart_() { + const translatableEntities = DmlEntity.getTranslatableEntities() + const translatableEntitiesSet = new Set( + translatableEntities.map((entity) => toSnakeCase(entity.entity)) + ) + + const currentTranslationSettings = await this.settingsService_.list() + const currentTranslationSettingsSet = new Set( + currentTranslationSettings.map((setting) => setting.entity_type) + ) + + const settingsToUpsert: ( + | CreateTranslationSettingsDTO + | UpdateTranslationSettingsDTO + )[] = [] + + for (const setting of currentTranslationSettings) { + if ( + !translatableEntitiesSet.has(setting.entity_type) && + setting.is_active + ) { + settingsToUpsert.push({ + id: setting.id, + is_active: false, + }) + } + } + + for (const entity of translatableEntities) { + const snakeCaseEntityType = toSnakeCase(entity.entity) + const hasCurrentSettings = + currentTranslationSettingsSet.has(snakeCaseEntityType) + if (!hasCurrentSettings) { + settingsToUpsert.push({ + entity_type: snakeCaseEntityType, + fields: entity.fields, + }) + } + } + + await this.settingsService_.upsert(settingsToUpsert) + } + @InjectManager() async getTranslatableFields( entityType?: string, @@ -90,7 +143,8 @@ export default class TranslationModuleService sharedContext ) return settings.reduce((acc, setting) => { - acc[setting.entity_type] = setting.fields as unknown as string[] + acc[toSnakeCase(setting.entity_type)] = + setting.fields as unknown as string[] return acc }, {} as Record) } @@ -377,6 +431,42 @@ export default class TranslationModuleService return Array.isArray(data) ? serialized : serialized[0] } + @InjectManager() + @EmitEvents() + // @ts-expect-error + async createTranslationSettings( + data: CreateTranslationSettingsDTO[] | CreateTranslationSettingsDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise< + | TranslationTypes.TranslationSettingsDTO + | TranslationTypes.TranslationSettingsDTO[] + > { + const dataArray = Array.isArray(data) ? data : [data] + + await this.validateSettings_(dataArray, sharedContext) + + // @ts-expect-error TS can't match union type to overloads + return await super.createTranslationSettings(data, sharedContext) + } + + @InjectManager() + @EmitEvents() + // @ts-expect-error + async updateTranslationSettings( + data: UpdateTranslationSettingsDTO | UpdateTranslationSettingsDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise< + | TranslationTypes.TranslationSettingsDTO[] + | TranslationTypes.TranslationSettingsDTO + > { + const dataArray = Array.isArray(data) ? data : [data] + + await this.validateSettings_(dataArray, sharedContext) + + // @ts-expect-error TS can't match union type to overloads + return await super.updateTranslationSettings(data, sharedContext) + } + @InjectManager() async getStatistics( input: TranslationTypes.TranslationStatisticsInput, @@ -492,4 +582,79 @@ export default class TranslationModuleService return result } + + /** + * Validates the translation settings to create or update against the translatable entities and their translatable fields configuration. + * @param dataToValidate - The data to validate. + * @param sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + */ + @InjectManager() + protected async validateSettings_( + dataToValidate: ( + | CreateTranslationSettingsDTO + | UpdateTranslationSettingsDTO + )[], + @MedusaContext() sharedContext: Context = {} + ) { + const translatableEntities = DmlEntity.getTranslatableEntities() + const translatableEntitiesMap = new Map( + translatableEntities.map((entity) => [toSnakeCase(entity.entity), entity]) + ) + + const invalidSettings: { + entity_type: string + is_invalid_entity: boolean + invalidFields?: string[] + }[] = [] + + for (const item of dataToValidate) { + let itemEntityType = item.entity_type + if (!itemEntityType) { + const translationSetting = await this.retrieveTranslationSettings( + //@ts-expect-error - if no entity_type, we are on an update + item.id, + { select: ["entity_type"] }, + sharedContext + ) + itemEntityType = translationSetting.entity_type + } + + const entity = translatableEntitiesMap.get(itemEntityType) + + if (!entity) { + invalidSettings.push({ + entity_type: itemEntityType, + is_invalid_entity: true, + }) + } else { + const invalidFields = arrayDifference(item.fields ?? [], entity.fields) + if (invalidFields.length) { + invalidSettings.push({ + entity_type: itemEntityType, + is_invalid_entity: false, + invalidFields, + }) + } + } + } + + if (invalidSettings.length) { + throw new MedusaError( + MedusaErrorTypes.INVALID_DATA, + "Invalid translation settings:\n" + + invalidSettings + .map( + (setting) => + `- ${setting.entity_type} ${ + setting.is_invalid_entity + ? "is not a translatable entity" + : `doesn't have the following fields set as translatable: ${setting.invalidFields?.join( + ", " + )}` + }` + ) + .join("\n") + ) + } + } } diff --git a/packages/modules/translation/src/utils/constants.ts b/packages/modules/translation/src/utils/constants.ts deleted file mode 100644 index 3d987a9703..0000000000 --- a/packages/modules/translation/src/utils/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const TRANSLATABLE_FIELDS_CONFIG_KEY = "translatableFieldsConfig" diff --git a/packages/modules/translation/src/utils/translatable-fields.ts b/packages/modules/translation/src/utils/translatable-fields.ts deleted file mode 100644 index 45483191ff..0000000000 --- a/packages/modules/translation/src/utils/translatable-fields.ts +++ /dev/null @@ -1,41 +0,0 @@ -export const PRODUCT_TRANSLATABLE_FIELDS = [ - "title", - "description", - "material", - "subtitle", -] -export const PRODUCT_VARIANT_TRANSLATABLE_FIELDS = ["title", "material"] -export const PRODUCT_TYPE_TRANSLATABLE_FIELDS = ["value"] -export const PRODUCT_COLLECTION_TRANSLATABLE_FIELDS = ["title"] -export const PRODUCT_CATEGORY_TRANSLATABLE_FIELDS = ["name", "description"] -export const PRODUCT_TAG_TRANSLATABLE_FIELDS = ["value"] -export const PRODUCT_OPTION_TRANSLATABLE_FIELDS = ["title"] -export const PRODUCT_OPTION_VALUE_TRANSLATABLE_FIELDS = ["value"] -export const REGION_TRANSLATABLE_FIELDS = ["name"] -export const CUSTOMER_GROUP_TRANSLATABLE_FIELDS = ["name"] -export const SHIPPING_OPTION_TRANSLATABLE_FIELDS = ["name"] -export const SHIPPING_OPTION_TYPE_TRANSLATABLE_FIELDS = ["label", "description"] -export const TAX_RATE_TRANSLATABLE_FIELDS = ["name"] - -// export const RETURN_REASON_TRANSLATABLE_FIELDS = [ -// "value", -// "label", -// "description", -// ] - -export const translatableFieldsConfig = { - product: PRODUCT_TRANSLATABLE_FIELDS, - product_variant: PRODUCT_VARIANT_TRANSLATABLE_FIELDS, - product_type: PRODUCT_TYPE_TRANSLATABLE_FIELDS, - product_collection: PRODUCT_COLLECTION_TRANSLATABLE_FIELDS, - product_category: PRODUCT_CATEGORY_TRANSLATABLE_FIELDS, - product_tag: PRODUCT_TAG_TRANSLATABLE_FIELDS, - product_option: PRODUCT_OPTION_TRANSLATABLE_FIELDS, - product_option_value: PRODUCT_OPTION_VALUE_TRANSLATABLE_FIELDS, - region: REGION_TRANSLATABLE_FIELDS, - customer_group: CUSTOMER_GROUP_TRANSLATABLE_FIELDS, - shipping_option: SHIPPING_OPTION_TRANSLATABLE_FIELDS, - shipping_option_type: SHIPPING_OPTION_TYPE_TRANSLATABLE_FIELDS, - tax_rate: TAX_RATE_TRANSLATABLE_FIELDS, - // return_reason: RETURN_REASON_TRANSLATABLE_FIELDS, -}