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
5
.changeset/nine-paths-relax.md
Normal file
5
.changeset/nine-paths-relax.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/framework": patch
|
||||
---
|
||||
|
||||
fix(framework): Prevent registering express handler for disabled routes
|
||||
@@ -26,6 +26,7 @@ packages/*
|
||||
!packages/create-medusa-app
|
||||
!packages/modules/product
|
||||
!packages/modules/locking
|
||||
!packages/modules/translation
|
||||
!packages/core/orchestration
|
||||
!packages/core/workflows-sdk
|
||||
!packages/core/core-flows
|
||||
|
||||
83
.eslintrc.js
83
.eslintrc.js
@@ -93,56 +93,57 @@ module.exports = {
|
||||
"./packages/design-system/toolbox/tsconfig.json",
|
||||
|
||||
"./packages/cli/create-medusa-app/tsconfig.json",
|
||||
"./packages/cli/medusa-cli/tsconfig.spec.json",
|
||||
"./packages/cli/oas/medusa-oas-cli/tsconfig.spec.json",
|
||||
"./packages/cli/medusa-cli/tsconfig.json",
|
||||
"./packages/cli/oas/medusa-oas-cli/tsconfig.json",
|
||||
|
||||
"./packages/core/orchestration/tsconfig.json",
|
||||
"./packages/core/workflows-sdk/tsconfig.spec.json",
|
||||
"./packages/core/workflows-sdk/tsconfig.json",
|
||||
"./packages/core/modules-sdk/tsconfig.json",
|
||||
"./packages/core/js-sdk/tsconfig.json",
|
||||
"./packages/core/types/tsconfig.json",
|
||||
"./packages/core/utils/tsconfig.spec.json",
|
||||
"./packages/core/utils/tsconfig.json",
|
||||
"./packages/core/medusa-test-utils/tsconfig.json",
|
||||
|
||||
"./packages/modules/product/tsconfig.json",
|
||||
"./packages/modules/event-bus-local/tsconfig.spec.json",
|
||||
"./packages/modules/event-bus-redis/tsconfig.spec.json",
|
||||
"./packages/modules/cache-redis/tsconfig.spec.json",
|
||||
"./packages/modules/cache-inmemory/tsconfig.spec.json",
|
||||
"./packages/modules/caching/tsconfig.spec.json",
|
||||
"./packages/modules/workflow-engine-redis/tsconfig.spec.json",
|
||||
"./packages/modules/workflow-engine-inmemory/tsconfig.spec.json",
|
||||
"./packages/modules/fulfillment/tsconfig.spec.json",
|
||||
"./packages/modules/api-key/tsconfig.spec.json",
|
||||
"./packages/modules/auth/tsconfig.spec.json",
|
||||
"./packages/modules/cart/tsconfig.spec.json",
|
||||
"./packages/modules/currency/tsconfig.spec.json",
|
||||
"./packages/modules/index/tsconfig.spec.json",
|
||||
"./packages/modules/customer/tsconfig.spec.json",
|
||||
"./packages/modules/file/tsconfig.spec.json",
|
||||
"./packages/modules/inventory-next/tsconfig.spec.json",
|
||||
"./packages/modules/stock-location-next/tsconfig.spec.json",
|
||||
"./packages/modules/order/tsconfig.spec.json",
|
||||
"./packages/modules/payment/tsconfig.spec.json",
|
||||
"./packages/modules/pricing/tsconfig.spec.json",
|
||||
"./packages/modules/promotion/tsconfig.spec.json",
|
||||
"./packages/modules/region/tsconfig.spec.json",
|
||||
"./packages/modules/sales-channel/tsconfig.spec.json",
|
||||
"./packages/modules/store/tsconfig.spec.json",
|
||||
"./packages/modules/tax/tsconfig.spec.json",
|
||||
"./packages/modules/workflow-engine-inmemory/tsconfig.spec.json",
|
||||
"./packages/modules/workflow-engine-redis/tsconfig.spec.json",
|
||||
"./packages/modules/link-modules/tsconfig.spec.json",
|
||||
"./packages/modules/user/tsconfig.spec.json",
|
||||
"./packages/modules/locking/tsconfig.spec.json",
|
||||
"./packages/modules/event-bus-local/tsconfig.json",
|
||||
"./packages/modules/event-bus-redis/tsconfig.json",
|
||||
"./packages/modules/cache-redis/tsconfig.json",
|
||||
"./packages/modules/cache-inmemory/tsconfig.json",
|
||||
"./packages/modules/caching/tsconfig.json",
|
||||
"./packages/modules/workflow-engine-redis/tsconfig.json",
|
||||
"./packages/modules/workflow-engine-inmemory/tsconfig.json",
|
||||
"./packages/modules/fulfillment/tsconfig.json",
|
||||
"./packages/modules/api-key/tsconfig.json",
|
||||
"./packages/modules/auth/tsconfig.json",
|
||||
"./packages/modules/cart/tsconfig.json",
|
||||
"./packages/modules/currency/tsconfig.json",
|
||||
"./packages/modules/index/tsconfig.json",
|
||||
"./packages/modules/customer/tsconfig.json",
|
||||
"./packages/modules/file/tsconfig.json",
|
||||
"./packages/modules/inventory-next/tsconfig.json",
|
||||
"./packages/modules/stock-location-next/tsconfig.json",
|
||||
"./packages/modules/order/tsconfig.json",
|
||||
"./packages/modules/payment/tsconfig.json",
|
||||
"./packages/modules/pricing/tsconfig.json",
|
||||
"./packages/modules/promotion/tsconfig.json",
|
||||
"./packages/modules/region/tsconfig.json",
|
||||
"./packages/modules/sales-channel/tsconfig.json",
|
||||
"./packages/modules/store/tsconfig.json",
|
||||
"./packages/modules/tax/tsconfig.json",
|
||||
"./packages/modules/workflow-engine-inmemory/tsconfig.json",
|
||||
"./packages/modules/workflow-engine-redis/tsconfig.json",
|
||||
"./packages/modules/link-modules/tsconfig.json",
|
||||
"./packages/modules/user/tsconfig.json",
|
||||
"./packages/modules/locking/tsconfig.json",
|
||||
"./packages/modules/translation/tsconfig.json",
|
||||
|
||||
"./packages/modules/providers/file-local/tsconfig.spec.json",
|
||||
"./packages/modules/providers/file-s3/tsconfig.spec.json",
|
||||
"./packages/modules/providers/fulfillment-manual/tsconfig.spec.json",
|
||||
"./packages/modules/providers/payment-stripe/tsconfig.spec.json",
|
||||
"./packages/modules/providers/locking-postgres/tsconfig.spec.json",
|
||||
"./packages/modules/providers/locking-redis/tsconfig.spec.json",
|
||||
"./packages/modules/providers/caching-redis/tsconfig.spec.json",
|
||||
"./packages/modules/providers/file-local/tsconfig.json",
|
||||
"./packages/modules/providers/file-s3/tsconfig.json",
|
||||
"./packages/modules/providers/fulfillment-manual/tsconfig.json",
|
||||
"./packages/modules/providers/payment-stripe/tsconfig.json",
|
||||
"./packages/modules/providers/locking-postgres/tsconfig.json",
|
||||
"./packages/modules/providers/locking-redis/tsconfig.json",
|
||||
"./packages/modules/providers/caching-redis/tsconfig.json",
|
||||
|
||||
"./packages/framework/tsconfig.json",
|
||||
],
|
||||
|
||||
@@ -117,7 +117,8 @@ The code snippets in this section assume that your forked Medusa project and the
|
||||
"@medusajs/draft-order": "file:../medusa/packages/plugins/draft-order",
|
||||
"@medusajs/deps": "file:../medusa/packages/deps",
|
||||
"@medusajs/caching-redis": "file:../medusa/packages/modules/providers/caching-redis",
|
||||
"@medusajs/caching": "file:../medusa/packages/modules/caching"
|
||||
"@medusajs/caching": "file:../medusa/packages/modules/caching",
|
||||
"@medusajs/translation": "file:../medusa/packages/modules/translation",
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -8,3 +8,7 @@ defineFileConfig({
|
||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
res.json({ message: "Custom GET" })
|
||||
}
|
||||
|
||||
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
res.json({ message: "Custom POST", body: req.validatedBody })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
defineMiddlewares,
|
||||
validateAndTransformBody,
|
||||
} from "@medusajs/framework/http"
|
||||
import { z } from "zod"
|
||||
|
||||
const CustomPostSchema = z.object({
|
||||
foo: z.string(),
|
||||
})
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/custom",
|
||||
middlewares: [validateAndTransformBody(CustomPostSchema)],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -43,6 +43,23 @@ medusaIntegrationTestRunner({
|
||||
|
||||
it("should load endpoint when feature flag is enabled", async () => {
|
||||
expect((await api.get("/custom")).status).toBe(200)
|
||||
expect(
|
||||
(
|
||||
await api.post("/custom", {
|
||||
foo: "test",
|
||||
})
|
||||
).status
|
||||
).toBe(200)
|
||||
})
|
||||
|
||||
it("should return 400 for POST route with invalid body when feature flag is enabled", async () => {
|
||||
const response = await api
|
||||
.post("/custom", {
|
||||
invalid: 1,
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -41,6 +41,16 @@ medusaIntegrationTestRunner({
|
||||
it("should not load endpoint when feature flag is disabled", async () => {
|
||||
expect(api.get("/custom")).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("should return 404 (not 400) for POST route with middleware when feature flag is disabled", async () => {
|
||||
const { response } = await api
|
||||
.post("/custom", {
|
||||
invalid: "test",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -207,13 +207,20 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
)
|
||||
|
||||
expect(remainingPricePreferences).toEqual([
|
||||
expect.objectContaining({
|
||||
attribute: "currency_code",
|
||||
value: "EUR",
|
||||
is_tax_inclusive: true,
|
||||
}),
|
||||
])
|
||||
expect(remainingPricePreferences).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
attribute: "currency_code",
|
||||
value: "EUR",
|
||||
is_tax_inclusive: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
attribute: "currency_code",
|
||||
value: "eur",
|
||||
is_tax_inclusive: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -67,7 +67,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.sales_channels).toBeTruthy()
|
||||
expect(response.data.sales_channels.length).toBe(2)
|
||||
expect(response.data.sales_channels.length).toBe(3) // includes the default sales channel
|
||||
expect(response.data).toEqual(
|
||||
expect.objectContaining({
|
||||
sales_channels: expect.arrayContaining([
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"@medusajs/store": "workspace:^",
|
||||
"@medusajs/tax": "workspace:^",
|
||||
"@medusajs/test-utils": "workspace:*",
|
||||
"@medusajs/translation": "workspace:*",
|
||||
"@medusajs/user": "workspace:^",
|
||||
"@medusajs/utils": "workspace:^",
|
||||
"@medusajs/workflow-engine-inmemory": "workspace:*",
|
||||
|
||||
@@ -1009,6 +1009,10 @@ export function getRouteMap({
|
||||
path: "currencies",
|
||||
lazy: () => import("../../routes/store/store-add-currencies"),
|
||||
},
|
||||
{
|
||||
path: "locales",
|
||||
lazy: () => import("../../routes/store/store-add-locales"),
|
||||
},
|
||||
{
|
||||
path: "metadata/edit",
|
||||
lazy: () => import("../../routes/store/store-metadata"),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { sdk } from "../../lib/client"
|
||||
|
||||
export type FeatureFlags = {
|
||||
view_configurations?: boolean
|
||||
translation?: boolean
|
||||
[key: string]: boolean | undefined
|
||||
}
|
||||
|
||||
@@ -10,13 +11,16 @@ export const useFeatureFlags = () => {
|
||||
return useQuery<FeatureFlags>({
|
||||
queryKey: ["admin", "feature-flags"],
|
||||
queryFn: async () => {
|
||||
const response = await sdk.client.fetch<{ feature_flags: FeatureFlags }>("/admin/feature-flags", {
|
||||
method: "GET",
|
||||
})
|
||||
|
||||
const response = await sdk.client.fetch<{ feature_flags: FeatureFlags }>(
|
||||
"/admin/feature-flags",
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
)
|
||||
|
||||
return response.feature_flags
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from "./fulfillment-providers"
|
||||
export * from "./fulfillment-sets"
|
||||
export * from "./inventory"
|
||||
export * from "./invites"
|
||||
export * from "./locales"
|
||||
export * from "./notification"
|
||||
export * from "./orders"
|
||||
export * from "./payment-collections"
|
||||
|
||||
52
packages/admin/dashboard/src/hooks/api/locales.tsx
Normal file
52
packages/admin/dashboard/src/hooks/api/locales.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { FetchError } from "@medusajs/js-sdk"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const LOCALES_QUERY_KEY = "locales" as const
|
||||
const localesQueryKeys = queryKeysFactory(LOCALES_QUERY_KEY)
|
||||
|
||||
export const useLocales = (
|
||||
query?: HttpTypes.AdminLocaleListParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminLocaleListResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminLocaleListResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => sdk.admin.locale.list(query),
|
||||
queryKey: localesQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useLocale = (
|
||||
id: string,
|
||||
query?: HttpTypes.AdminLocaleParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminLocaleResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminLocaleResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: localesQueryKeys.detail(id),
|
||||
queryFn: async () => sdk.admin.locale.retrieve(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
@@ -9303,6 +9303,9 @@
|
||||
"defaultCurrency": {
|
||||
"type": "string"
|
||||
},
|
||||
"defaultLocale": {
|
||||
"type": "string"
|
||||
},
|
||||
"defaultRegion": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9321,6 +9324,9 @@
|
||||
"inviteLinkTemplate": {
|
||||
"type": "string"
|
||||
},
|
||||
"locales": {
|
||||
"type": "string"
|
||||
},
|
||||
"currencies": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9339,6 +9345,15 @@
|
||||
"removeCurrencyWarning_other": {
|
||||
"type": "string"
|
||||
},
|
||||
"removeLocaleWarning_one": {
|
||||
"type": "string"
|
||||
},
|
||||
"removeLocaleWarning_other": {
|
||||
"type": "string"
|
||||
},
|
||||
"localeAlreadyAdded": {
|
||||
"type": "string"
|
||||
},
|
||||
"currencyAlreadyAdded": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9358,20 +9373,28 @@
|
||||
"update": {
|
||||
"type": "string"
|
||||
},
|
||||
"localesUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"currenciesUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"currenciesRemoved": {
|
||||
"type": "string"
|
||||
},
|
||||
"localesRemoved": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedTaxInclusivitySuccessfully": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"update",
|
||||
"localesUpdated",
|
||||
"currenciesUpdated",
|
||||
"currenciesRemoved",
|
||||
"localesRemoved",
|
||||
"updatedTaxInclusivitySuccessfully"
|
||||
],
|
||||
"additionalProperties": false
|
||||
@@ -9382,19 +9405,24 @@
|
||||
"manageYourStoresDetails",
|
||||
"editStore",
|
||||
"defaultCurrency",
|
||||
"defaultLocale",
|
||||
"defaultRegion",
|
||||
"defaultSalesChannel",
|
||||
"defaultLocation",
|
||||
"swapLinkTemplate",
|
||||
"paymentLinkTemplate",
|
||||
"inviteLinkTemplate",
|
||||
"locales",
|
||||
"currencies",
|
||||
"addCurrencies",
|
||||
"enableTaxInclusivePricing",
|
||||
"disableTaxInclusivePricing",
|
||||
"removeCurrencyWarning_one",
|
||||
"removeCurrencyWarning_other",
|
||||
"removeLocaleWarning_one",
|
||||
"removeLocaleWarning_other",
|
||||
"currencyAlreadyAdded",
|
||||
"localeAlreadyAdded",
|
||||
"edit",
|
||||
"toast"
|
||||
],
|
||||
|
||||
@@ -2497,26 +2497,33 @@
|
||||
"manageYourStoresDetails": "Manage your store's details",
|
||||
"editStore": "Edit store",
|
||||
"defaultCurrency": "Default currency",
|
||||
"defaultLocale": "Default locale",
|
||||
"defaultRegion": "Default region",
|
||||
"defaultSalesChannel": "Default sales channel",
|
||||
"defaultLocation": "Default location",
|
||||
"swapLinkTemplate": "Swap link template",
|
||||
"paymentLinkTemplate": "Payment link template",
|
||||
"inviteLinkTemplate": "Invite link template",
|
||||
"locales": "Locales",
|
||||
"currencies": "Currencies",
|
||||
"addCurrencies": "Add currencies",
|
||||
"enableTaxInclusivePricing": "Enable tax inclusive pricing",
|
||||
"disableTaxInclusivePricing": "Disable tax inclusive pricing",
|
||||
"removeCurrencyWarning_one": "You are about to remove {{count}} currency from your store. Ensure that you have removed all prices using the currency before proceeding.",
|
||||
"removeCurrencyWarning_other": "You are about to remove {{count}} currencies from your store. Ensure that you have removed all prices using the currencies before proceeding.",
|
||||
"removeLocaleWarning_one": "You are about to remove {{count}} locale from your store. Any translation using this locale will be removed.",
|
||||
"removeLocaleWarning_other": "You are about to remove {{count}} locales from your store. Any translation using these locales will be removed.",
|
||||
"currencyAlreadyAdded": "The currency has already been added to your store.",
|
||||
"localeAlreadyAdded": "The locale has already been added to your store.",
|
||||
"edit": {
|
||||
"header": "Edit Store"
|
||||
},
|
||||
"toast": {
|
||||
"update": "Store successfully updated",
|
||||
"currenciesUpdated": "Currencies updated successfully",
|
||||
"localesUpdated": "Locales updated successfully",
|
||||
"currenciesRemoved": "Removed currencies from the store successfully",
|
||||
"localesRemoved": "Removed locales from the store successfully",
|
||||
"updatedTaxInclusivitySuccessfully": "Tax inclusive pricing updated successfully"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2448,26 +2448,33 @@
|
||||
"manageYourStoresDetails": "Gestiona los detalles de tu tienda",
|
||||
"editStore": "Editar tienda",
|
||||
"defaultCurrency": "Moneda por defecto",
|
||||
"defaultLocale": "Idioma por defecto",
|
||||
"defaultRegion": "Región por defecto",
|
||||
"defaultSalesChannel": "Canal de ventas predeterminado",
|
||||
"defaultLocation": "Ubicación predeterminada",
|
||||
"swapLinkTemplate": "Plantilla de enlace de cambio",
|
||||
"paymentLinkTemplate": "Plantilla de enlace de pago",
|
||||
"inviteLinkTemplate": "Plantilla de enlace de invitación",
|
||||
"locales": "Idiomas",
|
||||
"currencies": "Monedas",
|
||||
"addCurrencies": "Agregar monedas",
|
||||
"enableTaxInclusivePricing": "Habilitar precios con impuestos incluidos",
|
||||
"disableTaxInclusivePricing": "Deshabilitar precios con impuestos incluidos",
|
||||
"removeCurrencyWarning_one": "Estás a punto de eliminar {{count}} moneda de tu tienda. Asegúrate de haber eliminado todos los precios que usen esta moneda antes de continuar.",
|
||||
"removeCurrencyWarning_other": "Estás a punto de eliminar {{count}} monedas de tu tienda. Asegúrate de haber eliminado todos los precios que usen estas monedas antes de continuar.",
|
||||
"removeLocaleWarning_one": "Estás a punto de eliminar {{count}} idioma de tu tienda. Cualquier traducción que use este idioma será eliminada.",
|
||||
"removeLocaleWarning_other": "Estás a punto de eliminar {{count}} idiomas de tu tienda. Cualquier traducción que use estos idiomas será eliminada.",
|
||||
"currencyAlreadyAdded": "La moneda ya ha sido agregada a tu tienda.",
|
||||
"localeAlreadyAdded": "El idioma ya ha sido agregado a tu tienda.",
|
||||
"edit": {
|
||||
"header": "Editar Tienda"
|
||||
},
|
||||
"toast": {
|
||||
"update": "Tienda actualizada exitosamente",
|
||||
"currenciesUpdated": "Monedas actualizadas exitosamente",
|
||||
"localesUpdated": "Idiomas actualizados exitosamente",
|
||||
"currenciesRemoved": "Monedas eliminadas de la tienda exitosamente",
|
||||
"localesRemoved": "Idiomas eliminados de la tienda exitosamente",
|
||||
"updatedTaxInclusivitySuccessfully": "Precios con impuestos incluidos actualizados exitosamente"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
TextCell,
|
||||
TextHeader,
|
||||
} from "../../../../components/table/table-cells/common/text-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<HttpTypes.AdminLocale>()
|
||||
|
||||
export const useLocalesTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("code", {
|
||||
header: () => <TextHeader text={t("fields.code")} />,
|
||||
cell: ({ getValue }) => <TextCell text={getValue()} />,
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: () => <TextHeader text={t("fields.name")} />,
|
||||
cell: ({ getValue }) => <TextCell text={getValue()} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||
|
||||
export const useLocalesTableQuery = ({
|
||||
pageSize = 10,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(["order", "q", "offset"], prefix)
|
||||
|
||||
const { offset, ...rest } = raw
|
||||
|
||||
const searchParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? parseInt(offset) : 0,
|
||||
...rest,
|
||||
}
|
||||
|
||||
return { searchParams, raw }
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { z } from "zod"
|
||||
import { useLocalesTableQuery } from "../../../common/hooks/use-locales-table-query"
|
||||
import { useRouteModal } from "../../../../../components/modals/route-modal-provider"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useLocales, useUpdateStore } from "../../../../../hooks/api"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
createColumnHelper,
|
||||
OnChangeFn,
|
||||
RowSelectionState,
|
||||
} from "@tanstack/react-table"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { Button, Checkbox, Hint, toast, Tooltip } from "@medusajs/ui"
|
||||
import { RouteFocusModal } from "../../../../../components/modals/route-focus-modal"
|
||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||
import { _DataTable } from "../../../../../components/table/data-table"
|
||||
import { useLocalesTableColumns } from "../../../common/hooks/use-locales-table-columns"
|
||||
|
||||
type AddLocalesFormProps = {
|
||||
store: HttpTypes.AdminStore
|
||||
}
|
||||
|
||||
const AddLocalesSchema = z.object({
|
||||
locales: z.array(z.string()).min(1),
|
||||
})
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PREFIX = "al"
|
||||
|
||||
export const AddLocalesForm = ({ store }: AddLocalesFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const { raw, searchParams } = useLocalesTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const {
|
||||
locales,
|
||||
count,
|
||||
isPending: isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useLocales(searchParams, {
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const form = useForm<z.infer<typeof AddLocalesSchema>>({
|
||||
defaultValues: {
|
||||
locales: [],
|
||||
},
|
||||
resolver: zodResolver(AddLocalesSchema),
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { setValue } = form
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const updated = typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const ids = Object.keys(updated)
|
||||
setValue("locales", ids, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
|
||||
setRowSelection(updated)
|
||||
}
|
||||
|
||||
const preSelectedRows =
|
||||
store.supported_locales?.map((l) => l.locale_code) ?? []
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: locales ?? [],
|
||||
columns,
|
||||
count: count,
|
||||
getRowId: (row) => row.code,
|
||||
enableRowSelection: (row) => !preSelectedRows.includes(row.original.code),
|
||||
enablePagination: true,
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useUpdateStore(store.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const locales = Array.from(
|
||||
new Set([...data.locales, ...preSelectedRows])
|
||||
) as string[]
|
||||
|
||||
let defaultLocale = store.supported_locales?.find(
|
||||
(l) => l.is_default
|
||||
)?.locale_code
|
||||
|
||||
if (!locales.includes(defaultLocale ?? "")) {
|
||||
defaultLocale = locales?.[0]
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
supported_locales: locales.map((l) => ({
|
||||
locale_code: l,
|
||||
is_default: l === defaultLocale,
|
||||
})),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("store.toast.localesUpdated"))
|
||||
handleSuccess()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<KeyboundForm
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{form.formState.errors.locales && (
|
||||
<Hint variant="error">
|
||||
{form.formState.errors.locales.message}
|
||||
</Hint>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<_DataTable
|
||||
table={table}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
columns={columns}
|
||||
layout="fill"
|
||||
pagination
|
||||
search="autofocus"
|
||||
prefix={PREFIX}
|
||||
orderBy={[
|
||||
{ key: "name", label: t("fields.name") },
|
||||
{ key: "code", label: t("fields.code") },
|
||||
]}
|
||||
isLoading={isLoading}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</RouteFocusModal.Body>
|
||||
<RouteFocusModal.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Footer>
|
||||
</KeyboundForm>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<HttpTypes.AdminLocale>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
const base = useLocalesTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isPreSelected = !row.getCanSelect()
|
||||
const isSelected = row.getIsSelected() || isPreSelected
|
||||
|
||||
const Component = (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={isPreSelected}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isPreSelected) {
|
||||
return (
|
||||
<Tooltip content={t("store.localeAlreadyAdded")} side="right">
|
||||
{Component}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return Component
|
||||
},
|
||||
}),
|
||||
...base,
|
||||
],
|
||||
[t, base]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { StoreAddLocales as Component } from "./store-add-locales"
|
||||
@@ -0,0 +1,29 @@
|
||||
import { RouteFocusModal } from "../../../components/modals/route-focus-modal"
|
||||
import { useStore } from "../../../hooks/api"
|
||||
import { AddLocalesForm } from "./components/add-locales-form/add-locales-form"
|
||||
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
export const StoreAddLocales = () => {
|
||||
const isEnabled = useFeatureFlag("translation")
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (!isEnabled) {
|
||||
navigate(-1)
|
||||
return null
|
||||
}
|
||||
|
||||
const { store, isPending, isError, error } = useStore()
|
||||
|
||||
const ready = !!store && !isPending
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{ready && <AddLocalesForm store={store} />}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { useSalesChannel, useStockLocation } from "../../../../../hooks/api"
|
||||
import { useRegion } from "../../../../../hooks/api/regions"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
|
||||
type StoreGeneralSectionProps = {
|
||||
store: AdminStore
|
||||
@@ -14,12 +15,14 @@ type StoreGeneralSectionProps = {
|
||||
|
||||
export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isTranslationsEnabled = useFeatureFlag("translation")
|
||||
|
||||
const { region } = useRegion(store.default_region_id!, undefined, {
|
||||
enabled: !!store.default_region_id,
|
||||
})
|
||||
|
||||
const defaultCurrency = store.supported_currencies?.find((c) => c.is_default)
|
||||
const defaultLocale = store.supported_locales?.find((l) => l.is_default)
|
||||
|
||||
const { sales_channel } = useSalesChannel(store.default_sales_channel_id!, {
|
||||
enabled: !!store.default_sales_channel_id,
|
||||
@@ -85,6 +88,27 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{isTranslationsEnabled && (
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("store.defaultLocale")}
|
||||
</Text>
|
||||
{defaultLocale ? (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Badge size="2xsmall">
|
||||
{defaultLocale.locale_code?.toUpperCase()}
|
||||
</Badge>
|
||||
<Text size="small" leading="compact">
|
||||
{defaultLocale.locale?.name}
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="small" leading="compact">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("store.defaultRegion")}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./store-locale-section"
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Plus, Trash } from "@medusajs/icons"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
Checkbox,
|
||||
CommandBar,
|
||||
Container,
|
||||
Heading,
|
||||
toast,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { _DataTable } from "../../../../../components/table/data-table"
|
||||
import { useUpdateStore } from "../../../../../hooks/api/store"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useLocalesTableColumns } from "../../../common/hooks/use-locales-table-columns"
|
||||
import { useLocalesTableQuery } from "../../../common/hooks/use-locales-table-query"
|
||||
import { useLocales } from "../../../../../hooks/api"
|
||||
|
||||
type StoreLocaleSectionProps = {
|
||||
store: HttpTypes.AdminStore
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export const StoreLocaleSection = ({ store }: StoreLocaleSectionProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { searchParams, raw } = useLocalesTableQuery({ pageSize: PAGE_SIZE })
|
||||
|
||||
const { locales, count, isPending, isError, error } = useLocales(
|
||||
{
|
||||
code: store.supported_locales?.map((l) => l.locale_code),
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: !!store.supported_locales?.length,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: locales ?? [],
|
||||
columns,
|
||||
count: count,
|
||||
getRowId: (row) => row.code,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater: setRowSelection,
|
||||
},
|
||||
enablePagination: true,
|
||||
enableRowSelection: true,
|
||||
pageSize: PAGE_SIZE,
|
||||
meta: {
|
||||
storeId: store.id,
|
||||
supportedLocales: store.supported_locales,
|
||||
defaultLocaleCode: store.supported_locales?.find((l) => l.is_default)
|
||||
?.locale_code,
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync } = useUpdateStore(store.id)
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleDeleteLocales = async () => {
|
||||
const ids = Object.keys(rowSelection)
|
||||
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("store.removeLocaleWarning", {
|
||||
count: ids.length,
|
||||
}),
|
||||
confirmText: t("actions.remove"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
supported_locales:
|
||||
store.supported_locales?.filter(
|
||||
(l) => !ids.includes(l.locale_code)
|
||||
) ?? [],
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setRowSelection({})
|
||||
toast.success(t("store.toast.localesRemoved"))
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(e.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const isLoading = isPending
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("store.locales")}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Plus />,
|
||||
label: t("actions.add"),
|
||||
to: "locales",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<_DataTable
|
||||
orderBy={[
|
||||
{ key: "name", label: t("fields.name") },
|
||||
{ key: "code", label: t("fields.code") },
|
||||
]}
|
||||
search
|
||||
pagination
|
||||
table={table}
|
||||
pageSize={PAGE_SIZE}
|
||||
columns={columns}
|
||||
count={!store.supported_locales?.length ? 0 : count}
|
||||
isLoading={!store.supported_locales?.length ? false : isLoading}
|
||||
queryObject={raw}
|
||||
/>
|
||||
<CommandBar open={!!Object.keys(rowSelection).length}>
|
||||
<CommandBar.Bar>
|
||||
<CommandBar.Value>
|
||||
{t("general.countSelected", {
|
||||
count: Object.keys(rowSelection).length,
|
||||
})}
|
||||
</CommandBar.Value>
|
||||
<CommandBar.Seperator />
|
||||
<CommandBar.Command
|
||||
action={handleDeleteLocales}
|
||||
shortcut="r"
|
||||
label={t("actions.remove")}
|
||||
/>
|
||||
</CommandBar.Bar>
|
||||
</CommandBar>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const LocaleActions = ({
|
||||
storeId,
|
||||
locale,
|
||||
supportedLocales,
|
||||
defaultLocaleCode,
|
||||
}: {
|
||||
storeId: string
|
||||
locale: HttpTypes.AdminLocale
|
||||
supportedLocales: HttpTypes.AdminStoreLocale[]
|
||||
defaultLocaleCode: string
|
||||
}) => {
|
||||
const { mutateAsync } = useUpdateStore(storeId)
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleRemove = async () => {
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("store.removeLocaleWarning", {
|
||||
count: 1,
|
||||
}),
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
verificationText: locale.name,
|
||||
confirmText: t("actions.remove"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
supported_locales: supportedLocales.filter(
|
||||
(l) => l.locale_code !== locale.code
|
||||
),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("store.toast.localesRemoved"))
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(e.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.remove"),
|
||||
onClick: handleRemove,
|
||||
disabled: locale.code === defaultLocaleCode,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<HttpTypes.AdminLocale>()
|
||||
|
||||
const useColumns = () => {
|
||||
const base = useLocalesTableColumns()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
...base,
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
const { supportedLocales, storeId, defaultLocaleCode } = table.options
|
||||
.meta as {
|
||||
defaultLocaleCode: string
|
||||
supportedLocales: HttpTypes.AdminStoreLocale[]
|
||||
storeId: string
|
||||
}
|
||||
|
||||
return (
|
||||
<LocaleActions
|
||||
storeId={storeId}
|
||||
locale={row.original}
|
||||
supportedLocales={supportedLocales}
|
||||
defaultLocaleCode={defaultLocaleCode}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[base, t]
|
||||
)
|
||||
}
|
||||
@@ -8,9 +8,12 @@ import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useExtension } from "../../../providers/extension-provider"
|
||||
import { StoreCurrencySection } from "./components/store-currency-section"
|
||||
import { StoreLocaleSection } from "./components/store-locale-section"
|
||||
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
|
||||
|
||||
export const StoreDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<ReturnType<typeof storeLoader>>
|
||||
const isTranslationsEnabled = useFeatureFlag("translation")
|
||||
|
||||
const { store, isPending, isError, error } = useStore(undefined, {
|
||||
initialData,
|
||||
@@ -39,6 +42,7 @@ export const StoreDetail = () => {
|
||||
>
|
||||
<StoreGeneralSection store={store} />
|
||||
<StoreCurrencySection store={store} />
|
||||
{isTranslationsEnabled && <StoreLocaleSection store={store} />}
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useUpdateStore } from "../../../../../hooks/api/store"
|
||||
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { useDocumentDirection } from "../../../../../hooks/use-document-direction"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
|
||||
type EditStoreFormProps = {
|
||||
store: HttpTypes.AdminStore
|
||||
@@ -21,6 +22,7 @@ type EditStoreFormProps = {
|
||||
const EditStoreSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
default_currency_code: z.string().optional(),
|
||||
default_locale_code: z.string().optional(),
|
||||
default_region_id: z.string().optional(),
|
||||
default_sales_channel_id: z.string().optional(),
|
||||
default_location_id: z.string().optional(),
|
||||
@@ -28,12 +30,16 @@ const EditStoreSchema = z.object({
|
||||
|
||||
export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isTranslationsEnabled = useFeatureFlag("translation")
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const direction = useDocumentDirection()
|
||||
const direction = useDocumentDirection()
|
||||
const form = useForm<z.infer<typeof EditStoreSchema>>({
|
||||
defaultValues: {
|
||||
name: store.name,
|
||||
default_region_id: store.default_region_id || undefined,
|
||||
default_locale_code:
|
||||
store.supported_locales?.find((l) => l.is_default)?.locale_code ||
|
||||
undefined,
|
||||
default_currency_code:
|
||||
store.supported_currencies?.find((c) => c.is_default)?.currency_code ||
|
||||
undefined,
|
||||
@@ -73,10 +79,14 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
const { default_currency_code, ...rest } = values
|
||||
const { default_currency_code, default_locale_code, ...rest } = values
|
||||
|
||||
const normalizedMutation: HttpTypes.AdminUpdateStore = {
|
||||
...rest,
|
||||
supported_locales: store.supported_locales?.map((l) => ({
|
||||
...l,
|
||||
is_default: l.locale_code === default_locale_code,
|
||||
})),
|
||||
supported_currencies: store.supported_currencies?.map((c) => ({
|
||||
...c,
|
||||
is_default: c.currency_code === default_currency_code,
|
||||
@@ -95,7 +105,10 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col overflow-hidden">
|
||||
<KeyboundForm
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteDrawer.Body className="overflow-y-auto">
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<Form.Field
|
||||
@@ -143,6 +156,40 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{isTranslationsEnabled && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="default_locale_code"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("store.defaultLocale")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Select
|
||||
dir={direction}
|
||||
{...field}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<Select.Trigger ref={field.ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{store.supported_locales?.map((locale) => (
|
||||
<Select.Item
|
||||
key={locale.locale_code}
|
||||
value={locale.locale_code}
|
||||
>
|
||||
{locale.locale_code}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="default_region_id"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
112
packages/core/framework/src/http/__tests__/apply-locale.spec.ts
Normal file
112
packages/core/framework/src/http/__tests__/apply-locale.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { MedusaRequest, MedusaResponse } from "../types"
|
||||
import { applyLocale } from "../middlewares/apply-locale"
|
||||
import { MedusaContainer } from "@medusajs/types"
|
||||
|
||||
describe("applyLocale", () => {
|
||||
let mockRequest: Partial<MedusaRequest>
|
||||
let mockResponse: MedusaResponse
|
||||
let nextFunction: jest.Mock
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {
|
||||
query: {},
|
||||
get: jest.fn(),
|
||||
scope: {
|
||||
resolve: jest.fn().mockReturnValue({
|
||||
graph: jest.fn().mockResolvedValue({
|
||||
data: [{ supported_locales: [{ locale_code: "en-US" }] }],
|
||||
}),
|
||||
}),
|
||||
} as unknown as MedusaContainer,
|
||||
}
|
||||
mockResponse = {} as MedusaResponse
|
||||
nextFunction = jest.fn()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should set locale from query parameter", async () => {
|
||||
mockRequest.query = { locale: "en-US" }
|
||||
|
||||
await applyLocale(mockRequest as MedusaRequest, mockResponse, nextFunction)
|
||||
|
||||
expect(mockRequest.locale).toBe("en-US")
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should set locale from Content-Language header when query param is not present", async () => {
|
||||
mockRequest.query = {}
|
||||
;(mockRequest.get as jest.Mock).mockImplementation((header: string) => {
|
||||
if (header === "content-language") {
|
||||
return "fr-FR"
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
await applyLocale(mockRequest as MedusaRequest, mockResponse, nextFunction)
|
||||
|
||||
expect(mockRequest.locale).toBe("fr-FR")
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should prioritize query parameter over Content-Language header", async () => {
|
||||
mockRequest.query = { locale: "de-DE" }
|
||||
;(mockRequest.get as jest.Mock).mockImplementation((header: string) => {
|
||||
if (header === "content-language") {
|
||||
return "fr-FR"
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
await applyLocale(mockRequest as MedusaRequest, mockResponse, nextFunction)
|
||||
|
||||
expect(mockRequest.locale).toBe("de-DE")
|
||||
expect(mockRequest.get).not.toHaveBeenCalled()
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should not set locale when neither query param nor header is present", async () => {
|
||||
mockRequest.query = {}
|
||||
;(mockRequest.get as jest.Mock).mockReturnValue(undefined)
|
||||
|
||||
await applyLocale(mockRequest as MedusaRequest, mockResponse, nextFunction)
|
||||
|
||||
expect(mockRequest.locale).toBeUndefined()
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should handle empty string in query parameter", async () => {
|
||||
mockRequest.query = { locale: "" }
|
||||
;(mockRequest.get as jest.Mock).mockImplementation((header: string) => {
|
||||
if (header === "content-language") {
|
||||
return "es-ES"
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
await applyLocale(mockRequest as MedusaRequest, mockResponse, nextFunction)
|
||||
|
||||
// Empty string is falsy, so it should fall back to header
|
||||
expect(mockRequest.locale).toBe("es-ES")
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should handle various locale formats", async () => {
|
||||
const locales = ["en", "en-US", "zh-Hans-CN", "pt-BR"]
|
||||
|
||||
for (const locale of locales) {
|
||||
mockRequest.query = { locale }
|
||||
mockRequest.locale = undefined
|
||||
|
||||
await applyLocale(
|
||||
mockRequest as MedusaRequest,
|
||||
mockResponse,
|
||||
nextFunction
|
||||
)
|
||||
|
||||
expect(mockRequest.locale).toBe(locale)
|
||||
}
|
||||
})
|
||||
})
|
||||
64
packages/core/framework/src/http/middlewares/apply-locale.ts
Normal file
64
packages/core/framework/src/http/middlewares/apply-locale.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ContainerRegistrationKeys, normalizeLocale } from "@medusajs/utils"
|
||||
import type {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../types"
|
||||
|
||||
const CONTENT_LANGUAGE_HEADER = "content-language"
|
||||
|
||||
/**
|
||||
* Middleware that resolves the locale for the current request.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. Query parameter `?locale=en-US`
|
||||
* 2. Content-Language header
|
||||
*
|
||||
* The resolved locale is set on `req.locale`.
|
||||
*/
|
||||
export async function applyLocale(
|
||||
req: MedusaRequest,
|
||||
_: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) {
|
||||
// 1. Check query parameter
|
||||
const queryLocale = req.query.locale as string | undefined
|
||||
if (queryLocale) {
|
||||
req.locale = normalizeLocale(queryLocale)
|
||||
return next()
|
||||
}
|
||||
|
||||
// 2. Check Content-Language header
|
||||
const headerLocale = req.get(CONTENT_LANGUAGE_HEADER)
|
||||
if (headerLocale) {
|
||||
req.locale = normalizeLocale(headerLocale)
|
||||
return next()
|
||||
}
|
||||
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
const {
|
||||
data: [store],
|
||||
} = await query.graph(
|
||||
{
|
||||
entity: "store",
|
||||
fields: ["id", "supported_locales"],
|
||||
pagination: {
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
cache: {
|
||||
enable: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (store?.supported_locales?.length) {
|
||||
req.locale = store.supported_locales.find(
|
||||
(locale) => locale.is_default
|
||||
)?.locale_code
|
||||
return next()
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
@@ -3,5 +3,6 @@ export * from "./error-handler"
|
||||
export * from "./exception-formatter"
|
||||
export * from "./apply-default-filters"
|
||||
export * from "./apply-params-as-filters"
|
||||
export * from "./apply-locale"
|
||||
export * from "./clear-filters-by-key"
|
||||
export * from "./set-context"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ContainerRegistrationKeys, parseCorsOrigins, FeatureFlag } from "@medusajs/utils"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
FeatureFlag,
|
||||
isFileDisabled,
|
||||
parseCorsOrigins,
|
||||
} from "@medusajs/utils"
|
||||
import cors, { CorsOptions } from "cors"
|
||||
import type {
|
||||
ErrorRequestHandler,
|
||||
@@ -20,6 +25,7 @@ import type {
|
||||
} from "./types"
|
||||
|
||||
import { Logger, MedusaContainer } from "@medusajs/types"
|
||||
import { join } from "path"
|
||||
import { configManager } from "../config"
|
||||
import { MiddlewareFileLoader } from "./middleware-file-loader"
|
||||
import { authenticate, AuthType } from "./middlewares"
|
||||
@@ -109,6 +115,38 @@ export class ApiLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a route file is disabled for a given matcher and method
|
||||
* by trying to find the corresponding route file path
|
||||
*/
|
||||
#isRouteFileDisabled(matcher: string): boolean {
|
||||
const routePathSegments = matcher
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map((segment) => {
|
||||
if (segment.startsWith(":")) {
|
||||
return `[${segment.slice(1)}]`
|
||||
}
|
||||
return segment
|
||||
})
|
||||
|
||||
for (const sourceDir of this.#sourceDirs) {
|
||||
for (const ext of [".ts", ".js"]) {
|
||||
const routeFilePath = join(
|
||||
sourceDir,
|
||||
...routePathSegments,
|
||||
`route${ext}`
|
||||
)
|
||||
|
||||
if (isFileDisabled(routeFilePath)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a middleware or a route handler with Express
|
||||
*/
|
||||
@@ -145,6 +183,14 @@ export class ApiLoader {
|
||||
? route.methods
|
||||
: [route.methods]
|
||||
methods.forEach((method) => {
|
||||
const isDisabled = this.#isRouteFileDisabled(route.matcher)
|
||||
if (isDisabled) {
|
||||
this.#logger.debug(
|
||||
`skipping disabled route middleware registration for ${method} ${route.matcher}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.#logger.debug(
|
||||
`registering route middleware ${method} ${route.matcher}`
|
||||
)
|
||||
|
||||
@@ -183,6 +183,14 @@ export interface MedusaRequest<
|
||||
* requests that allows for additional_data
|
||||
*/
|
||||
additionalDataValidator?: ZodOptional<ZodNullable<ZodObject<any, any>>>
|
||||
|
||||
/**
|
||||
* The locale for the current request, resolved from:
|
||||
* 1. Query parameter `?locale=`
|
||||
* 2. Content-Language header
|
||||
* 3. Store's default locale
|
||||
*/
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export interface AuthContext {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
IStockLocationService,
|
||||
IStoreModuleService,
|
||||
ITaxModuleService,
|
||||
ITranslationModuleService,
|
||||
IUserModuleService,
|
||||
IWorkflowEngineService,
|
||||
Logger,
|
||||
@@ -34,8 +35,8 @@ import {
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/types"
|
||||
import { ContainerRegistrationKeys, Modules } from "@medusajs/utils"
|
||||
import { Knex } from "../deps/mikro-orm-knex"
|
||||
import { AwilixContainer, ResolveOptions } from "../deps/awilix"
|
||||
import { Knex } from "../deps/mikro-orm-knex"
|
||||
|
||||
declare module "@medusajs/types" {
|
||||
export interface ModuleImplementations {
|
||||
@@ -80,6 +81,7 @@ declare module "@medusajs/types" {
|
||||
[Modules.SETTINGS]: ISettingsModuleService
|
||||
[Modules.CACHING]: ICachingModuleService
|
||||
[Modules.INDEX]: IIndexService
|
||||
[Modules.TRANSLATION]: ITranslationModuleService
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import { User } from "./user"
|
||||
import { Views } from "./views"
|
||||
import { WorkflowExecution } from "./workflow-execution"
|
||||
import { ShippingOptionType } from "./shipping-option-type"
|
||||
import { Locale } from "./locale"
|
||||
|
||||
export class Admin {
|
||||
/**
|
||||
@@ -179,6 +180,10 @@ export class Admin {
|
||||
* @tags currency
|
||||
*/
|
||||
public currency: Currency
|
||||
/**
|
||||
* @tags locale
|
||||
*/
|
||||
public locale: Locale
|
||||
/**
|
||||
* @tags payment
|
||||
*/
|
||||
@@ -265,6 +270,7 @@ export class Admin {
|
||||
this.store = new Store(client)
|
||||
this.productTag = new ProductTag(client)
|
||||
this.user = new User(client)
|
||||
this.locale = new Locale(client)
|
||||
this.currency = new Currency(client)
|
||||
this.payment = new Payment(client)
|
||||
this.productVariant = new ProductVariant(client)
|
||||
|
||||
119
packages/core/js-sdk/src/admin/locale.ts
Normal file
119
packages/core/js-sdk/src/admin/locale.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Client } from "../client"
|
||||
import { ClientHeaders } from "../types"
|
||||
|
||||
export class Locale {
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
private client: Client
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of locales. It sends a request to the
|
||||
* [List Locales](https://docs.medusajs.com/api/admin#locales_getlocales)
|
||||
* API route.
|
||||
*
|
||||
* @param query - Filters and pagination configurations.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @returns The paginated list of locales.
|
||||
*
|
||||
* @example
|
||||
* To retrieve the list of locales:
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.locales.list()
|
||||
* .then(({ locales, count, limit, offset }) => {
|
||||
* console.log(locales)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* To configure the pagination, pass the `limit` and `offset` query parameters.
|
||||
*
|
||||
* For example, to retrieve only 10 items and skip 10 items:
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.locales.list({
|
||||
* limit: 10,
|
||||
* offset: 10
|
||||
* })
|
||||
* .then(({ locales, count, limit, offset }) => {
|
||||
* console.log(locales)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Using the `fields` query parameter, you can specify the fields and relations to retrieve
|
||||
* in each locale:
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.locales.list({
|
||||
* fields: "code,name"
|
||||
* })
|
||||
* .then(({ locales, count, limit, offset }) => {
|
||||
* console.log(locales)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
|
||||
*/
|
||||
async list(query?: HttpTypes.AdminLocaleListParams, headers?: ClientHeaders) {
|
||||
return this.client.fetch<HttpTypes.AdminLocaleListResponse>(
|
||||
`/admin/locales`,
|
||||
{
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method retrieves a locale by its code. It sends a request to the
|
||||
* [Get Locale](https://docs.medusajs.com/api/admin#locales_getlocalescode) API route.
|
||||
*
|
||||
* @param code - The locale's code.
|
||||
* @param query - Configure the fields to retrieve in the locale.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The locale's details.
|
||||
*
|
||||
* @example
|
||||
* To retrieve a locale by its code:
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.locale.retrieve("en-US")
|
||||
* .then(({ locale }) => {
|
||||
* console.log(locale)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* To specify the fields and relations to retrieve:
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.locale.retrieve("en-US", {
|
||||
* fields: "code,name"
|
||||
* })
|
||||
* .then(({ locale }) => {
|
||||
* console.log(locale)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
|
||||
*/
|
||||
async retrieve(
|
||||
code: string,
|
||||
query?: HttpTypes.AdminLocaleParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return this.client.fetch<HttpTypes.AdminLocaleResponse>(
|
||||
`/admin/locales/${code}`,
|
||||
{
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export * as StockLocationTypes from "./stock-location"
|
||||
export * as StoreTypes from "./store"
|
||||
export * as TaxTypes from "./tax"
|
||||
export * as TransactionBaseTypes from "./transaction-base"
|
||||
export * as TranslationTypes from "./translation"
|
||||
export * as UserTypes from "./user"
|
||||
export * as WorkflowTypes from "./workflow"
|
||||
export * as WorkflowsSdkTypes from "./workflows-sdk"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
export interface BaseCurrency {
|
||||
/**
|
||||
* The currency's code.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* usd
|
||||
*/
|
||||
code: string
|
||||
/**
|
||||
* The currency's symbol.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* $
|
||||
*/
|
||||
symbol: string
|
||||
/**
|
||||
* The currency's symbol in its native language or country.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* $
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,8 @@ export * from "./claim"
|
||||
export * from "./collection"
|
||||
export * from "./common"
|
||||
export * from "./currency"
|
||||
export * from "./locale"
|
||||
export * from "./translations"
|
||||
export * from "./customer"
|
||||
export * from "./customer-group"
|
||||
export * from "./draft-order"
|
||||
|
||||
3
packages/core/types/src/http/locale/admin/entities.ts
Normal file
3
packages/core/types/src/http/locale/admin/entities.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { BaseLocale } from "../common"
|
||||
|
||||
export interface AdminLocale extends BaseLocale {}
|
||||
3
packages/core/types/src/http/locale/admin/index.ts
Normal file
3
packages/core/types/src/http/locale/admin/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./entities"
|
||||
export * from "./queries"
|
||||
export * from "./responses"
|
||||
17
packages/core/types/src/http/locale/admin/queries.ts
Normal file
17
packages/core/types/src/http/locale/admin/queries.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { BaseFilterable } from "../../../dal"
|
||||
import { FindParams, SelectParams } from "../../common"
|
||||
|
||||
export interface AdminLocaleParams extends SelectParams {}
|
||||
|
||||
export interface AdminLocaleListParams
|
||||
extends FindParams,
|
||||
BaseFilterable<AdminLocaleListParams> {
|
||||
/**
|
||||
* Query or keyword to search the locale's searchable fields.
|
||||
*/
|
||||
q?: string
|
||||
/**
|
||||
* Filter by locale code(s).
|
||||
*/
|
||||
code?: string | string[]
|
||||
}
|
||||
17
packages/core/types/src/http/locale/admin/responses.ts
Normal file
17
packages/core/types/src/http/locale/admin/responses.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PaginatedResponse } from "../../common"
|
||||
import { AdminLocale } from "./entities"
|
||||
|
||||
export interface AdminLocaleResponse {
|
||||
/**
|
||||
* The locale's details.
|
||||
*/
|
||||
locale: AdminLocale
|
||||
}
|
||||
|
||||
export interface AdminLocaleListResponse
|
||||
extends PaginatedResponse<{
|
||||
/**
|
||||
* The list of locales.
|
||||
*/
|
||||
locales: AdminLocale[]
|
||||
}> {}
|
||||
28
packages/core/types/src/http/locale/common.ts
Normal file
28
packages/core/types/src/http/locale/common.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface BaseLocale {
|
||||
/**
|
||||
* The locale's code.
|
||||
*
|
||||
* @example
|
||||
* en-US
|
||||
*/
|
||||
code: string
|
||||
/**
|
||||
* The locale's name.
|
||||
*
|
||||
* @example
|
||||
* English (United States)
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* The date the locale was created.
|
||||
*/
|
||||
created_at: string
|
||||
/**
|
||||
* The date the locale was updated.
|
||||
*/
|
||||
updated_at: string
|
||||
/**
|
||||
* The date the locale was deleted.
|
||||
*/
|
||||
deleted_at: string | null
|
||||
}
|
||||
2
packages/core/types/src/http/locale/index.ts
Normal file
2
packages/core/types/src/http/locale/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./admin"
|
||||
export * from "./common"
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AdminCurrency } from "../../currency"
|
||||
import { AdminLocale } from "../../locale"
|
||||
|
||||
export interface AdminStoreCurrency {
|
||||
/**
|
||||
@@ -7,7 +8,7 @@ export interface AdminStoreCurrency {
|
||||
id: string
|
||||
/**
|
||||
* The currency code.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* "usd"
|
||||
*/
|
||||
@@ -38,6 +39,44 @@ export interface AdminStoreCurrency {
|
||||
deleted_at: string | null
|
||||
}
|
||||
|
||||
export interface AdminStoreLocale {
|
||||
/**
|
||||
* The locale's ID.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* The locale's code.
|
||||
*
|
||||
* @example
|
||||
* "en-US"
|
||||
*/
|
||||
locale_code: string
|
||||
/**
|
||||
* The ID of the store that the locale belongs to.
|
||||
*/
|
||||
store_id: string
|
||||
/**
|
||||
* Whether the locale is the default locale for the store.
|
||||
*/
|
||||
is_default: boolean
|
||||
/**
|
||||
* The locale's details.
|
||||
*/
|
||||
locale: AdminLocale
|
||||
/**
|
||||
* The date the locale was created.
|
||||
*/
|
||||
created_at: string
|
||||
/**
|
||||
* The date the locale was updated.
|
||||
*/
|
||||
updated_at: string
|
||||
/**
|
||||
* The date the locale was deleted.
|
||||
*/
|
||||
deleted_at: string | null
|
||||
}
|
||||
|
||||
export interface AdminStore {
|
||||
/**
|
||||
* The store's ID.
|
||||
@@ -51,6 +90,10 @@ export interface AdminStore {
|
||||
* The store's supported currencies.
|
||||
*/
|
||||
supported_currencies: AdminStoreCurrency[]
|
||||
/**
|
||||
* The store's supported locales.
|
||||
*/
|
||||
supported_locales: AdminStoreLocale[]
|
||||
/**
|
||||
* The store's default sales channel ID.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export interface AdminUpdateStoreSupportedCurrency {
|
||||
/**
|
||||
* The currency's ISO 3 code.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* usd
|
||||
*/
|
||||
@@ -15,12 +15,23 @@ export interface AdminUpdateStoreSupportedCurrency {
|
||||
is_default?: boolean
|
||||
/**
|
||||
* Whether prices in this currency are tax inclusive.
|
||||
*
|
||||
*
|
||||
* Learn more in [this documentation](https://docs.medusajs.com/resources/commerce-modules/pricing/tax-inclusive-pricing).
|
||||
*/
|
||||
is_tax_inclusive?: boolean
|
||||
}
|
||||
|
||||
export interface AdminUpdateStoreSupportedLocale {
|
||||
/**
|
||||
* The locale's BCP 47 language tag.
|
||||
*/
|
||||
locale_code: string
|
||||
/**
|
||||
* Whether this locale is the default locale in the store.
|
||||
*/
|
||||
is_default?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The data to update in a store.
|
||||
*/
|
||||
@@ -33,6 +44,10 @@ export interface AdminUpdateStore {
|
||||
* The supported currencies of the store.
|
||||
*/
|
||||
supported_currencies?: AdminUpdateStoreSupportedCurrency[]
|
||||
/**
|
||||
* The supported locales of the store.
|
||||
*/
|
||||
supported_locales?: AdminUpdateStoreSupportedLocale[]
|
||||
/**
|
||||
* The ID of the default sales channel of the store.
|
||||
*/
|
||||
|
||||
41
packages/core/types/src/http/translations/admin/entities.ts
Normal file
41
packages/core/types/src/http/translations/admin/entities.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export interface AdminTranslation {
|
||||
/**
|
||||
* The ID of the translation.
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* The ID of the entity being translated.
|
||||
*/
|
||||
reference_id: string
|
||||
|
||||
/**
|
||||
* The type of entity being translated (e.g., "product", "product_variant").
|
||||
*/
|
||||
reference: string
|
||||
|
||||
/**
|
||||
* The BCP 47 language tag code for this translation (e.g., "en-US", "fr-FR").
|
||||
*/
|
||||
locale_code: string
|
||||
|
||||
/**
|
||||
* The translated fields as key-value pairs.
|
||||
*/
|
||||
translations: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* The date and time the translation was created.
|
||||
*/
|
||||
created_at: Date | string
|
||||
|
||||
/**
|
||||
* The date and time the translation was last updated.
|
||||
*/
|
||||
updated_at: Date | string
|
||||
|
||||
/**
|
||||
* The date and time the translation was deleted.
|
||||
*/
|
||||
deleted_at: Date | string | null
|
||||
}
|
||||
3
packages/core/types/src/http/translations/admin/index.ts
Normal file
3
packages/core/types/src/http/translations/admin/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./queries"
|
||||
export * from "./responses"
|
||||
export * from "./entities"
|
||||
23
packages/core/types/src/http/translations/admin/queries.ts
Normal file
23
packages/core/types/src/http/translations/admin/queries.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { BaseFilterable } from "../../.."
|
||||
import { FindParams } from "../../common/request"
|
||||
|
||||
export interface AdminTranslationsListParams
|
||||
extends FindParams,
|
||||
BaseFilterable<AdminTranslationsListParams> {
|
||||
/**
|
||||
* Query or keywords to search the translations searchable fields.
|
||||
*/
|
||||
q?: string
|
||||
/**
|
||||
* Filter by entity ID.
|
||||
*/
|
||||
reference_id?: string | string[]
|
||||
/**
|
||||
* Filter by entity type.
|
||||
*/
|
||||
reference?: string
|
||||
/**
|
||||
* Filter by locale code.
|
||||
*/
|
||||
locale_code?: string | string[]
|
||||
}
|
||||
35
packages/core/types/src/http/translations/admin/responses.ts
Normal file
35
packages/core/types/src/http/translations/admin/responses.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { PaginatedResponse } from "../../common"
|
||||
import { AdminTranslation } from "./entities"
|
||||
|
||||
export interface AdminTranslationsResponse {
|
||||
/**
|
||||
* The list of translations.
|
||||
*/
|
||||
translation: AdminTranslation
|
||||
}
|
||||
|
||||
export type AdminTranslationsListResponse = PaginatedResponse<{
|
||||
/**
|
||||
* The list of translations.
|
||||
*/
|
||||
translations: AdminTranslation[]
|
||||
}>
|
||||
|
||||
export interface AdminTranslationsBatchResponse {
|
||||
/**
|
||||
* The created translations.
|
||||
*/
|
||||
created: AdminTranslation[]
|
||||
/**
|
||||
* The updated translations.
|
||||
*/
|
||||
updated: AdminTranslation[]
|
||||
/**
|
||||
* The deleted translations.
|
||||
*/
|
||||
deleted: {
|
||||
ids: string[]
|
||||
object: "translation"
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
1
packages/core/types/src/http/translations/index.ts
Normal file
1
packages/core/types/src/http/translations/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./admin"
|
||||
@@ -42,6 +42,7 @@ export * from "./store"
|
||||
export * from "./tax"
|
||||
export * from "./totals"
|
||||
export * from "./transaction-base"
|
||||
export * from "./translation"
|
||||
export * from "./user"
|
||||
export * from "./workflow"
|
||||
export * from "./workflows"
|
||||
|
||||
@@ -31,6 +31,37 @@ export interface StoreCurrencyDTO {
|
||||
deleted_at: string | null
|
||||
}
|
||||
|
||||
export interface StoreLocaleDTO {
|
||||
/**
|
||||
* The ID of the store locale.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* The locale code of the store locale.
|
||||
*/
|
||||
locale_code: string
|
||||
/**
|
||||
* Whether the locale is the default one for the store.
|
||||
*/
|
||||
is_default: boolean
|
||||
/**
|
||||
* The store ID associated with the locale.
|
||||
*/
|
||||
store_id: string
|
||||
/**
|
||||
* The created date of the locale.
|
||||
*/
|
||||
created_at: string
|
||||
/**
|
||||
* The updated date of the locale.
|
||||
*/
|
||||
updated_at: string
|
||||
/**
|
||||
* The deleted date of the locale.
|
||||
*/
|
||||
deleted_at: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* The store details.
|
||||
*/
|
||||
@@ -50,6 +81,11 @@ export interface StoreDTO {
|
||||
*/
|
||||
supported_currencies?: StoreCurrencyDTO[]
|
||||
|
||||
/**
|
||||
* The supported locale codes of the store.
|
||||
*/
|
||||
supported_locales?: StoreLocaleDTO[]
|
||||
|
||||
/**
|
||||
* The associated default sales channel's ID.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,17 @@ export interface CreateStoreCurrencyDTO {
|
||||
is_default?: boolean
|
||||
}
|
||||
|
||||
export interface CreateStoreLocaleDTO {
|
||||
/**
|
||||
* The locale code of the store locale.
|
||||
*/
|
||||
locale_code: string
|
||||
/**
|
||||
* Whether the locale is the default one for the store.
|
||||
*/
|
||||
is_default?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The store to be created.
|
||||
*/
|
||||
@@ -23,6 +34,11 @@ export interface CreateStoreDTO {
|
||||
*/
|
||||
supported_currencies?: CreateStoreCurrencyDTO[]
|
||||
|
||||
/**
|
||||
* The suppoprted locale codes of the store.
|
||||
*/
|
||||
supported_locales?: CreateStoreLocaleDTO[]
|
||||
|
||||
/**
|
||||
* The associated default sales channel's ID.
|
||||
*/
|
||||
@@ -68,6 +84,11 @@ export interface UpdateStoreDTO {
|
||||
*/
|
||||
supported_currencies?: CreateStoreCurrencyDTO[]
|
||||
|
||||
/**
|
||||
* The supported locale codes of the store.
|
||||
*/
|
||||
supported_locales?: CreateStoreLocaleDTO[]
|
||||
|
||||
/**
|
||||
* The associated default sales channel's ID.
|
||||
*/
|
||||
|
||||
134
packages/core/types/src/translation/common.ts
Normal file
134
packages/core/types/src/translation/common.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { BaseFilterable, OperatorMap } from "../dal"
|
||||
|
||||
/**
|
||||
* The locale details.
|
||||
*/
|
||||
export interface LocaleDTO {
|
||||
/**
|
||||
* The ID of the locale.
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* The BCP 47 language tag code of the locale (e.g., "en-US", "fr-FR").
|
||||
*/
|
||||
code: string
|
||||
|
||||
/**
|
||||
* The human-readable name of the locale (e.g., "English (United States)").
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* The date and time the locale was created.
|
||||
*/
|
||||
created_at: Date | string
|
||||
|
||||
/**
|
||||
* The date and time the locale was last updated.
|
||||
*/
|
||||
updated_at: Date | string
|
||||
|
||||
/**
|
||||
* The date and time the locale was deleted.
|
||||
*/
|
||||
deleted_at: Date | string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* The translation details.
|
||||
*/
|
||||
export interface TranslationDTO {
|
||||
/**
|
||||
* The ID of the translation.
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* The ID of the entity being translated.
|
||||
*/
|
||||
reference_id: string
|
||||
|
||||
/**
|
||||
* The type of entity being translated (e.g., "product", "product_variant").
|
||||
*/
|
||||
reference: string
|
||||
|
||||
/**
|
||||
* The BCP 47 language tag code for this translation (e.g., "en-US", "fr-FR").
|
||||
*/
|
||||
locale_code: string
|
||||
|
||||
/**
|
||||
* The translated fields as key-value pairs.
|
||||
*/
|
||||
translations: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* The date and time the translation was created.
|
||||
*/
|
||||
created_at: Date | string
|
||||
|
||||
/**
|
||||
* The date and time the translation was last updated.
|
||||
*/
|
||||
updated_at: Date | string
|
||||
|
||||
/**
|
||||
* The date and time the translation was deleted.
|
||||
*/
|
||||
deleted_at: Date | string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* The filters to apply on the retrieved locales.
|
||||
*/
|
||||
export interface FilterableLocaleProps
|
||||
extends BaseFilterable<FilterableLocaleProps> {
|
||||
/**
|
||||
* The IDs to filter the locales by.
|
||||
*/
|
||||
id?: string[] | string | OperatorMap<string | string[]>
|
||||
|
||||
/**
|
||||
* Filter locales by their code.
|
||||
*/
|
||||
code?: string | string[] | OperatorMap<string>
|
||||
|
||||
/**
|
||||
* Filter locales by their name.
|
||||
*/
|
||||
name?: string | OperatorMap<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* The filters to apply on the retrieved translations.
|
||||
*/
|
||||
export interface FilterableTranslationProps
|
||||
extends BaseFilterable<FilterableTranslationProps> {
|
||||
/**
|
||||
* Search through translated content using this search term.
|
||||
* This searches within the JSONB translations field values.
|
||||
*/
|
||||
q?: string
|
||||
|
||||
/**
|
||||
* The IDs to filter the translations by.
|
||||
*/
|
||||
id?: string[] | string | OperatorMap<string | string[]>
|
||||
|
||||
/**
|
||||
* Filter translations by entity ID.
|
||||
*/
|
||||
reference_id?: string | string[] | OperatorMap<string>
|
||||
|
||||
/**
|
||||
* Filter translations by entity type.
|
||||
*/
|
||||
reference?: string | string[] | OperatorMap<string>
|
||||
|
||||
/**
|
||||
* Filter translations by locale code.
|
||||
*/
|
||||
locale_code?: string | string[] | OperatorMap<string>
|
||||
}
|
||||
3
packages/core/types/src/translation/index.ts
Normal file
3
packages/core/types/src/translation/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./common"
|
||||
export * from "./mutations"
|
||||
export * from "./service"
|
||||
144
packages/core/types/src/translation/mutations.ts
Normal file
144
packages/core/types/src/translation/mutations.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* The locale to be created.
|
||||
*/
|
||||
export interface CreateLocaleDTO {
|
||||
/**
|
||||
* The ID of the locale to create.
|
||||
*/
|
||||
id?: string
|
||||
|
||||
/**
|
||||
* The BCP 47 language tag code of the locale (e.g., "en-US", "fr-FR").
|
||||
*/
|
||||
code: string
|
||||
|
||||
/**
|
||||
* The human-readable name of the locale (e.g., "English (United States)").
|
||||
*/
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes to update in the locale.
|
||||
*/
|
||||
export interface UpdateLocaleDTO {
|
||||
/**
|
||||
* The ID of the locale to update.
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* The BCP 47 language tag code of the locale.
|
||||
*/
|
||||
code?: string
|
||||
|
||||
/**
|
||||
* The human-readable name of the locale.
|
||||
*/
|
||||
name?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes in the locale to be created or updated.
|
||||
*/
|
||||
export interface UpsertLocaleDTO {
|
||||
/**
|
||||
* The ID of the locale in case of an update.
|
||||
*/
|
||||
id?: string
|
||||
|
||||
/**
|
||||
* The BCP 47 language tag code of the locale.
|
||||
*/
|
||||
code?: string
|
||||
|
||||
/**
|
||||
* The human-readable name of the locale.
|
||||
*/
|
||||
name?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The translation to be created.
|
||||
*/
|
||||
export interface CreateTranslationDTO {
|
||||
/**
|
||||
* The ID of the entity being translated.
|
||||
*/
|
||||
reference_id: string
|
||||
|
||||
/**
|
||||
* The type of entity being translated (e.g., "product", "product_variant").
|
||||
*/
|
||||
reference: string
|
||||
|
||||
/**
|
||||
* The BCP 47 language tag code for this translation (e.g., "en-US", "fr-FR").
|
||||
*/
|
||||
locale_code: string
|
||||
|
||||
/**
|
||||
* The translated fields as key-value pairs.
|
||||
*/
|
||||
translations: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes to update in the translation.
|
||||
*/
|
||||
export interface UpdateTranslationDTO {
|
||||
/**
|
||||
* The ID of the translation to update.
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* The ID of the entity being translated.
|
||||
*/
|
||||
reference_id?: string
|
||||
|
||||
/**
|
||||
* The type of entity being translated.
|
||||
*/
|
||||
reference?: string
|
||||
|
||||
/**
|
||||
* The BCP 47 language tag code for this translation.
|
||||
*/
|
||||
locale_code?: string
|
||||
|
||||
/**
|
||||
* The translated fields as key-value pairs.
|
||||
*/
|
||||
translations?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes in the translation to be created or updated.
|
||||
*/
|
||||
export interface UpsertTranslationDTO {
|
||||
/**
|
||||
* The ID of the translation in case of an update.
|
||||
*/
|
||||
id?: string
|
||||
|
||||
/**
|
||||
* The ID of the entity being translated.
|
||||
*/
|
||||
reference_id?: string
|
||||
|
||||
/**
|
||||
* The type of entity being translated.
|
||||
*/
|
||||
reference?: string
|
||||
|
||||
/**
|
||||
* The BCP 47 language tag code for this translation.
|
||||
*/
|
||||
locale_code?: string
|
||||
|
||||
/**
|
||||
* The translated fields as key-value pairs.
|
||||
*/
|
||||
translations?: Record<string, unknown>
|
||||
}
|
||||
292
packages/core/types/src/translation/service.ts
Normal file
292
packages/core/types/src/translation/service.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { FindConfig } from "../common"
|
||||
import { RestoreReturn, SoftDeleteReturn } from "../dal"
|
||||
import { IModuleService } from "../modules-sdk"
|
||||
import { Context } from "../shared-context"
|
||||
import {
|
||||
FilterableLocaleProps,
|
||||
FilterableTranslationProps,
|
||||
LocaleDTO,
|
||||
TranslationDTO,
|
||||
} from "./common"
|
||||
import {
|
||||
CreateLocaleDTO,
|
||||
CreateTranslationDTO,
|
||||
UpdateLocaleDTO,
|
||||
UpdateTranslationDTO,
|
||||
} from "./mutations"
|
||||
|
||||
/**
|
||||
* The main service interface for the Translation Module.
|
||||
* Method signatures match what MedusaService generates.
|
||||
*/
|
||||
export interface ITranslationModuleService extends IModuleService {
|
||||
/**
|
||||
* This method retrieves a locale by its ID.
|
||||
*
|
||||
* @param {string} id - The ID of the locale.
|
||||
* @param {FindConfig<LocaleDTO>} config - The configurations determining how the locale is retrieved.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<LocaleDTO>} The retrieved locale.
|
||||
*/
|
||||
retrieveLocale(
|
||||
id: string,
|
||||
config?: FindConfig<LocaleDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<LocaleDTO>
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of locales based on optional filters and configuration.
|
||||
*
|
||||
* @param {FilterableLocaleProps} filters - The filters to apply on the retrieved locales.
|
||||
* @param {FindConfig<LocaleDTO>} config - The configurations determining how the locale is retrieved.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<LocaleDTO[]>} The list of locales.
|
||||
*/
|
||||
listLocales(
|
||||
filters?: FilterableLocaleProps,
|
||||
config?: FindConfig<LocaleDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<LocaleDTO[]>
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of locales along with the total count.
|
||||
*
|
||||
* @param {FilterableLocaleProps} filters - The filters to apply on the retrieved locales.
|
||||
* @param {FindConfig<LocaleDTO>} config - The configurations determining how the locale is retrieved.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<[LocaleDTO[], number]>} The list of locales along with their total count.
|
||||
*/
|
||||
listAndCountLocales(
|
||||
filters?: FilterableLocaleProps,
|
||||
config?: FindConfig<LocaleDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[LocaleDTO[], number]>
|
||||
|
||||
/**
|
||||
* This method creates a locale.
|
||||
*
|
||||
* @param {CreateLocaleDTO} data - The locale to be created.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<LocaleDTO>} The created locale.
|
||||
*/
|
||||
createLocales(
|
||||
data: CreateLocaleDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<LocaleDTO>
|
||||
|
||||
/**
|
||||
* This method creates locales.
|
||||
*
|
||||
* @param {CreateLocaleDTO[]} data - The locales to be created.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<LocaleDTO[]>} The created locales.
|
||||
*/
|
||||
createLocales(
|
||||
data: CreateLocaleDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<LocaleDTO[]>
|
||||
|
||||
/**
|
||||
* This method updates an existing locale. The ID should be included in the data object.
|
||||
*
|
||||
* @param {UpdateLocaleDTO} data - The attributes to update in the locale (including id).
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<LocaleDTO>} The updated locale.
|
||||
*/
|
||||
updateLocales(
|
||||
data: UpdateLocaleDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<LocaleDTO>
|
||||
|
||||
/**
|
||||
* This method updates existing locales using an array or selector-based approach.
|
||||
*
|
||||
* @param {UpdateLocaleDTO[] | { selector: Record<string, any>; data: UpdateLocaleDTO | UpdateLocaleDTO[] }} dataOrOptions - The data or options for bulk update.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<LocaleDTO[]>} The updated locales.
|
||||
*/
|
||||
updateLocales(
|
||||
dataOrOptions:
|
||||
| UpdateLocaleDTO[]
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data: UpdateLocaleDTO | UpdateLocaleDTO[]
|
||||
},
|
||||
sharedContext?: Context
|
||||
): Promise<LocaleDTO[]>
|
||||
|
||||
/**
|
||||
* This method deletes locales by their IDs or objects.
|
||||
*
|
||||
* @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects identifying the locales to delete.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<void>} Resolves when the locales are deleted.
|
||||
*/
|
||||
deleteLocales(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
|
||||
/**
|
||||
* This method soft deletes locales by their IDs or objects.
|
||||
*
|
||||
* @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects identifying the locales to soft delete.
|
||||
* @param {SoftDeleteReturn<TReturnableLinkableKeys>} config - An object for related entities that should be soft-deleted.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<Record<string, string[]> | void>} An object with IDs of related records that were also soft deleted.
|
||||
*/
|
||||
softDeleteLocales<TReturnableLinkableKeys extends string = string>(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
|
||||
/**
|
||||
* This method restores soft deleted locales by their IDs or objects.
|
||||
*
|
||||
* @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects identifying the locales to restore.
|
||||
* @param {RestoreReturn<TReturnableLinkableKeys>} config - Configurations determining which relations to restore.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<Record<string, string[]> | void>} An object with IDs of related records that were restored.
|
||||
*/
|
||||
restoreLocales<TReturnableLinkableKeys extends string = string>(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
|
||||
/**
|
||||
* This method retrieves a translation by its ID.
|
||||
*
|
||||
* @param {string} id - The ID of the translation.
|
||||
* @param {FindConfig<TranslationDTO>} config - The configurations determining how the translation is retrieved.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<TranslationDTO>} The retrieved translation.
|
||||
*/
|
||||
retrieveTranslation(
|
||||
id: string,
|
||||
config?: FindConfig<TranslationDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<TranslationDTO>
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of translations based on optional filters and configuration.
|
||||
*
|
||||
* @param {FilterableTranslationProps} filters - The filters to apply on the retrieved translations.
|
||||
* @param {FindConfig<TranslationDTO>} config - The configurations determining how the translation is retrieved.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<TranslationDTO[]>} The list of translations.
|
||||
*/
|
||||
listTranslations(
|
||||
filters?: FilterableTranslationProps,
|
||||
config?: FindConfig<TranslationDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<TranslationDTO[]>
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of translations along with the total count.
|
||||
*
|
||||
* @param {FilterableTranslationProps} filters - The filters to apply on the retrieved translations.
|
||||
* @param {FindConfig<TranslationDTO>} config - The configurations determining how the translation is retrieved.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<[TranslationDTO[], number]>} The list of translations along with their total count.
|
||||
*/
|
||||
listAndCountTranslations(
|
||||
filters?: FilterableTranslationProps,
|
||||
config?: FindConfig<TranslationDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[TranslationDTO[], number]>
|
||||
|
||||
/**
|
||||
* This method creates a translation.
|
||||
*
|
||||
* @param {CreateTranslationDTO} data - The translation to be created.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<TranslationDTO>} The created translation.
|
||||
*/
|
||||
createTranslations(
|
||||
data: CreateTranslationDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<TranslationDTO>
|
||||
|
||||
/**
|
||||
* This method creates translations.
|
||||
*
|
||||
* @param {CreateTranslationDTO[]} data - The translations to be created.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<TranslationDTO[]>} The created translations.
|
||||
*/
|
||||
createTranslations(
|
||||
data: CreateTranslationDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<TranslationDTO[]>
|
||||
|
||||
/**
|
||||
* This method updates an existing translation. The ID should be included in the data object.
|
||||
*
|
||||
* @param {UpdateTranslationDTO} data - The attributes to update in the translation (including id).
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<TranslationDTO>} The updated translation.
|
||||
*/
|
||||
updateTranslations(
|
||||
data: UpdateTranslationDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<TranslationDTO>
|
||||
|
||||
/**
|
||||
* This method updates existing translations using an array or selector-based approach.
|
||||
*
|
||||
* @param {UpdateTranslationDTO[] | { selector: Record<string, any>; data: UpdateTranslationDTO | UpdateTranslationDTO[] }} dataOrOptions - The data or options for bulk update.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<TranslationDTO[]>} The updated translations.
|
||||
*/
|
||||
updateTranslations(
|
||||
dataOrOptions:
|
||||
| UpdateTranslationDTO[]
|
||||
| {
|
||||
selector: Record<string, any>
|
||||
data: UpdateTranslationDTO | UpdateTranslationDTO[]
|
||||
},
|
||||
sharedContext?: Context
|
||||
): Promise<TranslationDTO[]>
|
||||
|
||||
/**
|
||||
* This method deletes translations by their IDs or objects.
|
||||
*
|
||||
* @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects identifying the translations to delete.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<void>} Resolves when the translations are deleted.
|
||||
*/
|
||||
deleteTranslations(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
|
||||
/**
|
||||
* This method soft deletes translations by their IDs or objects.
|
||||
*
|
||||
* @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects identifying the translations to soft delete.
|
||||
* @param {SoftDeleteReturn<TReturnableLinkableKeys>} config - An object for related entities that should be soft-deleted.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<Record<string, string[]> | void>} An object with IDs of related records that were also soft deleted.
|
||||
*/
|
||||
softDeleteTranslations<TReturnableLinkableKeys extends string = string>(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
|
||||
/**
|
||||
* This method restores soft deleted translations by their IDs or objects.
|
||||
*
|
||||
* @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects identifying the translations to restore.
|
||||
* @param {RestoreReturn<TReturnableLinkableKeys>} config - Configurations determining which relations to restore.
|
||||
* @param {Context} sharedContext
|
||||
* @returns {Promise<Record<string, string[]> | void>} An object with IDs of related records that were restored.
|
||||
*/
|
||||
restoreTranslations<TReturnableLinkableKeys extends string = string>(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
}
|
||||
@@ -19,4 +19,5 @@ export * as SearchUtils from "./search"
|
||||
export * as ShippingProfileUtils from "./shipping"
|
||||
export * as UserUtils from "./user"
|
||||
export * as CachingUtils from "./caching"
|
||||
export * as TranslationsUtils from "./translations"
|
||||
export * as DevServerUtils from "./dev-server"
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { normalizeLocale } from "../normalize-locale"
|
||||
|
||||
describe("normalizeLocale", function () {
|
||||
it("should normalize single segment locales to lowercase", function () {
|
||||
const expectations = [
|
||||
{
|
||||
input: "eN",
|
||||
output: "en",
|
||||
},
|
||||
{
|
||||
input: "EN",
|
||||
output: "en",
|
||||
},
|
||||
{
|
||||
input: "En",
|
||||
output: "en",
|
||||
},
|
||||
{
|
||||
input: "en",
|
||||
output: "en",
|
||||
},
|
||||
{
|
||||
input: "fr",
|
||||
output: "fr",
|
||||
},
|
||||
{
|
||||
input: "FR",
|
||||
output: "fr",
|
||||
},
|
||||
{
|
||||
input: "de",
|
||||
output: "de",
|
||||
},
|
||||
]
|
||||
|
||||
expectations.forEach((expectation) => {
|
||||
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
|
||||
})
|
||||
})
|
||||
|
||||
it("should normalize two segment locales (language-region)", function () {
|
||||
const expectations = [
|
||||
{
|
||||
input: "en-Us",
|
||||
output: "en-US",
|
||||
},
|
||||
{
|
||||
input: "EN-US",
|
||||
output: "en-US",
|
||||
},
|
||||
{
|
||||
input: "en-us",
|
||||
output: "en-US",
|
||||
},
|
||||
{
|
||||
input: "En-Us",
|
||||
output: "en-US",
|
||||
},
|
||||
{
|
||||
input: "fr-FR",
|
||||
output: "fr-FR",
|
||||
},
|
||||
{
|
||||
input: "FR-fr",
|
||||
output: "fr-FR",
|
||||
},
|
||||
{
|
||||
input: "de-DE",
|
||||
output: "de-DE",
|
||||
},
|
||||
{
|
||||
input: "es-ES",
|
||||
output: "es-ES",
|
||||
},
|
||||
{
|
||||
input: "pt-BR",
|
||||
output: "pt-BR",
|
||||
},
|
||||
]
|
||||
|
||||
expectations.forEach((expectation) => {
|
||||
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
|
||||
})
|
||||
})
|
||||
|
||||
it("should normalize three segment locales (language-script-region)", function () {
|
||||
const expectations = [
|
||||
{
|
||||
input: "RU-cYrl-By",
|
||||
output: "ru-Cyrl-BY",
|
||||
},
|
||||
{
|
||||
input: "ru-cyrl-by",
|
||||
output: "ru-Cyrl-BY",
|
||||
},
|
||||
{
|
||||
input: "RU-CYRL-BY",
|
||||
output: "ru-Cyrl-BY",
|
||||
},
|
||||
{
|
||||
input: "zh-Hans-CN",
|
||||
output: "zh-Hans-CN",
|
||||
},
|
||||
{
|
||||
input: "ZH-HANS-CN",
|
||||
output: "zh-Hans-CN",
|
||||
},
|
||||
{
|
||||
input: "sr-Latn-RS",
|
||||
output: "sr-Latn-RS",
|
||||
},
|
||||
{
|
||||
input: "SR-LATN-RS",
|
||||
output: "sr-Latn-RS",
|
||||
},
|
||||
]
|
||||
|
||||
expectations.forEach((expectation) => {
|
||||
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
|
||||
})
|
||||
})
|
||||
|
||||
it("should return locale as-is for more than three segments", function () {
|
||||
const expectations = [
|
||||
{
|
||||
input: "en-US-x-private",
|
||||
output: "en-US-x-private",
|
||||
},
|
||||
{
|
||||
input: "en-US-x-private-extended",
|
||||
output: "en-US-x-private-extended",
|
||||
},
|
||||
{
|
||||
input: "en-US-x-private-extended-more",
|
||||
output: "en-US-x-private-extended-more",
|
||||
},
|
||||
]
|
||||
|
||||
expectations.forEach((expectation) => {
|
||||
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle edge cases", function () {
|
||||
const expectations = [
|
||||
{
|
||||
input: "",
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
input: "a",
|
||||
output: "a",
|
||||
},
|
||||
{
|
||||
input: "A",
|
||||
output: "a",
|
||||
},
|
||||
{
|
||||
input: "a-B",
|
||||
output: "a-B",
|
||||
},
|
||||
{
|
||||
input: "a-b-C",
|
||||
output: "a-B-C",
|
||||
},
|
||||
]
|
||||
|
||||
expectations.forEach((expectation) => {
|
||||
expect(normalizeLocale(expectation.input)).toEqual(expectation.output)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -185,6 +185,9 @@ function resolveModules(
|
||||
{ resolve: MODULE_PACKAGE_NAMES[Modules.ORDER] },
|
||||
{ resolve: MODULE_PACKAGE_NAMES[Modules.SETTINGS] },
|
||||
|
||||
// TODO: re-enable this once we have the final release
|
||||
// { resolve: MODULE_PACKAGE_NAMES[Modules.TRANSLATION] },
|
||||
|
||||
{
|
||||
resolve: MODULE_PACKAGE_NAMES[Modules.AUTH],
|
||||
options: {
|
||||
|
||||
@@ -53,6 +53,7 @@ export * from "./medusa-container"
|
||||
export * from "./merge-metadata"
|
||||
export * from "./merge-plugin-modules"
|
||||
export * from "./normalize-csv-value"
|
||||
export * from "./normalize-locale"
|
||||
export * from "./normalize-import-path-with-source"
|
||||
export * from "./object-from-string-path"
|
||||
export * from "./object-to-string-path"
|
||||
|
||||
40
packages/core/utils/src/common/normalize-locale.ts
Normal file
40
packages/core/utils/src/common/normalize-locale.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { upperCaseFirst } from "./upper-case-first"
|
||||
|
||||
/**
|
||||
* Normalizes a locale string to {@link https://developer.mozilla.org/en-US/docs/Glossary/BCP_47_language_tag|BCP 47 language tag format}
|
||||
* @param locale - The locale string to normalize
|
||||
* @returns The normalized locale string
|
||||
*
|
||||
* @example
|
||||
* input: "en-Us"
|
||||
* output: "en-US"
|
||||
*
|
||||
* @example
|
||||
* input: "eN"
|
||||
* output: "en"
|
||||
*
|
||||
* @example
|
||||
* input: "RU-cYrl-By"
|
||||
* output: "ru-Cyrl-BY"
|
||||
*/
|
||||
export function normalizeLocale(locale: string) {
|
||||
const segments = locale.split("-")
|
||||
|
||||
if (segments.length === 1) {
|
||||
return segments[0].toLowerCase()
|
||||
}
|
||||
|
||||
// e.g en-US
|
||||
if (segments.length === 2) {
|
||||
return `${segments[0].toLowerCase()}-${segments[1].toUpperCase()}`
|
||||
}
|
||||
|
||||
// e.g ru-Cyrl-BY
|
||||
if (segments.length === 3) {
|
||||
return `${segments[0].toLowerCase()}-${upperCaseFirst(
|
||||
segments[1].toLowerCase()
|
||||
)}-${segments[2].toUpperCase()}`
|
||||
}
|
||||
|
||||
return locale
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export * from "./totals"
|
||||
export * from "./totals/big-number"
|
||||
export * from "./user"
|
||||
export * from "./caching"
|
||||
export * from "./translations"
|
||||
export * from "./dev-server"
|
||||
|
||||
export const MedusaModuleType = Symbol.for("MedusaModule")
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { LinkModulesExtraFields, ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { camelToSnakeCase, isObject, pluralize, toPascalCase } from "../common"
|
||||
import {
|
||||
camelToSnakeCase,
|
||||
getCallerFilePath,
|
||||
isFileDisabled,
|
||||
isObject,
|
||||
MEDUSA_SKIP_FILE,
|
||||
pluralize,
|
||||
toPascalCase,
|
||||
} from "../common"
|
||||
import { composeLinkName } from "../link/compose-link-name"
|
||||
|
||||
export const DefineLinkSymbol = Symbol.for("DefineLink")
|
||||
@@ -193,6 +201,11 @@ export function defineLink(
|
||||
rightService: DefineLinkInputSource | DefineReadOnlyLinkInputSource,
|
||||
linkServiceOptions?: ExtraOptions | ReadOnlyExtraOptions
|
||||
): DefineLinkExport {
|
||||
const callerFilePath = getCallerFilePath()
|
||||
if (isFileDisabled(callerFilePath ?? "")) {
|
||||
return { [MEDUSA_SKIP_FILE]: true } as any
|
||||
}
|
||||
|
||||
const serviceAObj = prepareServiceConfig(leftService)
|
||||
const serviceBObj = prepareServiceConfig(rightService)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export const Modules = {
|
||||
LOCKING: "locking",
|
||||
SETTINGS: "settings",
|
||||
CACHING: "caching",
|
||||
TRANSLATION: "translation",
|
||||
} as const
|
||||
|
||||
export const MODULE_PACKAGE_NAMES = {
|
||||
@@ -60,6 +61,7 @@ export const MODULE_PACKAGE_NAMES = {
|
||||
[Modules.LOCKING]: "@medusajs/medusa/locking",
|
||||
[Modules.SETTINGS]: "@medusajs/medusa/settings",
|
||||
[Modules.CACHING]: "@medusajs/caching",
|
||||
[Modules.TRANSLATION]: "@medusajs/translation",
|
||||
}
|
||||
|
||||
export const REVERSED_MODULE_PACKAGE_NAMES = Object.entries(
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
import { FeatureFlag } from "../../feature-flags"
|
||||
import { applyTranslations } from "../apply-translations"
|
||||
|
||||
jest.mock("../../feature-flags/flag-router", () => ({
|
||||
...jest.requireActual("../../feature-flags/flag-router"),
|
||||
FeatureFlag: {
|
||||
isFeatureEnabled: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mockFeatureFlagIsEnabled = FeatureFlag.isFeatureEnabled as jest.Mock
|
||||
|
||||
describe("applyTranslations", () => {
|
||||
let mockQuery: { graph: jest.Mock }
|
||||
let mockContainer: { resolve: jest.Mock }
|
||||
let mockReq: { locale?: string }
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockQuery = {
|
||||
graph: jest.fn().mockResolvedValue({ data: [] }),
|
||||
}
|
||||
mockContainer = {
|
||||
resolve: jest.fn().mockReturnValue(mockQuery),
|
||||
}
|
||||
mockReq = {
|
||||
locale: "en-US",
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
mockFeatureFlagIsEnabled.mockReturnValue(true)
|
||||
})
|
||||
|
||||
it("should apply translations to a simple object", async () => {
|
||||
const inputObjects = [{ id: "prod_1", title: "Original Title" }]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
reference_id: "prod_1",
|
||||
translations: { title: "Translated Title" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(inputObjects[0].title).toBe("Translated Title")
|
||||
})
|
||||
|
||||
it("should apply translations to nested objects", async () => {
|
||||
const inputObjects = [
|
||||
{
|
||||
id: "prod_1",
|
||||
title: "Product Title",
|
||||
category: {
|
||||
id: "cat_1",
|
||||
name: "Category Name",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
reference_id: "prod_1",
|
||||
translations: { title: "Translated Product Title", category: true },
|
||||
},
|
||||
{
|
||||
reference_id: "cat_1",
|
||||
translations: { name: "Translated Category Name" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(inputObjects[0].title).toBe("Translated Product Title")
|
||||
expect(inputObjects[0].category.name).toBe("Translated Category Name")
|
||||
})
|
||||
|
||||
it("should apply translations to arrays of objects", async () => {
|
||||
const inputObjects = [
|
||||
{
|
||||
id: "prod_1",
|
||||
title: "Product Title",
|
||||
variants: [
|
||||
{ id: "var_1", name: "Variant 1" },
|
||||
{ id: "var_2", name: "Variant 2" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
reference_id: "prod_1",
|
||||
translations: { title: "Translated Product" },
|
||||
},
|
||||
{
|
||||
reference_id: "var_1",
|
||||
translations: { name: "Translated Variant 1" },
|
||||
},
|
||||
{
|
||||
reference_id: "var_2",
|
||||
translations: { name: "Translated Variant 2" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(inputObjects[0].title).toBe("Translated Product")
|
||||
expect(inputObjects[0].variants[0].name).toBe("Translated Variant 1")
|
||||
expect(inputObjects[0].variants[1].name).toBe("Translated Variant 2")
|
||||
})
|
||||
|
||||
it("should use the locale from the request", async () => {
|
||||
mockReq.locale = "fr-FR"
|
||||
const inputObjects = [{ id: "prod_1", title: "Original" }]
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(mockQuery.graph).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
locale_code: "fr-FR",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
cache: expect.objectContaining({
|
||||
enable: true,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should batch queries when there are more than 250 ids", async () => {
|
||||
const inputObjects = Array.from({ length: 300 }, (_, i) => ({
|
||||
id: `prod_${i}`,
|
||||
title: `Product ${i}`,
|
||||
}))
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(mockQuery.graph).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("should apply translations to multiple input objects", async () => {
|
||||
const inputObjects = [
|
||||
{ id: "prod_1", title: "Product 1" },
|
||||
{ id: "prod_2", title: "Product 2" },
|
||||
]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({
|
||||
data: [
|
||||
{ reference_id: "prod_1", translations: { title: "Translated 1" } },
|
||||
{ reference_id: "prod_2", translations: { title: "Translated 2" } },
|
||||
],
|
||||
})
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(inputObjects[0].title).toBe("Translated 1")
|
||||
expect(inputObjects[1].title).toBe("Translated 2")
|
||||
})
|
||||
|
||||
it("should handle translations with null values", async () => {
|
||||
const inputObjects = [{ id: "prod_1", title: "Original" }]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
reference_id: "prod_1",
|
||||
translations: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(inputObjects[0].title).toBe("Original")
|
||||
})
|
||||
|
||||
it("should return early when feature flag is disabled", async () => {
|
||||
mockFeatureFlagIsEnabled.mockReturnValue(false)
|
||||
const inputObjects = [{ id: "prod_1", title: "Original" }]
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(mockContainer.resolve).not.toHaveBeenCalled()
|
||||
expect(inputObjects[0].title).toBe("Original")
|
||||
})
|
||||
|
||||
it("should not modify objects when no translations are found", async () => {
|
||||
mockFeatureFlagIsEnabled.mockReturnValue(true)
|
||||
const inputObjects = [{ id: "prod_1", title: "Original Title" }]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({ data: [] })
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(inputObjects[0].title).toBe("Original Title")
|
||||
})
|
||||
|
||||
it("should handle empty input array without errors", async () => {
|
||||
mockFeatureFlagIsEnabled.mockReturnValue(true)
|
||||
const inputObjects: Record<string, any>[] = []
|
||||
|
||||
await expect(
|
||||
applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it("should not modify properties that do not exist in the object", async () => {
|
||||
mockFeatureFlagIsEnabled.mockReturnValue(true)
|
||||
const inputObjects = [{ id: "prod_1", title: "Original" }]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
reference_id: "prod_1",
|
||||
translations: { description: "Translated Description" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(inputObjects[0].title).toBe("Original")
|
||||
expect(inputObjects[0]).not.toHaveProperty("description")
|
||||
})
|
||||
|
||||
it("should handle objects with undefined id gracefully", async () => {
|
||||
mockFeatureFlagIsEnabled.mockReturnValue(true)
|
||||
const inputObjects = [{ id: undefined, title: "Original" }]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({ data: [] })
|
||||
|
||||
await expect(
|
||||
applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects as any,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it("should only apply translations to matching keys", async () => {
|
||||
mockFeatureFlagIsEnabled.mockReturnValue(true)
|
||||
const inputObjects = [
|
||||
{ id: "prod_1", title: "Original Title", handle: "original-handle" },
|
||||
]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
reference_id: "prod_1",
|
||||
translations: { title: "Translated Title" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(inputObjects[0].title).toBe("Translated Title")
|
||||
expect(inputObjects[0].handle).toBe("original-handle")
|
||||
})
|
||||
|
||||
it("should handle deeply nested structures", async () => {
|
||||
mockFeatureFlagIsEnabled.mockReturnValue(true)
|
||||
const inputObjects = [
|
||||
{
|
||||
id: "prod_1",
|
||||
title: "Product",
|
||||
category: {
|
||||
id: "cat_1",
|
||||
name: "Category",
|
||||
parent: {
|
||||
id: "cat_parent",
|
||||
name: "Parent Category",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
mockQuery.graph.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
reference_id: "prod_1",
|
||||
translations: { title: "Translated Product" },
|
||||
},
|
||||
{
|
||||
reference_id: "cat_1",
|
||||
translations: { name: "Translated Category" },
|
||||
},
|
||||
{
|
||||
reference_id: "cat_parent",
|
||||
translations: { name: "Translated Parent" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await applyTranslations({
|
||||
localeCode: mockReq.locale as string,
|
||||
objects: inputObjects,
|
||||
container: mockContainer as any,
|
||||
})
|
||||
|
||||
expect(inputObjects[0].title).toBe("Translated Product")
|
||||
expect(inputObjects[0].category.name).toBe("Translated Category")
|
||||
expect(inputObjects[0].category.parent.name).toBe("Translated Parent")
|
||||
})
|
||||
})
|
||||
139
packages/core/utils/src/translations/apply-translations.ts
Normal file
139
packages/core/utils/src/translations/apply-translations.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { MedusaContainer, RemoteQueryFunction } from "@medusajs/types"
|
||||
import { ContainerRegistrationKeys } from "../common/container"
|
||||
import { isObject } from "../common/is-object"
|
||||
import { FeatureFlag } from "../feature-flags/flag-router"
|
||||
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"metadata",
|
||||
]
|
||||
|
||||
function canApplyTranslationTo(object: Record<string, any>) {
|
||||
return "id" in object && !!object.id
|
||||
}
|
||||
|
||||
function gatherIds(object: Record<string, any>, gatheredIds: Set<string>) {
|
||||
gatheredIds.add(object.id)
|
||||
Object.entries(object).forEach(([, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => item && gatherIds(item, gatheredIds))
|
||||
} else if (isObject(value)) {
|
||||
gatherIds(value, gatheredIds)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function applyTranslation(
|
||||
object: Record<string, any>,
|
||||
entityIdToTranslation: Map<string, Record<string, any>>
|
||||
) {
|
||||
const translation = entityIdToTranslation.get(object.id)
|
||||
const hasTranslation = !!translation
|
||||
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
if (excludedKeys.includes(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hasTranslation) {
|
||||
if (
|
||||
key in translation &&
|
||||
typeof object[key] === typeof translation[key]
|
||||
) {
|
||||
object[key] = translation[key]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(
|
||||
(item) =>
|
||||
item &&
|
||||
canApplyTranslationTo(item) &&
|
||||
applyTranslation(item, entityIdToTranslation)
|
||||
)
|
||||
} else if (isObject(value) && canApplyTranslationTo(value)) {
|
||||
applyTranslation(value, entityIdToTranslation)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function applyTranslations({
|
||||
localeCode,
|
||||
objects,
|
||||
container,
|
||||
}: {
|
||||
localeCode: string | undefined
|
||||
objects: Record<string, any>[]
|
||||
container: MedusaContainer
|
||||
}) {
|
||||
const isTranslationEnabled = FeatureFlag.isFeatureEnabled("translation")
|
||||
|
||||
if (!isTranslationEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const locale = localeCode
|
||||
|
||||
if (!locale) {
|
||||
return
|
||||
}
|
||||
|
||||
const objects_ = objects.filter((o) => !!o)
|
||||
if (!objects_.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const gatheredIds: Set<string> = new Set()
|
||||
|
||||
for (const inputObject of objects_) {
|
||||
gatherIds(inputObject, gatheredIds)
|
||||
}
|
||||
|
||||
const query = container.resolve<RemoteQueryFunction>(
|
||||
ContainerRegistrationKeys.QUERY
|
||||
)
|
||||
|
||||
const queryBatchSize = 250
|
||||
const queryBatches = Math.ceil(gatheredIds.size / queryBatchSize)
|
||||
|
||||
const entityIdToTranslation = new Map<string, Record<string, any>>()
|
||||
|
||||
for (let i = 0; i < queryBatches; i++) {
|
||||
// TODO: concurrently fetch if needed
|
||||
const queryBatch = Array.from(gatheredIds)
|
||||
.slice(i * queryBatchSize, (i + 1) * queryBatchSize)
|
||||
.sort()
|
||||
|
||||
const { data: translations } = await query.graph(
|
||||
{
|
||||
entity: "translations",
|
||||
fields: ["translations", "reference_id"],
|
||||
filters: {
|
||||
reference_id: queryBatch,
|
||||
locale_code: locale,
|
||||
},
|
||||
pagination: {
|
||||
take: queryBatchSize,
|
||||
},
|
||||
},
|
||||
{
|
||||
cache: { enable: true },
|
||||
}
|
||||
)
|
||||
|
||||
for (const translation of translations) {
|
||||
entityIdToTranslation.set(
|
||||
translation.reference_id,
|
||||
translation.translations ?? {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const inputObject of objects_) {
|
||||
applyTranslation(inputObject, entityIdToTranslation)
|
||||
}
|
||||
}
|
||||
1
packages/core/utils/src/translations/index.ts
Normal file
1
packages/core/utils/src/translations/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./apply-translations"
|
||||
@@ -25,6 +25,7 @@
|
||||
"author": "Medusa",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@medusajs/core-flows": "2.12.1",
|
||||
"@medusajs/framework": "2.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -35,10 +36,14 @@
|
||||
"ulid": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@medusajs/core-flows": "2.12.1",
|
||||
"@medusajs/framework": "2.12.1",
|
||||
"@medusajs/medusa": "2.12.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@medusajs/core-flows": {
|
||||
"optional": true
|
||||
},
|
||||
"@medusajs/medusa": {
|
||||
"optional": true
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "./medusa-test-runner-utils"
|
||||
import { waitWorkflowExecutions } from "./medusa-test-runner-utils/wait-workflow-executions"
|
||||
import { ulid } from "ulid"
|
||||
import { createDefaultsWorkflow } from "@medusajs/core-flows"
|
||||
|
||||
export interface MedusaSuiteOptions {
|
||||
dbConnection: any // knex instance
|
||||
@@ -287,6 +288,7 @@ class MedusaTestRunner {
|
||||
cwd: this.cwd,
|
||||
})
|
||||
await medusaAppLoader.runModulesLoader()
|
||||
await createDefaultsWorkflow(copiedContainer).run()
|
||||
} catch (error) {
|
||||
await copiedContainer.dispose?.()
|
||||
logger.error("Error running modules loaders:", error?.message)
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"@medusajs/store": "2.12.1",
|
||||
"@medusajs/tax": "2.12.1",
|
||||
"@medusajs/telemetry": "2.12.1",
|
||||
"@medusajs/translation": "2.12.1",
|
||||
"@medusajs/user": "2.12.1",
|
||||
"@medusajs/workflow-engine-inmemory": "2.12.1",
|
||||
"@medusajs/workflow-engine-redis": "2.12.1",
|
||||
|
||||
28
packages/medusa/src/api/admin/locales/[code]/route.ts
Normal file
28
packages/medusa/src/api/admin/locales/[code]/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { HttpTypes } from "@medusajs/framework/types"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest<HttpTypes.AdminLocaleParams>,
|
||||
res: MedusaResponse<HttpTypes.AdminLocaleResponse>
|
||||
) => {
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const {
|
||||
data: [locale],
|
||||
} = await query.graph(
|
||||
{
|
||||
entity: "locale",
|
||||
filters: {
|
||||
code: req.params.code,
|
||||
},
|
||||
fields: req.queryConfig.fields,
|
||||
},
|
||||
{
|
||||
cache: { enable: true },
|
||||
throwIfKeyNotFound: true,
|
||||
}
|
||||
)
|
||||
|
||||
res.status(200).json({ locale })
|
||||
}
|
||||
27
packages/medusa/src/api/admin/locales/middlewares.ts
Normal file
27
packages/medusa/src/api/admin/locales/middlewares.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { MiddlewareRoute } from "@medusajs/framework/http"
|
||||
import { validateAndTransformQuery } from "@medusajs/framework"
|
||||
import * as QueryConfig from "./query-config"
|
||||
import { AdminGetLocalesParams, AdminGetLocaleParams } from "./validators"
|
||||
|
||||
export const adminLocalesRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/locales",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetLocalesParams,
|
||||
QueryConfig.listTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/locales/:code",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetLocaleParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
12
packages/medusa/src/api/admin/locales/query-config.ts
Normal file
12
packages/medusa/src/api/admin/locales/query-config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const defaultAdminLocaleFields = ["code", "name"]
|
||||
|
||||
export const retrieveTransformQueryConfig = {
|
||||
defaults: defaultAdminLocaleFields,
|
||||
isList: false,
|
||||
}
|
||||
|
||||
export const listTransformQueryConfig = {
|
||||
...retrieveTransformQueryConfig,
|
||||
defaultLimit: 200,
|
||||
isList: true,
|
||||
}
|
||||
29
packages/medusa/src/api/admin/locales/route.ts
Normal file
29
packages/medusa/src/api/admin/locales/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { HttpTypes } from "@medusajs/framework/types"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest<HttpTypes.AdminLocaleListParams>,
|
||||
res: MedusaResponse<HttpTypes.AdminLocaleListResponse>
|
||||
) => {
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const { data: locales, metadata } = await query.graph(
|
||||
{
|
||||
entity: "locale",
|
||||
filters: req.filterableFields,
|
||||
fields: req.queryConfig.fields,
|
||||
pagination: req.queryConfig.pagination,
|
||||
},
|
||||
{
|
||||
cache: { enable: true },
|
||||
}
|
||||
)
|
||||
|
||||
res.json({
|
||||
locales,
|
||||
count: metadata?.count ?? 0,
|
||||
offset: metadata?.skip ?? 0,
|
||||
limit: metadata?.take ?? 0,
|
||||
})
|
||||
}
|
||||
18
packages/medusa/src/api/admin/locales/validators.ts
Normal file
18
packages/medusa/src/api/admin/locales/validators.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod"
|
||||
import { createFindParams, createSelectParams } from "../../utils/validators"
|
||||
import { applyAndAndOrOperators } from "../../utils/common-validators"
|
||||
|
||||
export const AdminGetLocaleParams = createSelectParams()
|
||||
|
||||
export const AdminGetLocalesParamsFields = z.object({
|
||||
q: z.string().optional(),
|
||||
code: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
})
|
||||
|
||||
export type AdminGetLocalesParamsType = z.infer<typeof AdminGetLocalesParams>
|
||||
export const AdminGetLocalesParams = createFindParams({
|
||||
offset: 0,
|
||||
limit: 200,
|
||||
})
|
||||
.merge(AdminGetLocalesParamsFields)
|
||||
.merge(applyAndAndOrOperators(AdminGetLocalesParamsFields))
|
||||
@@ -3,6 +3,8 @@ export const defaultAdminStoreFields = [
|
||||
"name",
|
||||
"*supported_currencies",
|
||||
"*supported_currencies.currency",
|
||||
"*supported_locales",
|
||||
"*supported_locales.locale_code",
|
||||
"default_sales_channel_id",
|
||||
"default_region_id",
|
||||
"default_location_id",
|
||||
|
||||
@@ -31,6 +31,14 @@ export const AdminUpdateStore = z.object({
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
supported_locales: z
|
||||
.array(
|
||||
z.object({
|
||||
locale_code: z.string(),
|
||||
is_default: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
default_sales_channel_id: z.string().nullish(),
|
||||
default_region_id: z.string().nullish(),
|
||||
default_location_id: z.string().nullish(),
|
||||
|
||||
68
packages/medusa/src/api/admin/translations/batch/route.ts
Normal file
68
packages/medusa/src/api/admin/translations/batch/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { batchTranslationsWorkflow } from "@medusajs/core-flows"
|
||||
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
defineFileConfig,
|
||||
FeatureFlag,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { BatchMethodRequest, HttpTypes } from "@medusajs/types"
|
||||
import { defaultAdminTranslationFields } from "../query-config"
|
||||
import {
|
||||
AdminCreateTranslationType,
|
||||
AdminUpdateTranslationType,
|
||||
} from "../validators"
|
||||
import TranslationFeatureFlag from "../../../../feature-flags/translation"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<
|
||||
BatchMethodRequest<AdminCreateTranslationType, AdminUpdateTranslationType>
|
||||
>,
|
||||
res: MedusaResponse<HttpTypes.AdminTranslationsBatchResponse>
|
||||
) => {
|
||||
const { create = [], update = [], delete: deleteIds = [] } = req.validatedBody
|
||||
|
||||
const { result } = await batchTranslationsWorkflow(req.scope).run({
|
||||
input: {
|
||||
create,
|
||||
update,
|
||||
delete: deleteIds,
|
||||
},
|
||||
})
|
||||
|
||||
const ids = Array.from(
|
||||
new Set([
|
||||
...result.created.map((t) => t.id),
|
||||
...result.updated.map((t) => t.id),
|
||||
])
|
||||
)
|
||||
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
const { data: translations } = await query.graph({
|
||||
entity: "translation",
|
||||
fields: defaultAdminTranslationFields,
|
||||
filters: {
|
||||
id: ids,
|
||||
},
|
||||
})
|
||||
|
||||
const created = translations.filter((t) =>
|
||||
result.created.some((r) => r.id === t.id)
|
||||
)
|
||||
const updated = translations.filter((t) =>
|
||||
result.updated.some((r) => r.id === t.id)
|
||||
)
|
||||
|
||||
return res.status(200).json({
|
||||
created,
|
||||
updated,
|
||||
deleted: {
|
||||
ids: deleteIds,
|
||||
object: "translation",
|
||||
deleted: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
defineFileConfig({
|
||||
isDisabled: () => !FeatureFlag.isFeatureEnabled(TranslationFeatureFlag.key),
|
||||
})
|
||||
32
packages/medusa/src/api/admin/translations/middlewares.ts
Normal file
32
packages/medusa/src/api/admin/translations/middlewares.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
MiddlewareRoute,
|
||||
validateAndTransformBody,
|
||||
validateAndTransformQuery,
|
||||
} from "@medusajs/framework"
|
||||
import {
|
||||
AdminBatchTranslations,
|
||||
AdminGetTranslationsParams,
|
||||
} from "./validators"
|
||||
import * as QueryConfig from "./query-config"
|
||||
import { DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT } from "../../../utils"
|
||||
|
||||
export const adminTranslationsRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/translations",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetTranslationsParams,
|
||||
QueryConfig.listTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/translations/batch",
|
||||
bodyParser: {
|
||||
sizeLimit: DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT,
|
||||
},
|
||||
middlewares: [validateAndTransformBody(AdminBatchTranslations)],
|
||||
},
|
||||
]
|
||||
17
packages/medusa/src/api/admin/translations/query-config.ts
Normal file
17
packages/medusa/src/api/admin/translations/query-config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const defaultAdminTranslationFields = [
|
||||
"id",
|
||||
"reference_id",
|
||||
"reference",
|
||||
"locale_code",
|
||||
"translations",
|
||||
]
|
||||
|
||||
export const retrieveTransformQueryConfig = {
|
||||
defaults: defaultAdminTranslationFields,
|
||||
isList: false,
|
||||
}
|
||||
|
||||
export const listTransformQueryConfig = {
|
||||
...retrieveTransformQueryConfig,
|
||||
isList: true,
|
||||
}
|
||||
38
packages/medusa/src/api/admin/translations/route.ts
Normal file
38
packages/medusa/src/api/admin/translations/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { HttpTypes } from "@medusajs/framework/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
defineFileConfig,
|
||||
FeatureFlag,
|
||||
} from "@medusajs/framework/utils"
|
||||
import TranslationFeatureFlag from "../../../feature-flags/translation"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest<HttpTypes.AdminTranslationsListParams>,
|
||||
res: MedusaResponse<HttpTypes.AdminTranslationsListResponse>
|
||||
) => {
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const { data: translations, metadata } = await query.graph(
|
||||
{
|
||||
entity: "translation",
|
||||
fields: req.queryConfig.fields,
|
||||
filters: req.filterableFields,
|
||||
pagination: req.queryConfig.pagination,
|
||||
},
|
||||
{
|
||||
cache: { enable: true },
|
||||
}
|
||||
)
|
||||
|
||||
return res.status(200).json({
|
||||
translations,
|
||||
count: metadata?.count ?? 0,
|
||||
offset: metadata?.skip ?? 0,
|
||||
limit: metadata?.take ?? 0,
|
||||
})
|
||||
}
|
||||
|
||||
defineFileConfig({
|
||||
isDisabled: () => !FeatureFlag.isFeatureEnabled(TranslationFeatureFlag.key),
|
||||
})
|
||||
50
packages/medusa/src/api/admin/translations/validators.ts
Normal file
50
packages/medusa/src/api/admin/translations/validators.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { applyAndAndOrOperators } from "../../utils/common-validators"
|
||||
import {
|
||||
createBatchBody,
|
||||
createFindParams,
|
||||
createSelectParams,
|
||||
} from "../../utils/validators"
|
||||
import { z } from "zod"
|
||||
|
||||
export const AdminGetTranslationParams = createSelectParams()
|
||||
|
||||
export const AdminGetTranslationParamsFields = z.object({
|
||||
q: z.string().optional(),
|
||||
reference_id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
reference: z.string().optional(),
|
||||
locale_code: z.string().optional(),
|
||||
})
|
||||
|
||||
export type AdminGetTranslationsParamsType = z.infer<
|
||||
typeof AdminGetTranslationsParams
|
||||
>
|
||||
|
||||
export const AdminGetTranslationsParams = createFindParams({
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
})
|
||||
.merge(AdminGetTranslationParamsFields)
|
||||
.merge(applyAndAndOrOperators(AdminGetTranslationParamsFields))
|
||||
|
||||
export type AdminCreateTranslationType = z.infer<typeof AdminCreateTranslation>
|
||||
export const AdminCreateTranslation = z.object({
|
||||
reference_id: z.string(),
|
||||
reference: z.string(),
|
||||
locale_code: z.string(),
|
||||
translations: z.record(z.string()),
|
||||
})
|
||||
|
||||
export type AdminUpdateTranslationType = z.infer<typeof AdminUpdateTranslation>
|
||||
export const AdminUpdateTranslation = z.object({
|
||||
id: z.string(),
|
||||
reference_id: z.string().optional(),
|
||||
reference: z.string().optional(),
|
||||
locale_code: z.string().optional(),
|
||||
translations: z.record(z.string()).optional(),
|
||||
})
|
||||
|
||||
export type AdminBatchTranslationsType = z.infer<typeof AdminBatchTranslations>
|
||||
export const AdminBatchTranslations = createBatchBody(
|
||||
AdminCreateTranslation,
|
||||
AdminUpdateTranslation
|
||||
)
|
||||
@@ -66,6 +66,8 @@ import { storeReturnReasonRoutesMiddlewares } from "./store/return-reasons/middl
|
||||
import { storeShippingOptionRoutesMiddlewares } from "./store/shipping-options/middlewares"
|
||||
import { adminShippingOptionTypeRoutesMiddlewares } from "./admin/shipping-option-types/middlewares"
|
||||
import { adminIndexRoutesMiddlewares } from "./admin/index/middlewares"
|
||||
import { adminLocalesRoutesMiddlewares } from "./admin/locales/middlewares"
|
||||
import { adminTranslationsRoutesMiddlewares } from "./admin/translations/middlewares"
|
||||
|
||||
export default defineMiddlewares([
|
||||
...storeRoutesMiddlewares,
|
||||
@@ -94,10 +96,12 @@ export default defineMiddlewares([
|
||||
...adminInviteRoutesMiddlewares,
|
||||
...adminTaxRateRoutesMiddlewares,
|
||||
...adminTaxRegionRoutesMiddlewares,
|
||||
...adminTranslationsRoutesMiddlewares,
|
||||
...adminApiKeyRoutesMiddlewares,
|
||||
...hooksRoutesMiddlewares,
|
||||
...adminStoreRoutesMiddlewares,
|
||||
...adminCurrencyRoutesMiddlewares,
|
||||
...adminLocalesRoutesMiddlewares,
|
||||
...storeCurrencyRoutesMiddlewares,
|
||||
...adminProductRoutesMiddlewares,
|
||||
...adminPaymentRoutesMiddlewares,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { MedusaResponse } from "@medusajs/framework/http"
|
||||
import { HttpTypes } from "@medusajs/framework/types"
|
||||
import { isPresent, MedusaError, QueryContext } from "@medusajs/framework/utils"
|
||||
import {
|
||||
applyTranslations,
|
||||
isPresent,
|
||||
MedusaError,
|
||||
QueryContext,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { wrapVariantsWithInventoryQuantityForSalesChannel } from "../../../utils/middlewares"
|
||||
import {
|
||||
filterOutInternalProductCategories,
|
||||
@@ -69,5 +74,10 @@ export const GET = async (
|
||||
}
|
||||
|
||||
await wrapProductsWithTaxPrices(req, [product])
|
||||
await applyTranslations({
|
||||
localeCode: req.locale,
|
||||
objects: [product],
|
||||
container: req.scope,
|
||||
})
|
||||
res.json({ product })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { validateAndTransformQuery } from "@medusajs/framework"
|
||||
import {
|
||||
applyDefaultFilters,
|
||||
applyLocale,
|
||||
applyParamsAsFilters,
|
||||
authenticate,
|
||||
clearFiltersByKey,
|
||||
@@ -63,6 +64,10 @@ async function applyMaybeLinkFilterIfNecessary(
|
||||
}
|
||||
|
||||
export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
matcher: "/store/products/*",
|
||||
middlewares: [applyLocale],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/store/products",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user