feat(): Translation settings + user configuration + admin hook and js sdk + dashboard (#14355)

* feat(): Translation settings + user configuration

* feat(): Translation settings + user configuration

* Create gentle-bees-grow.md

* add entities end point

* add entities end point

* add admin hook and js sdk method

* update changeset

* fix tests

* fix tests

* rm unnecessary copy

* update dashboard to use the new resources

* update dashboard to use the new resources

* update dashboard to use the new resources

* allow type inference through interface augmentation in the defineConfig of medusa-config

* allow type inference through interface augmentation in the defineConfig of medusa-config

* exclude id and _id props

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-12-19 15:29:48 +01:00
committed by GitHub
co-authored by Oli Juhl
parent 797878af26
commit b21a599d11
26 changed files with 1041 additions and 260 deletions
@@ -21,13 +21,18 @@ import {
} 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 = {
baseRepository: DAL.RepositoryService
translationService: ModulesSdkTypes.IMedusaInternalService<typeof Translation>
localeService: ModulesSdkTypes.IMedusaInternalService<typeof Locale>
translationSettingsService: ModulesSdkTypes.IMedusaInternalService<
typeof Settings
>
[TRANSLATABLE_FIELDS_CONFIG_KEY]: Record<string, string[]>
}
@@ -39,9 +44,13 @@ export default class TranslationModuleService
Translation: {
dto: TranslationTypes.TranslationDTO
}
TranslationSettings: {
dto: TranslationTypes.TranslationSettingsDTO
}
}>({
Locale,
Translation,
TranslationSettings: Settings,
})
implements ITranslationModuleService
{
@@ -52,20 +61,38 @@ export default class TranslationModuleService
protected localeService_: ModulesSdkTypes.IMedusaInternalService<
typeof Locale
>
private readonly translatableFieldsConfig_: Record<string, string[]>
protected settingsService_: ModulesSdkTypes.IMedusaInternalService<
typeof Settings
>
constructor({
baseRepository,
translationService,
localeService,
translatableFieldsConfig,
translationSettingsService,
}: InjectedDependencies) {
super(...arguments)
this.baseRepository_ = baseRepository
this.translationService_ = translationService
this.localeService_ = localeService
this.translatableFieldsConfig_ = translatableFieldsConfig
this.settingsService_ = translationSettingsService
}
@InjectManager()
async getTranslatableFields(
entityType?: string,
@MedusaContext() sharedContext: Context = {}
): Promise<Record<string, string[]>> {
const filters = entityType ? { entity_type: entityType } : {}
const settings = await this.settingsService_.list(
filters,
{},
sharedContext
)
return settings.reduce((acc, setting) => {
acc[setting.entity_type] = setting.fields as unknown as string[]
return acc
}, {} as Record<string, string[]>)
}
static prepareFilters(
@@ -83,6 +110,54 @@ export default class TranslationModuleService
return restFilters
}
@InjectManager()
// @ts-expect-error
async retrieveTranslation(
id: string,
config: FindConfig<TranslationTypes.TranslationDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TranslationTypes.TranslationDTO> {
const configWithReference =
TranslationModuleService.ensureReferenceFieldInConfig(config)
const result = await this.translationService_.retrieve(
id,
configWithReference,
sharedContext
)
const serialized =
await this.baseRepository_.serialize<TranslationTypes.TranslationDTO>(
result
)
const translatableFieldsConfig = await this.getTranslatableFields(
undefined,
sharedContext
)
return filterTranslationFields([serialized], translatableFieldsConfig)[0]
}
/**
* Ensures the 'reference' field is included in the select config.
* This is needed for filtering translations by translatable fields.
*/
static ensureReferenceFieldInConfig(
config: FindConfig<TranslationTypes.TranslationDTO>
): FindConfig<TranslationTypes.TranslationDTO> {
if (!config?.select?.length) {
return config
}
const select = config.select as string[]
if (!select.includes("reference")) {
return { ...config, select: [...select, "reference"] }
}
return config
}
@InjectManager()
// @ts-expect-error
async listTranslations(
@@ -91,16 +166,25 @@ export default class TranslationModuleService
@MedusaContext() sharedContext: Context = {}
): Promise<TranslationTypes.TranslationDTO[]> {
const preparedFilters = TranslationModuleService.prepareFilters(filters)
const configWithReference =
TranslationModuleService.ensureReferenceFieldInConfig(config)
const results = await this.translationService_.list(
preparedFilters,
config,
configWithReference,
sharedContext
)
return await this.baseRepository_.serialize<
const serialized = await this.baseRepository_.serialize<
TranslationTypes.TranslationDTO[]
>(results)
const translatableFieldsConfig = await this.getTranslatableFields(
undefined,
sharedContext
)
return filterTranslationFields(serialized, translatableFieldsConfig)
}
@InjectManager()
@@ -111,17 +195,26 @@ export default class TranslationModuleService
@MedusaContext() sharedContext: Context = {}
): Promise<[TranslationTypes.TranslationDTO[], number]> {
const preparedFilters = TranslationModuleService.prepareFilters(filters)
const configWithReference =
TranslationModuleService.ensureReferenceFieldInConfig(config)
const [results, count] = await this.translationService_.listAndCount(
preparedFilters,
config,
configWithReference,
sharedContext
)
const serialized = await this.baseRepository_.serialize<
TranslationTypes.TranslationDTO[]
>(results)
const translatableFieldsConfig = await this.getTranslatableFields(
undefined,
sharedContext
)
return [
await this.baseRepository_.serialize<TranslationTypes.TranslationDTO[]>(
results
),
filterTranslationFields(serialized, translatableFieldsConfig),
count,
]
}
@@ -183,12 +276,16 @@ export default class TranslationModuleService
TranslationTypes.TranslationDTO | TranslationTypes.TranslationDTO[]
> {
const dataArray = Array.isArray(data) ? data : [data]
const translatableFieldsConfig = await this.getTranslatableFields(
undefined,
sharedContext
)
const normalizedData = dataArray.map((translation) => ({
...translation,
locale_code: normalizeLocale(translation.locale_code),
translated_field_count: computeTranslatedFieldCount(
translation.translations as Record<string, unknown>,
this.translatableFieldsConfig_[translation.reference]
translatableFieldsConfig[translation.reference]
),
}))
@@ -248,6 +345,11 @@ export default class TranslationModuleService
)
}
const translatableFieldsConfig = await this.getTranslatableFields(
undefined,
sharedContext
)
for (const update of dataArray) {
if (update.translations) {
const reference = update.reference || referenceMap[update.id]
@@ -257,7 +359,7 @@ export default class TranslationModuleService
}
).translated_field_count = computeTranslatedFieldCount(
update.translations as Record<string, unknown>,
this.translatableFieldsConfig_[reference] || []
translatableFieldsConfig[reference] || []
)
}
}
@@ -275,13 +377,6 @@ export default class TranslationModuleService
return Array.isArray(data) ? serialized : serialized[0]
}
getTranslatableFields(entityType?: string): Record<string, string[]> {
if (entityType) {
return { [entityType]: this.translatableFieldsConfig_[entityType] }
}
return this.translatableFieldsConfig_
}
@InjectManager()
async getStatistics(
input: TranslationTypes.TranslationStatisticsInput,
@@ -309,11 +404,16 @@ export default class TranslationModuleService
sharedContext.manager) as SqlEntityManager
const knex = manager.getKnex()
const translatableFieldsConfig = await this.getTranslatableFields(
undefined,
sharedContext
)
const result: TranslationTypes.TranslationStatisticsOutput = {}
const entityTypes: string[] = []
for (const entityType of Object.keys(entities)) {
const translatableFields = this.translatableFieldsConfig_[entityType]
const translatableFields = translatableFieldsConfig[entityType]
if (!translatableFields || translatableFields.length === 0) {
result[entityType] = {
@@ -352,7 +452,7 @@ export default class TranslationModuleService
)
for (const entityType of entityTypes) {
const translatableFields = this.translatableFieldsConfig_[entityType]
const translatableFields = translatableFieldsConfig[entityType]
const fieldsPerEntity = translatableFields.length
const entityCount = entities[entityType].count
const expectedPerLocale = entityCount * fieldsPerEntity