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:
@@ -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)
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
@@ -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 })
|
||||
}
|
||||
)
|
||||
@@ -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<CreateTranslationsWorkflowInput>
|
||||
): WorkflowResponse<TranslationDTO[]> => {
|
||||
validateTranslationsStep(input.translations)
|
||||
const translations = createTranslationsStep(input.translations)
|
||||
|
||||
const translationIdEvents = transform(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<UpdateTranslationsWorkflowInput>
|
||||
): WorkflowResponse<TranslationDTO[]> => {
|
||||
const validateInput = transform(input, (input) => {
|
||||
return "translations" in input ? input.translations : [input.update]
|
||||
})
|
||||
validateTranslationsStep(validateInput)
|
||||
|
||||
const translations = updateTranslationsStep(input)
|
||||
|
||||
const translationIdEvents = transform(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<string, string[]>
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@@ -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<string>
|
||||
}
|
||||
|
||||
export interface FilterableTranslationSettingsProps
|
||||
extends BaseFilterable<FilterableTranslationSettingsProps> {
|
||||
/**
|
||||
* The IDs to filter the translation settings by.
|
||||
*/
|
||||
id?: string[] | string | OperatorMap<string | string[]>
|
||||
|
||||
/**
|
||||
* Filter translation settings by entity type.
|
||||
*/
|
||||
entity_type?: string | string[] | OperatorMap<string | string[]>
|
||||
/**
|
||||
* Filter translation settings by active status.
|
||||
*/
|
||||
is_active?: boolean | OperatorMap<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<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
|
||||
}>
|
||||
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
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, unknown>
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<void>} 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<Record<string, string[]> | 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<void>} 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<Record<string, string[]> | 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<Record<string, string[]>>
|
||||
|
||||
/**
|
||||
* 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<TranslationSettingsDTO>} 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<TranslationSettingsDTO>
|
||||
|
||||
/**
|
||||
*
|
||||
* @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<TranslationSettingsDTO[]>} 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<TranslationSettingsDTO[]>
|
||||
|
||||
/**
|
||||
* 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<TranslationSettingsDTO>} 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<TranslationSettingsDTO>
|
||||
|
||||
/**
|
||||
* 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<TranslationSettingsDTO[]>} 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<TranslationSettingsDTO[]>
|
||||
|
||||
/**
|
||||
* 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<void>} Resolves when the translation settings are deleted.
|
||||
*
|
||||
* @example
|
||||
* await translationModuleService.deleteTranslationSettings([
|
||||
* "ts_123",
|
||||
* "ts_321",
|
||||
* ])
|
||||
*/
|
||||
deleteTranslationSettings(
|
||||
input: string[],
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
|
||||
/**
|
||||
* 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<TranslationSettingsDTO>} 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[]>} 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<TranslationSettingsDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<TranslationSettingsDTO[]>
|
||||
|
||||
/**
|
||||
* 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<TranslationSettingsDTO>} 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<TranslationSettingsDTO>,
|
||||
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<TranslationSettingsDTO>} 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<TranslationSettingsDTO>} 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<TranslationSettingsDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<TranslationSettingsDTO>
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<AdminBatchTranslationSettingsType>,
|
||||
res: MedusaResponse<HttpTypes.AdminBatchTranslationSettingsResponse>
|
||||
) => {
|
||||
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),
|
||||
})
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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<ITranslationModuleService>({
|
||||
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,
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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<TranslationModuleOptions>): Promise<void> => {
|
||||
const logger =
|
||||
container.resolve<Logger>(ContainerRegistrationKeys.LOGGER) ?? console
|
||||
const settingsService: ModulesSdkTypes.IMedusaInternalService<
|
||||
typeof Settings
|
||||
> = container.resolve("translationSettingsService")
|
||||
|
||||
const mergedConfig: Record<string, string[]> = 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))
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
|
||||
|
||||
export class Migration20260108122757 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table if exists "translation_settings" add column if not exists "is_active" boolean not null default true;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table if exists "translation_settings" drop column if exists "is_active";`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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<string, string[]>
|
||||
}
|
||||
|
||||
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<string, string[]>)
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const TRANSLATABLE_FIELDS_CONFIG_KEY = "translatableFieldsConfig"
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user