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
@@ -101,9 +101,10 @@
|
||||
"keyName": "IDX_store_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_deleted_at\" ON \"store\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_deleted_at\" ON \"store\" (\"deleted_at\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "store_pkey",
|
||||
@@ -111,12 +112,14 @@
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
@@ -197,17 +200,19 @@
|
||||
"keyName": "IDX_store_currency_store_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_currency_store_id\" ON \"store_currency\" (store_id) WHERE deleted_at IS NULL"
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_currency_store_id\" ON \"store_currency\" (\"store_id\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_store_currency_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_currency_deleted_at\" ON \"store_currency\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_currency_deleted_at\" ON \"store_currency\" (\"deleted_at\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "store_currency_pkey",
|
||||
@@ -215,6 +220,7 @@
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
@@ -234,7 +240,131 @@
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"locale_code": {
|
||||
"name": "locale_code",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"is_default": {
|
||||
"name": "is_default",
|
||||
"type": "boolean",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "false",
|
||||
"mappedType": "boolean"
|
||||
},
|
||||
"store_id": {
|
||||
"name": "store_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"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": "store_locale",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_store_locale_store_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_locale_store_id\" ON \"store_locale\" (\"store_id\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_store_locale_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_locale_deleted_at\" ON \"store_locale\" (\"deleted_at\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "store_locale_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"store_locale_store_id_foreign": {
|
||||
"constraintName": "store_locale_store_id_foreign",
|
||||
"columnNames": [
|
||||
"store_id"
|
||||
],
|
||||
"localTableName": "public.store_locale",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.store",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
]
|
||||
],
|
||||
"nativeEnums": {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20251202184737 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`create table if not exists "store_locale" ("id" text not null, "locale_code" text not null, "is_default" boolean not null default false, "store_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "store_locale_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_store_locale_store_id" ON "store_locale" ("store_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_store_locale_deleted_at" ON "store_locale" ("deleted_at") WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`alter table if exists "store_locale" add constraint "store_locale_store_id_foreign" foreign key ("store_id") references "store" ("id") on update cascade on delete cascade;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`drop table if exists "store_locale" cascade;`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as Store } from "./store"
|
||||
export { default as StoreCurrency } from "./currency"
|
||||
export { default as StoreLocale } from "./locale"
|
||||
|
||||
15
packages/modules/store/src/models/locale.ts
Normal file
15
packages/modules/store/src/models/locale.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
import Store from "./store"
|
||||
|
||||
const StoreLocale = model.define("StoreLocale", {
|
||||
id: model.id({ prefix: "stloc" }).primaryKey(),
|
||||
locale_code: model.text().searchable(),
|
||||
is_default: model.boolean().default(false),
|
||||
store: model
|
||||
.belongsTo(() => Store, {
|
||||
mappedBy: "supported_locales",
|
||||
})
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
export default StoreLocale
|
||||
@@ -1,5 +1,6 @@
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
import StoreCurrency from "./currency"
|
||||
import StoreLocale from "./locale"
|
||||
|
||||
const Store = model
|
||||
.define("Store", {
|
||||
@@ -12,9 +13,12 @@ const Store = model
|
||||
supported_currencies: model.hasMany(() => StoreCurrency, {
|
||||
mappedBy: "store",
|
||||
}),
|
||||
supported_locales: model.hasMany(() => StoreLocale, {
|
||||
mappedBy: "store",
|
||||
}),
|
||||
})
|
||||
.cascades({
|
||||
delete: ["supported_currencies"],
|
||||
delete: ["supported_currencies", "supported_locales"],
|
||||
})
|
||||
|
||||
export default Store
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
removeUndefined,
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
import { Store, StoreCurrency } from "@models"
|
||||
import { Store, StoreCurrency, StoreLocale } from "@models"
|
||||
import { UpdateStoreInput } from "@types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
@@ -32,7 +32,8 @@ export default class StoreModuleService
|
||||
extends MedusaService<{
|
||||
Store: { dto: StoreTypes.StoreDTO }
|
||||
StoreCurrency: { dto: StoreTypes.StoreCurrencyDTO }
|
||||
}>({ Store, StoreCurrency })
|
||||
StoreLocale: { dto: StoreTypes.StoreLocaleDTO }
|
||||
}>({ Store, StoreCurrency, StoreLocale })
|
||||
implements IStoreModuleService
|
||||
{
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
@@ -88,7 +89,7 @@ export default class StoreModuleService
|
||||
return (
|
||||
await this.storeService_.upsertWithReplace(
|
||||
normalizedInput,
|
||||
{ relations: ["supported_currencies"] },
|
||||
{ relations: ["supported_currencies", "supported_locales"] },
|
||||
sharedContext
|
||||
)
|
||||
).entities
|
||||
@@ -200,7 +201,7 @@ export default class StoreModuleService
|
||||
return (
|
||||
await this.storeService_.upsertWithReplace(
|
||||
normalizedInput,
|
||||
{ relations: ["supported_currencies"] },
|
||||
{ relations: ["supported_currencies", "supported_locales"] },
|
||||
sharedContext
|
||||
)
|
||||
).entities
|
||||
@@ -226,37 +227,56 @@ export default class StoreModuleService
|
||||
) {
|
||||
for (const store of stores) {
|
||||
if (store.supported_currencies?.length) {
|
||||
const duplicates = getDuplicates(
|
||||
store.supported_currencies?.map((c) => c.currency_code)
|
||||
StoreModuleService.validateSupportedItems(
|
||||
store.supported_currencies,
|
||||
(c) => c.currency_code,
|
||||
"currency"
|
||||
)
|
||||
|
||||
if (duplicates.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Duplicate currency codes: ${duplicates.join(", ")}`
|
||||
)
|
||||
}
|
||||
|
||||
let seenDefault = false
|
||||
store.supported_currencies?.forEach((c) => {
|
||||
if (c.is_default) {
|
||||
if (seenDefault) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Only one default currency is allowed`
|
||||
)
|
||||
}
|
||||
seenDefault = true
|
||||
}
|
||||
})
|
||||
|
||||
if (!seenDefault) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`There should be a default currency set for the store`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: If we are protecting this module behind a feature flag, we should check if the feature flag is enabled before validating the locales.
|
||||
if (store.supported_locales?.length) {
|
||||
StoreModuleService.validateSupportedItems(
|
||||
store.supported_locales,
|
||||
(l) => l.locale_code,
|
||||
"locale"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static validateSupportedItems<T extends { is_default?: boolean }>(
|
||||
items: T[],
|
||||
getCode: (item: T) => string,
|
||||
typeName: string
|
||||
) {
|
||||
const duplicates = getDuplicates(items.map(getCode))
|
||||
|
||||
if (duplicates.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Duplicate ${typeName} codes: ${duplicates.join(", ")}`
|
||||
)
|
||||
}
|
||||
|
||||
let seenDefault = false
|
||||
items.forEach((item) => {
|
||||
if (item.is_default) {
|
||||
if (seenDefault) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Only one default ${typeName} is allowed`
|
||||
)
|
||||
}
|
||||
seenDefault = true
|
||||
}
|
||||
})
|
||||
|
||||
if (!seenDefault) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`There should be a default ${typeName} set for the store`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user