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:
co-authored by
Oli Juhl
parent
797878af26
commit
b21a599d11
+88
-3
@@ -15,7 +15,11 @@ moduleIntegrationTestRunner<ITranslationModuleService>({
|
||||
service: TranslationModuleService,
|
||||
}).linkable
|
||||
|
||||
expect(Object.keys(linkable)).toEqual(["locale", "translation"])
|
||||
expect(Object.keys(linkable)).toEqual([
|
||||
"locale",
|
||||
"translation",
|
||||
"translationSettings",
|
||||
])
|
||||
|
||||
Object.keys(linkable).forEach((key) => {
|
||||
delete linkable[key].toJSON
|
||||
@@ -40,6 +44,15 @@ moduleIntegrationTestRunner<ITranslationModuleService>({
|
||||
field: "translation",
|
||||
},
|
||||
},
|
||||
translationSettings: {
|
||||
id: {
|
||||
linkable: "translation_settings_id",
|
||||
entity: "TranslationSettings",
|
||||
primaryKey: "id",
|
||||
serviceName: "translation",
|
||||
field: "translationSettings",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -647,11 +660,83 @@ moduleIntegrationTestRunner<ITranslationModuleService>({
|
||||
})
|
||||
})
|
||||
|
||||
describe("Settings", () => {
|
||||
describe("getTranslatableFields", () => {
|
||||
it("should return all translatable fields from database", async () => {
|
||||
const fields = await service.getTranslatableFields()
|
||||
|
||||
expect(fields).toHaveProperty("product")
|
||||
expect(fields).toHaveProperty("product_variant")
|
||||
expect(fields.product).toEqual(
|
||||
expect.arrayContaining(["title", "description"])
|
||||
)
|
||||
})
|
||||
|
||||
it("should return translatable fields for a specific entity type", async () => {
|
||||
const fields = await service.getTranslatableFields("product")
|
||||
|
||||
expect(Object.keys(fields)).toEqual(["product"])
|
||||
expect(fields.product).toEqual(
|
||||
expect.arrayContaining(["title", "description"])
|
||||
)
|
||||
})
|
||||
|
||||
it("should return empty object for unknown entity type", async () => {
|
||||
const fields = await service.getTranslatableFields("unknown_entity")
|
||||
|
||||
expect(fields).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("listing translations filters by configured fields", () => {
|
||||
it("should only return configured fields in translations", async () => {
|
||||
await service.createTranslations({
|
||||
reference_id: "prod_filter_1",
|
||||
reference: "product",
|
||||
locale_code: "en-US",
|
||||
translations: {
|
||||
title: "Product Title",
|
||||
description: "Product Description",
|
||||
unconfigured_field: "Should be filtered out",
|
||||
},
|
||||
})
|
||||
|
||||
const translations = await service.listTranslations({
|
||||
reference_id: "prod_filter_1",
|
||||
})
|
||||
|
||||
expect(translations).toHaveLength(1)
|
||||
expect(translations[0].translations).toHaveProperty("title")
|
||||
expect(translations[0].translations).toHaveProperty("description")
|
||||
expect(translations[0].translations).not.toHaveProperty(
|
||||
"unconfigured_field"
|
||||
)
|
||||
})
|
||||
|
||||
it("should return empty translations for unconfigured entity types", async () => {
|
||||
await service.createTranslations({
|
||||
reference_id: "unconfigured_1",
|
||||
reference: "unconfigured_entity",
|
||||
locale_code: "en-US",
|
||||
translations: {
|
||||
field1: "Value 1",
|
||||
field2: "Value 2",
|
||||
},
|
||||
})
|
||||
|
||||
const translations = await service.listTranslations({
|
||||
reference_id: "unconfigured_1",
|
||||
})
|
||||
|
||||
expect(translations).toHaveLength(1)
|
||||
expect(translations[0].translations).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Statistics", () => {
|
||||
describe("getStatistics", () => {
|
||||
it("should return statistics for a single entity type and locale", async () => {
|
||||
// Create translations for 2 products with some fields filled
|
||||
// Product has 4 translatable fields: title, description, material, subtitle
|
||||
await service.createTranslations([
|
||||
{
|
||||
reference_id: "prod_stat_1",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { defineMikroOrmCliConfig } from "@medusajs/framework/utils"
|
||||
import Locale from "./src/models/locale"
|
||||
import Translation from "./src/models/translation"
|
||||
import Settings from "./src/models/settings"
|
||||
|
||||
export default defineMikroOrmCliConfig("translation", {
|
||||
entities: [Locale, Translation],
|
||||
entities: [Locale, Translation, Settings],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import TranslationModuleService from "@services/translation-module"
|
||||
import loadDefaults from "./loaders/defaults"
|
||||
import loadConfig from "./loaders/config"
|
||||
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"
|
||||
|
||||
|
||||
@@ -1,11 +1,60 @@
|
||||
import { LoaderOptions } from "@medusajs/framework/types"
|
||||
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 }: LoaderOptions): Promise<void> => {
|
||||
container.register(
|
||||
TRANSLATABLE_FIELDS_CONFIG_KEY,
|
||||
asValue(translatableFieldsConfig)
|
||||
)
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -263,6 +263,104 @@
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"entity_type": {
|
||||
"name": "entity_type",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"fields": {
|
||||
"name": "fields",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "translation_settings",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_translation_settings_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_translation_settings_deleted_at\" ON \"translation_settings\" (\"deleted_at\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_translation_settings_entity_type_unique",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_translation_settings_entity_type_unique\" ON \"translation_settings\" (\"entity_type\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "translation_settings_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
],
|
||||
"nativeEnums": {}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
|
||||
|
||||
export class Migration20251218140235 extends Migration {
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table if exists "translation_settings" drop constraint if exists "translation_settings_entity_type_unique";`
|
||||
)
|
||||
this.addSql(
|
||||
`create table if not exists "translation_settings" ("id" text not null, "entity_type" text not null, "fields" jsonb not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "translation_settings_pkey" primary key ("id"));`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_translation_settings_deleted_at" ON "translation_settings" ("deleted_at") WHERE deleted_at IS NULL;`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_translation_settings_entity_type_unique" ON "translation_settings" ("entity_type") WHERE deleted_at IS NULL;`
|
||||
)
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`drop table if exists "translation_settings" cascade;`)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as Translation } from "./translation"
|
||||
export { default as Locale } from "./locale"
|
||||
export { default as Locale } from "./locale"
|
||||
export { default as Settings } from "./settings"
|
||||
@@ -0,0 +1,26 @@
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const Settings = model
|
||||
.define("translation_settings", {
|
||||
id: model.id({ prefix: "trset" }).primaryKey(),
|
||||
/**
|
||||
* The entity type that these settings apply to (e.g., "product", "product_variant").
|
||||
*/
|
||||
entity_type: model.text().searchable(),
|
||||
/**
|
||||
* The translatable fields for this entity type.
|
||||
* Array of field names that can be translated.
|
||||
*
|
||||
* @example
|
||||
* ["title", "description", "material"]
|
||||
*/
|
||||
fields: model.json(),
|
||||
})
|
||||
.indexes([
|
||||
{
|
||||
on: ["entity_type"],
|
||||
unique: true,
|
||||
},
|
||||
])
|
||||
|
||||
export default Settings
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { RemoteQueryEntryPoints } from "@medusajs/framework/types"
|
||||
|
||||
/**
|
||||
* Extracts only the keys of T where the value is a string (or nullable string), the key
|
||||
* is not __typename or id.
|
||||
* This filters out relations and other non-string fields.
|
||||
*/
|
||||
type StringValuedKeys<T> = {
|
||||
[K in keyof T]: K extends `${string}_id`
|
||||
? never
|
||||
: "__typename" extends K
|
||||
? never
|
||||
: "id" extends keyof K
|
||||
? never
|
||||
: NonNullable<T[K]> extends string
|
||||
? K
|
||||
: never
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* A discriminated union of all possible entity configurations.
|
||||
* When you specify a `type`, TypeScript will narrow `fields` to only
|
||||
* the string-valued keys of that specific entity type.
|
||||
*/
|
||||
export type TranslatableEntityConfig =
|
||||
| {
|
||||
[K in keyof RemoteQueryEntryPoints]: {
|
||||
type: K
|
||||
fields: StringValuedKeys<RemoteQueryEntryPoints[K]>[]
|
||||
}
|
||||
}[keyof RemoteQueryEntryPoints]
|
||||
| {
|
||||
type: string
|
||||
fields: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for configuring the translation module.
|
||||
*/
|
||||
export type TranslationModuleOptions = {
|
||||
entities?: TranslatableEntityConfig[]
|
||||
}
|
||||
|
||||
// Augment the global ModuleOptions registry
|
||||
declare module "@medusajs/types" {
|
||||
interface ModuleOptions {
|
||||
"@medusajs/translation": TranslationModuleOptions
|
||||
"@medusajs/medusa/translation": TranslationModuleOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { TranslationTypes } from "@medusajs/framework/types"
|
||||
|
||||
export function filterTranslationFields(
|
||||
translations: TranslationTypes.TranslationDTO[],
|
||||
translatableFieldsConfig: Record<string, string[]>
|
||||
): TranslationTypes.TranslationDTO[] {
|
||||
return translations.map((translation) => {
|
||||
const allowedFields = translatableFieldsConfig[translation.reference]
|
||||
if (!allowedFields?.length) {
|
||||
translation.translations = {}
|
||||
return translation
|
||||
}
|
||||
|
||||
const filteredTranslations: Record<string, unknown> = {}
|
||||
for (const field of allowedFields) {
|
||||
if (
|
||||
translation.translations &&
|
||||
field in (translation.translations as Record<string, unknown>)
|
||||
) {
|
||||
filteredTranslations[field] = (
|
||||
translation.translations as Record<string, unknown>
|
||||
)[field]
|
||||
}
|
||||
}
|
||||
|
||||
translation.translations = filteredTranslations
|
||||
return translation
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user