feat(): Introduce translation module and preliminary application of them (#14189)
* feat(): Translation first steps * feat(): locale middleware * feat(): readonly links * feat(): feature flag * feat(): modules sdk * feat(): translation module re export * start adding workflows * update typings * update typings * test(): Add integration tests * test(): centralize filters preparation * test(): centralize filters preparation * remove unnecessary importy * fix workflows * Define StoreLocale inside Store Module * Link definition to extend Store with supported_locales * store_locale migration * Add supported_locales handling in Store Module * Tests * Accept supported_locales in Store endpoints * Add locales to js-sdk * Include locale list and default locale in Store Detail section * Initialize local namespace in js-sdk * Add locales route * Make code primary key of locale table to facilitate upserts * Add locales routes * Show locale code as is * Add list translations api route * Batch endpoint * Types * New batchTranslationsWorkflow and various updates to existent ones * Edit default locale UI * WIP * Apply translation agnostically * middleware * Apply translation agnostically * fix Apply translation agnostically * apply translations to product list * Add feature flag * fetch translations by batches of 250 max * fix apply * improve and test util * apply to product list * dont manage translations if no locale * normalize locale * potential todo * Protect translations routes with feature flag * Extract normalize locale util to core/utils * Normalize locale on write * Normalize locale for read * Use feature flag to guard translations UI across the board * Avoid throwing incorrectly when locale_code not present in partial updates * move applyTranslations util * remove old tests * fix util tests * fix(): product end points * cleanup * update lock * remove unused var * cleanup * fix apply locale * missing new dep for test utils * Change entity_type, entity_id to reference, reference_id * Remove comment * Avoid registering translations route if ff not enabled * Prevent registering express handler for disabled route via defineFileConfig * Add tests * Add changeset * Update test * fix integration tests, module and internals * Add locale id plus fixed * Allow to pass array of reference_id * fix unit tests * fix link loading * fix store route * fix sales channel test * fix tests --------- Co-authored-by: Nicolas Gorga <nicogorga11@gmail.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
fea3d4ec49
commit
6dc0b8bed8
@@ -31,4 +31,5 @@ export * from "./shipping-profile"
|
||||
export * from "./stock-location"
|
||||
export * from "./store"
|
||||
export * from "./tax"
|
||||
export * from "./translation"
|
||||
export * from "./user"
|
||||
|
||||
2
packages/core/core-flows/src/translation/index.ts
Normal file
2
packages/core/core-flows/src/translation/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./steps"
|
||||
export * from "./workflows"
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
CreateTranslationDTO,
|
||||
ITranslationModuleService,
|
||||
} from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
export const createTranslationsStepId = "create-translations"
|
||||
/**
|
||||
* This step creates one or more translations.
|
||||
*
|
||||
* @example
|
||||
* const data = createTranslationsStep([
|
||||
* {
|
||||
* reference_id: "prod_123",
|
||||
* reference: "product",
|
||||
* locale_code: "fr-FR",
|
||||
* translations: { title: "Produit", description: "Description du produit" }
|
||||
* }
|
||||
* ])
|
||||
*/
|
||||
export const createTranslationsStep = createStep(
|
||||
createTranslationsStepId,
|
||||
async (data: CreateTranslationDTO[], { container }) => {
|
||||
const service = container.resolve<ITranslationModuleService>(
|
||||
Modules.TRANSLATION
|
||||
)
|
||||
|
||||
const created = await service.createTranslations(data)
|
||||
|
||||
return new StepResponse(
|
||||
created,
|
||||
created.map((translation) => translation.id)
|
||||
)
|
||||
},
|
||||
async (createdIds, { container }) => {
|
||||
if (!createdIds?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const service = container.resolve<ITranslationModuleService>(
|
||||
Modules.TRANSLATION
|
||||
)
|
||||
|
||||
await service.deleteTranslations(createdIds)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ITranslationModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
/**
|
||||
* The IDs of the translations to delete.
|
||||
*/
|
||||
export type DeleteTranslationsStepInput = string[]
|
||||
|
||||
export const deleteTranslationsStepId = "delete-translations"
|
||||
/**
|
||||
* This step deletes one or more translations.
|
||||
*/
|
||||
export const deleteTranslationsStep = createStep(
|
||||
deleteTranslationsStepId,
|
||||
async (ids: DeleteTranslationsStepInput, { container }) => {
|
||||
const service = container.resolve<ITranslationModuleService>(
|
||||
Modules.TRANSLATION
|
||||
)
|
||||
|
||||
await service.softDeleteTranslations(ids)
|
||||
|
||||
return new StepResponse(void 0, ids)
|
||||
},
|
||||
async (prevIds, { container }) => {
|
||||
if (!prevIds?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const service = container.resolve<ITranslationModuleService>(
|
||||
Modules.TRANSLATION
|
||||
)
|
||||
|
||||
await service.restoreTranslations(prevIds)
|
||||
}
|
||||
)
|
||||
4
packages/core/core-flows/src/translation/steps/index.ts
Normal file
4
packages/core/core-flows/src/translation/steps/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./create-translations"
|
||||
export * from "./delete-translations"
|
||||
export * from "./update-translations"
|
||||
export * from "./validate-translations"
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
FilterableTranslationProps,
|
||||
ITranslationModuleService,
|
||||
UpdateTranslationDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
MedusaError,
|
||||
MedusaErrorTypes,
|
||||
Modules,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
/**
|
||||
* The data to update translations.
|
||||
*/
|
||||
export type UpdateTranslationsStepInput =
|
||||
| {
|
||||
/**
|
||||
* The filters to select the translations to update.
|
||||
*/
|
||||
selector: FilterableTranslationProps
|
||||
/**
|
||||
* The data to update in the translations.
|
||||
*/
|
||||
update: UpdateTranslationDTO
|
||||
}
|
||||
| {
|
||||
translations: UpdateTranslationDTO[]
|
||||
}
|
||||
|
||||
export const updateTranslationsStepId = "update-translations"
|
||||
/**
|
||||
* This step updates translations matching the specified filters.
|
||||
*
|
||||
* @example
|
||||
* const data = updateTranslationsStep({
|
||||
* selector: {
|
||||
* reference_id: "prod_123",
|
||||
* locale_code: "fr-FR"
|
||||
* },
|
||||
* update: {
|
||||
* translations: { title: "Nouveau titre" }
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
export const updateTranslationsStep = createStep(
|
||||
updateTranslationsStepId,
|
||||
async (data: UpdateTranslationsStepInput, { container }) => {
|
||||
const service = container.resolve<ITranslationModuleService>(
|
||||
Modules.TRANSLATION
|
||||
)
|
||||
|
||||
if ("translations" in data) {
|
||||
if (data.translations.some((t) => !t.id)) {
|
||||
throw new MedusaError(
|
||||
MedusaErrorTypes.INVALID_DATA,
|
||||
"Translation ID is required when doing a batch update of translations"
|
||||
)
|
||||
}
|
||||
|
||||
if (!data.translations.length) {
|
||||
return new StepResponse([], [])
|
||||
}
|
||||
|
||||
const prevData = await service.listTranslations({
|
||||
id: data.translations.map((t) => t.id) as string[],
|
||||
})
|
||||
|
||||
const translations = await service.updateTranslations(data.translations)
|
||||
return new StepResponse(translations, prevData)
|
||||
}
|
||||
|
||||
const prevData = await service.listTranslations(data.selector, {
|
||||
select: [
|
||||
"id",
|
||||
"reference_id",
|
||||
"reference",
|
||||
"locale_code",
|
||||
"translations",
|
||||
],
|
||||
})
|
||||
|
||||
if (Object.keys(data.update).length === 0) {
|
||||
return new StepResponse(prevData, [])
|
||||
}
|
||||
|
||||
const translations = await service.updateTranslations({
|
||||
selector: data.selector,
|
||||
data: data.update,
|
||||
})
|
||||
|
||||
return new StepResponse(translations, prevData)
|
||||
},
|
||||
async (prevData, { container }) => {
|
||||
if (!prevData?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const service = container.resolve<ITranslationModuleService>(
|
||||
Modules.TRANSLATION
|
||||
)
|
||||
|
||||
await service.updateTranslations(
|
||||
prevData.map((t) => ({
|
||||
id: t.id,
|
||||
reference_id: t.reference_id,
|
||||
reference: t.reference,
|
||||
locale_code: t.locale_code,
|
||||
translations: t.translations,
|
||||
}))
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
MedusaErrorTypes,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
import { CreateTranslationDTO, UpdateTranslationDTO } from "@medusajs/types"
|
||||
|
||||
export const validateTranslationsStepId = "validate-translations"
|
||||
|
||||
export type ValidateTranslationsStepInput =
|
||||
| CreateTranslationDTO[]
|
||||
| CreateTranslationDTO
|
||||
| UpdateTranslationDTO[]
|
||||
| UpdateTranslationDTO
|
||||
|
||||
// TODO: Do we want to validate anything else here?
|
||||
export const validateTranslationsStep = createStep(
|
||||
validateTranslationsStepId,
|
||||
async (data: ValidateTranslationsStepInput, { container }) => {
|
||||
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
||||
const {
|
||||
data: [store],
|
||||
} = await query.graph(
|
||||
{
|
||||
entity: "store",
|
||||
fields: ["supported_locales.*"],
|
||||
pagination: {
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
cache: { enable: true },
|
||||
}
|
||||
)
|
||||
|
||||
const enabledLocales = (store.supported_locales ?? []).map(
|
||||
(locale) => locale.locale_code
|
||||
)
|
||||
const normalizedInput = Array.isArray(data) ? data : [data]
|
||||
|
||||
const unsupportedLocales = normalizedInput
|
||||
.filter((translation) => Boolean(translation.locale_code))
|
||||
.map((translation) => translation.locale_code)
|
||||
.filter((locale) => !enabledLocales.includes(locale ?? ""))
|
||||
|
||||
if (unsupportedLocales.length) {
|
||||
throw new MedusaError(
|
||||
MedusaErrorTypes.INVALID_DATA,
|
||||
`The following locales are not supported in the store: ${unsupportedLocales.join(
|
||||
", "
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return new StepResponse(void 0)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { CreateTranslationDTO, UpdateTranslationDTO } from "@medusajs/types"
|
||||
import { createTranslationsWorkflow } from "./create-translations"
|
||||
import { deleteTranslationsWorkflow } from "./delete-translations"
|
||||
import { updateTranslationsWorkflow } from "./update-translations"
|
||||
|
||||
export const batchTranslationsWorkflowId = "batch-translations"
|
||||
|
||||
export type BatchTranslationsWorkflowInput = {
|
||||
create: CreateTranslationDTO[]
|
||||
update: UpdateTranslationDTO[]
|
||||
delete: string[]
|
||||
}
|
||||
export const batchTranslationsWorkflow = createWorkflow(
|
||||
batchTranslationsWorkflowId,
|
||||
(input: BatchTranslationsWorkflowInput) => {
|
||||
const [created, updated, deleted] = parallelize(
|
||||
createTranslationsWorkflow.runAsStep({
|
||||
input: {
|
||||
translations: input.create,
|
||||
},
|
||||
}),
|
||||
updateTranslationsWorkflow.runAsStep({
|
||||
input: {
|
||||
translations: input.update,
|
||||
},
|
||||
}),
|
||||
deleteTranslationsWorkflow.runAsStep({
|
||||
input: {
|
||||
ids: input.delete,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
return new WorkflowResponse(
|
||||
transform({ created, updated, deleted }, (result) => result)
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
import { CreateTranslationDTO, TranslationDTO } from "@medusajs/framework/types"
|
||||
import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { emitEventStep } from "../../common/steps/emit-event"
|
||||
import { createTranslationsStep } from "../steps"
|
||||
import { validateTranslationsStep } from "../steps"
|
||||
|
||||
export type CreateTranslationsWorkflowInput = {
|
||||
translations: CreateTranslationDTO[]
|
||||
}
|
||||
|
||||
export const createTranslationsWorkflowId = "create-translations"
|
||||
/**
|
||||
* This workflow creates one or more translations.
|
||||
*
|
||||
* You can use this workflow within your own customizations or custom workflows, allowing you
|
||||
* to create translations in your custom flows.
|
||||
*
|
||||
* @example
|
||||
* const { result } = await createTranslationsWorkflow(container)
|
||||
* .run({
|
||||
* input: {
|
||||
* translations: [
|
||||
* {
|
||||
* reference_id: "prod_123",
|
||||
* reference: "product",
|
||||
* locale_code: "fr-FR",
|
||||
* translations: { title: "Produit", description: "Description du produit" }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
* Create one or more translations.
|
||||
*/
|
||||
export const createTranslationsWorkflow = createWorkflow(
|
||||
createTranslationsWorkflowId,
|
||||
(
|
||||
input: WorkflowData<CreateTranslationsWorkflowInput>
|
||||
): WorkflowResponse<TranslationDTO[]> => {
|
||||
validateTranslationsStep(input.translations)
|
||||
const translations = createTranslationsStep(input.translations)
|
||||
|
||||
const translationIdEvents = transform(
|
||||
{ translations },
|
||||
({ translations }) => {
|
||||
return translations.map((t) => {
|
||||
return { id: t.id }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
emitEventStep({
|
||||
eventName: "translation.created",
|
||||
data: translationIdEvents,
|
||||
})
|
||||
|
||||
return new WorkflowResponse(translations)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { emitEventStep } from "../../common/steps/emit-event"
|
||||
import { deleteTranslationsStep } from "../steps"
|
||||
|
||||
export type DeleteTranslationsWorkflowInput = { ids: string[] }
|
||||
|
||||
export const deleteTranslationsWorkflowId = "delete-translations"
|
||||
/**
|
||||
* This workflow deletes one or more translations.
|
||||
*
|
||||
* You can use this workflow within your own customizations or custom workflows, allowing you
|
||||
* to delete translations in your custom flows.
|
||||
*
|
||||
* @example
|
||||
* const { result } = await deleteTranslationsWorkflow(container)
|
||||
* .run({
|
||||
* input: {
|
||||
* ids: ["trans_123"]
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
* Delete one or more translations.
|
||||
*/
|
||||
export const deleteTranslationsWorkflow = createWorkflow(
|
||||
deleteTranslationsWorkflowId,
|
||||
(
|
||||
input: WorkflowData<DeleteTranslationsWorkflowInput>
|
||||
): WorkflowData<void> => {
|
||||
deleteTranslationsStep(input.ids)
|
||||
|
||||
const translationIdEvents = transform({ input }, ({ input }) => {
|
||||
return input.ids?.map((id) => {
|
||||
return { id }
|
||||
})
|
||||
})
|
||||
|
||||
emitEventStep({
|
||||
eventName: "translation.deleted",
|
||||
data: translationIdEvents,
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./create-translations"
|
||||
export * from "./delete-translations"
|
||||
export * from "./update-translations"
|
||||
export * from "./batch-translations"
|
||||
@@ -0,0 +1,67 @@
|
||||
import { TranslationDTO } from "@medusajs/framework/types"
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { emitEventStep } from "../../common/steps/emit-event"
|
||||
import { updateTranslationsStep, UpdateTranslationsStepInput } from "../steps"
|
||||
import { validateTranslationsStep } from "../steps"
|
||||
|
||||
export type UpdateTranslationsWorkflowInput = UpdateTranslationsStepInput
|
||||
|
||||
export const updateTranslationsWorkflowId = "update-translations"
|
||||
/**
|
||||
* This workflow updates translations matching the specified filters.
|
||||
*
|
||||
* You can use this workflow within your own customizations or custom workflows, allowing you
|
||||
* to update translations in your custom flows.
|
||||
*
|
||||
* @example
|
||||
* const { result } = await updateTranslationsWorkflow(container)
|
||||
* .run({
|
||||
* input: {
|
||||
* selector: {
|
||||
* reference_id: "prod_123",
|
||||
* locale_code: "fr-FR"
|
||||
* },
|
||||
* update: {
|
||||
* translations: { title: "Nouveau titre" }
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
* Update translations.
|
||||
*/
|
||||
export const updateTranslationsWorkflow = createWorkflow(
|
||||
updateTranslationsWorkflowId,
|
||||
(
|
||||
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(
|
||||
{ translations },
|
||||
({ translations }) => {
|
||||
return translations?.map((t) => {
|
||||
return { id: t.id }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
emitEventStep({
|
||||
eventName: "translation.updated",
|
||||
data: translationIdEvents,
|
||||
})
|
||||
|
||||
return new WorkflowResponse(translations)
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user