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:
Adrien de Peretti
2025-12-08 19:33:08 +01:00
committed by GitHub
co-authored by Nicolas Gorga Oli Juhl
parent fea3d4ec49
commit 6dc0b8bed8
130 changed files with 5649 additions and 112 deletions
@@ -0,0 +1,193 @@
import { raw } from "@medusajs/framework/mikro-orm/core"
import {
Context,
CreateTranslationDTO,
DAL,
FilterableTranslationProps,
FindConfig,
ITranslationModuleService,
LocaleDTO,
ModulesSdkTypes,
TranslationTypes,
} from "@medusajs/framework/types"
import {
EmitEvents,
InjectManager,
MedusaContext,
MedusaService,
normalizeLocale,
} from "@medusajs/framework/utils"
import Locale from "@models/locale"
import Translation from "@models/translation"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
translationService: ModulesSdkTypes.IMedusaInternalService<typeof Translation>
localeService: ModulesSdkTypes.IMedusaInternalService<typeof Locale>
}
export default class TranslationModuleService
extends MedusaService<{
Locale: {
dto: TranslationTypes.LocaleDTO
}
Translation: {
dto: TranslationTypes.TranslationDTO
}
}>({
Locale,
Translation,
})
implements ITranslationModuleService
{
protected baseRepository_: DAL.RepositoryService
protected translationService_: ModulesSdkTypes.IMedusaInternalService<
typeof Translation
>
protected localeService_: ModulesSdkTypes.IMedusaInternalService<
typeof Locale
>
constructor({
baseRepository,
translationService,
localeService,
}: InjectedDependencies) {
super(...arguments)
this.baseRepository_ = baseRepository
this.translationService_ = translationService
this.localeService_ = localeService
}
static prepareFilters(
filters: FilterableTranslationProps
): FilterableTranslationProps {
let { q, ...restFilters } = filters
if (q) {
restFilters = {
...restFilters,
[raw(`translations::text ILIKE ?`, [`%${q}%`])]: [],
}
}
return restFilters
}
@InjectManager()
// @ts-expect-error
async listTranslations(
filters: FilterableTranslationProps = {},
config: FindConfig<TranslationTypes.TranslationDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TranslationTypes.TranslationDTO[]> {
const preparedFilters = TranslationModuleService.prepareFilters(filters)
const results = await this.translationService_.list(
preparedFilters,
config,
sharedContext
)
return await this.baseRepository_.serialize<
TranslationTypes.TranslationDTO[]
>(results)
}
@InjectManager()
// @ts-expect-error
async listAndCountTranslations(
filters: FilterableTranslationProps = {},
config: FindConfig<TranslationTypes.TranslationDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TranslationTypes.TranslationDTO[], number]> {
const preparedFilters = TranslationModuleService.prepareFilters(filters)
const [results, count] = await this.translationService_.listAndCount(
preparedFilters,
config,
sharedContext
)
return [
await this.baseRepository_.serialize<TranslationTypes.TranslationDTO[]>(
results
),
count,
]
}
// @ts-expect-error
createLocales(
data: TranslationTypes.CreateLocaleDTO[],
sharedContext?: Context
): Promise<TranslationTypes.LocaleDTO[]>
// @ts-expect-error
createLocales(
data: TranslationTypes.CreateLocaleDTO,
sharedContext?: Context
): Promise<TranslationTypes.LocaleDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createLocales(
data: TranslationTypes.CreateLocaleDTO | TranslationTypes.CreateLocaleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TranslationTypes.LocaleDTO | TranslationTypes.LocaleDTO[]> {
const dataArray = Array.isArray(data) ? data : [data]
const normalizedData = dataArray.map((locale) => ({
...locale,
code: normalizeLocale(locale.code),
}))
const createdLocales = await this.localeService_.create(
normalizedData,
sharedContext
)
const serialized = await this.baseRepository_.serialize<LocaleDTO[]>(
createdLocales
)
return Array.isArray(data) ? serialized : serialized[0]
}
// @ts-expect-error
createTranslations(
data: CreateTranslationDTO,
sharedContext?: Context
): Promise<TranslationTypes.TranslationDTO>
// @ts-expect-error
createTranslations(
data: CreateTranslationDTO[],
sharedContext?: Context
): Promise<TranslationTypes.TranslationDTO[]>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createTranslations(
data: CreateTranslationDTO | CreateTranslationDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<
TranslationTypes.TranslationDTO | TranslationTypes.TranslationDTO[]
> {
const dataArray = Array.isArray(data) ? data : [data]
const normalizedData = dataArray.map((translation) => ({
...translation,
locale_code: normalizeLocale(translation.locale_code),
}))
const createdTranslations = await this.translationService_.create(
normalizedData,
sharedContext
)
const serialized = await this.baseRepository_.serialize<
TranslationTypes.TranslationDTO[]
>(createdTranslations)
return Array.isArray(data) ? serialized : serialized[0]
}
}