feat(): Introduce translation module and preliminary application of them (#14189)

* feat(): Translation first steps

* feat(): locale middleware

* feat(): readonly links

* feat(): feature flag

* feat(): modules sdk

* feat(): translation module re export

* start adding workflows

* update typings

* update typings

* test(): Add integration tests

* test(): centralize filters preparation

* test(): centralize filters preparation

* remove unnecessary importy

* fix workflows

* Define StoreLocale inside Store Module

* Link definition to extend Store with supported_locales

* store_locale migration

* Add supported_locales handling in Store Module

* Tests

* Accept supported_locales in Store endpoints

* Add locales to js-sdk

* Include locale list and default locale in Store Detail section

* Initialize local namespace in js-sdk

* Add locales route

* Make code primary key of locale table to facilitate upserts

* Add locales routes

* Show locale code as is

* Add list translations api route

* Batch endpoint

* Types

* New batchTranslationsWorkflow and various updates to existent ones

* Edit default locale UI

* WIP

* Apply translation agnostically

* middleware

* Apply translation agnostically

* fix Apply translation agnostically

* apply translations to product list

* Add feature flag

* fetch translations by batches of 250 max

* fix apply

* improve and test util

* apply to product list

* dont manage translations if no locale

* normalize locale

* potential todo

* Protect translations routes with feature flag

* Extract normalize locale util to core/utils

* Normalize locale on write

* Normalize locale for read

* Use feature flag to guard translations UI across the board

* Avoid throwing incorrectly when locale_code not present in partial updates

* move applyTranslations util

* remove old tests

* fix util tests

* fix(): product end points

* cleanup

* update lock

* remove unused var

* cleanup

* fix apply locale

* missing new dep for test utils

* Change entity_type, entity_id to reference, reference_id

* Remove comment

* Avoid registering translations route if ff not enabled

* Prevent registering express handler for disabled route via defineFileConfig

* Add tests

* Add changeset

* Update test

* fix integration tests, module and internals

* Add locale id plus fixed

* Allow to pass array of reference_id

* fix unit tests

* fix link loading

* fix store route

* fix sales channel test

* fix tests

---------

Co-authored-by: Nicolas Gorga <nicogorga11@gmail.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-12-08 19:33:08 +01:00
committed by GitHub
parent fea3d4ec49
commit 6dc0b8bed8
130 changed files with 5649 additions and 112 deletions

View File

@@ -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"),

View File

@@ -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
})
}
}

View File

@@ -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"

View 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 }
}

View File

@@ -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"
],

View File

@@ -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"
}
},

View File

@@ -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"
}
},

View File

@@ -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]
)
}

View File

@@ -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 }
}

View File

@@ -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]
)
}

View File

@@ -0,0 +1 @@
export { StoreAddLocales as Component } from "./store-add-locales"

View File

@@ -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>
)
}

View File

@@ -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")}

View File

@@ -0,0 +1 @@
export * from "./store-locale-section"

View File

@@ -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]
)
}

View File

@@ -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>
)
}

View File

@@ -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"