diff --git a/.changeset/wild-zebras-hammer.md b/.changeset/wild-zebras-hammer.md new file mode 100644 index 0000000000000..7ead2211f1894 --- /dev/null +++ b/.changeset/wild-zebras-hammer.md @@ -0,0 +1,6 @@ +--- +"@medusajs/ui": patch +"@medusajs/dashboard": patch +--- + +feat(ui,dashboard): Add new DataTable block diff --git a/.eslintrc.js b/.eslintrc.js index 705010c5c6280..7220b54576a6b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,6 +87,11 @@ module.exports = { "./packages/admin/admin-bundler/tsconfig.json", "./packages/admin/admin-vite-plugin/tsconfig.json", + "./packages/design-system/ui/tsconfig.json", + "./packages/design-system/icons/tsconfig.json", + "./packages/design-system/ui-preset/tsconfig.json", + "./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", @@ -167,7 +172,10 @@ module.exports = { }, }, { - files: ["packages/design-system/ui/**/*.{ts,tsx}"], + files: [ + "./packages/design-system/ui/**/*.ts", + "./packages/design-system/ui/**/*.tsx", + ], extends: [ "plugin:react/recommended", "plugin:storybook/recommended", @@ -196,7 +204,10 @@ module.exports = { }, }, { - files: ["packages/design-system/icons/**/*.{ts,tsx}"], + files: [ + "./packages/design-system/icons/**/*.ts", + "./packages/design-system/icons/**/*.tsx", + ], extends: [ "plugin:react/recommended", "plugin:@typescript-eslint/recommended", @@ -223,8 +234,8 @@ module.exports = { }, { files: [ - "packages/admin/dashboard/**/*.ts", - "packages/admin/dashboard/**/*.tsx", + "./packages/admin/dashboard/**/*.ts", + "./packages/admin/dashboard/**/*.tsx", ], plugins: ["unused-imports", "react-refresh"], extends: [ diff --git a/.prettierrc b/.prettierrc index 3718cf898655b..54e52522ce7b5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,7 +7,13 @@ "arrowParens": "always", "overrides": [ { - "files": "./packages/admin-ui/**/*.{js,jsx,ts,tsx}", + "files": "./packages/admin/dashboard/src/**/*.{ts,tsx}", + "options": { + "plugins": ["prettier-plugin-tailwindcss"] + } + }, + { + "files": "./packages/design-system/ui/src/**/*.{ts,tsx}", "options": { "plugins": ["prettier-plugin-tailwindcss"] } diff --git a/packages/admin/dashboard/src/components/common/action-menu/action-menu.tsx b/packages/admin/dashboard/src/components/common/action-menu/action-menu.tsx index cd918763c2591..a9ba82f4aab36 100644 --- a/packages/admin/dashboard/src/components/common/action-menu/action-menu.tsx +++ b/packages/admin/dashboard/src/components/common/action-menu/action-menu.tsx @@ -30,11 +30,16 @@ export type ActionGroup = { type ActionMenuProps = PropsWithChildren<{ groups: ActionGroup[] + variant?: "transparent" | "primary" }> -export const ActionMenu = ({ groups, children }: ActionMenuProps) => { +export const ActionMenu = ({ + groups, + variant = "transparent", + children, +}: ActionMenuProps) => { const inner = children ?? ( - + ) diff --git a/packages/admin/dashboard/src/components/data-table/data-table.tsx b/packages/admin/dashboard/src/components/data-table/data-table.tsx new file mode 100644 index 0000000000000..28cfa015c10ce --- /dev/null +++ b/packages/admin/dashboard/src/components/data-table/data-table.tsx @@ -0,0 +1,404 @@ +import { + Button, + DataTableCommand, + DataTableEmptyStateProps, + DataTableFilter, + DataTableFilteringState, + DataTablePaginationState, + DataTableRowSelectionState, + DataTableSortingState, + Heading, + DataTable as Primitive, + useDataTable, +} from "@medusajs/ui" +import { ColumnDef } from "@tanstack/react-table" +import React, { ReactNode, useCallback, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate, useSearchParams } from "react-router-dom" + +import { useQueryParams } from "../../hooks/use-query-params" +import { ActionMenu } from "../common/action-menu" + +type DataTableActionProps = { + label: string + disabled?: boolean +} & ( + | { + to: string + } + | { + onClick: () => void + } +) + +type DataTableActionMenuActionProps = { + label: string + icon: ReactNode + disabled?: boolean +} & ( + | { + to: string + } + | { + onClick: () => void + } +) + +type DataTableActionMenuGroupProps = { + actions: DataTableActionMenuActionProps[] +} + +type DataTableActionMenuProps = { + groups: DataTableActionMenuGroupProps[] +} + +interface DataTableProps { + data?: TData[] + columns: ColumnDef[] + filters?: DataTableFilter[] + commands?: DataTableCommand[] + action?: DataTableActionProps + actionMenu?: DataTableActionMenuProps + rowCount?: number + getRowId: (row: TData) => string + enablePagination?: boolean + enableSearch?: boolean + autoFocusSearch?: boolean + rowHref?: (row: TData) => string + emptyState?: DataTableEmptyStateProps + heading: string + prefix?: string + pageSize?: number + isLoading?: boolean + rowSelection?: { + state: DataTableRowSelectionState + onRowSelectionChange: (value: DataTableRowSelectionState) => void + } +} + +export const DataTable = ({ + data = [], + columns, + filters, + commands, + action, + actionMenu, + getRowId, + rowCount = 0, + enablePagination = true, + enableSearch = true, + autoFocusSearch = false, + rowHref, + heading, + prefix, + pageSize = 10, + emptyState, + rowSelection, + isLoading = false, +}: DataTableProps) => { + const { t } = useTranslation() + + const enableFiltering = filters && filters.length > 0 + const enableCommands = commands && commands.length > 0 + const enableSorting = columns.some((column) => column.enableSorting) + + const filterIds = filters?.map((f) => f.id) ?? [] + const prefixedFilterIds = filterIds.map((id) => getQueryParamKey(id, prefix)) + + const { offset, order, q, ...filterParams } = useQueryParams( + [ + ...filterIds, + ...(enableSorting ? ["order"] : []), + ...(enableSearch ? ["q"] : []), + ...(enablePagination ? ["offset"] : []), + ], + prefix + ) + const [_, setSearchParams] = useSearchParams() + + const [search, setSearch] = useState(q ?? "") + const handleSearchChange = (value: string) => { + setSearch(value) + setSearchParams((prev) => { + if (value) { + prev.set(getQueryParamKey("q", prefix), value) + } else { + prev.delete(getQueryParamKey("q", prefix)) + } + + return prev + }) + } + + const [pagination, setPagination] = useState( + offset ? parsePaginationState(offset, pageSize) : { pageIndex: 0, pageSize } + ) + const handlePaginationChange = (value: DataTablePaginationState) => { + setPagination(value) + setSearchParams((prev) => { + if (value.pageIndex === 0) { + prev.delete(getQueryParamKey("offset", prefix)) + } else { + prev.set( + getQueryParamKey("offset", prefix), + transformPaginationState(value).toString() + ) + } + + return prev + }) + } + + const [filtering, setFiltering] = useState( + parseFilterState(filterIds, filterParams) + ) + const handleFilteringChange = (value: DataTableFilteringState) => { + setFiltering(value) + + setSearchParams((prev) => { + Array.from(prev.keys()).forEach((key) => { + if (prefixedFilterIds.includes(key) && !(key in value)) { + prev.delete(key) + } + }) + + Object.entries(value).forEach(([key, filter]) => { + if ( + prefixedFilterIds.includes(getQueryParamKey(key, prefix)) && + filter + ) { + prev.set(getQueryParamKey(key, prefix), JSON.stringify(filter)) + } + }) + + return prev + }) + } + + const [sorting, setSorting] = useState( + order ? parseSortingState(order) : null + ) + const handleSortingChange = (value: DataTableSortingState) => { + setSorting(value) + setSearchParams((prev) => { + if (value) { + const valueToStore = transformSortingState(value) + + prev.set(getQueryParamKey("order", prefix), valueToStore) + } else { + prev.delete(getQueryParamKey("order", prefix)) + } + + return prev + }) + } + + const { pagination: paginationTranslations, toolbar: toolbarTranslations } = + useDataTableTranslations() + + const navigate = useNavigate() + + const onRowClick = useCallback( + (event: React.MouseEvent, row: TData) => { + if (!rowHref) { + return + } + + const href = rowHref(row) + + if (event.metaKey || event.ctrlKey || event.button === 1) { + window.open(href, "_blank", "noreferrer") + return + } + + if (event.shiftKey) { + window.open(href, undefined, "noreferrer") + return + } + + navigate(href) + }, + [navigate, rowHref] + ) + + const instance = useDataTable({ + data, + columns, + filters, + commands, + rowCount, + getRowId, + onRowClick: rowHref ? onRowClick : undefined, + pagination: enablePagination + ? { + state: pagination, + onPaginationChange: handlePaginationChange, + } + : undefined, + filtering: enableFiltering + ? { + state: filtering, + onFilteringChange: handleFilteringChange, + } + : undefined, + sorting: enableSorting + ? { + state: sorting, + onSortingChange: handleSortingChange, + } + : undefined, + search: enableSearch + ? { + state: search, + onSearchChange: handleSearchChange, + } + : undefined, + rowSelection, + isLoading, + }) + + return ( + + +
+ {heading} +
+ {enableFiltering && ( + + )} + + {actionMenu && } + {action && } +
+
+
+ {enableSearch && ( +
+ +
+ )} +
+ {enableFiltering && ( + + )} + + {actionMenu && } + {action && } +
+
+
+ + {enablePagination && ( + + )} + {enableCommands && ( + `${count} selected`} /> + )} +
+ ) +} + +function transformSortingState(value: DataTableSortingState) { + return value.desc ? `-${value.id}` : value.id +} + +function parseSortingState(value: string) { + return value.startsWith("-") + ? { id: value.slice(1), desc: true } + : { id: value, desc: false } +} + +function transformPaginationState(value: DataTablePaginationState) { + return value.pageIndex * value.pageSize +} + +function parsePaginationState(value: string, pageSize: number) { + const offset = parseInt(value) + + return { + pageIndex: Math.floor(offset / pageSize), + pageSize, + } +} + +function parseFilterState( + filterIds: string[], + value: Record +) { + if (!value) { + return {} + } + + const filters: DataTableFilteringState = {} + + for (const id of filterIds) { + const filterValue = value[id] + + if (filterValue) { + filters[id] = { + id, + value: JSON.parse(filterValue), + } + } + } + + return filters +} + +function getQueryParamKey(key: string, prefix?: string) { + return prefix ? `${prefix}_${key}` : key +} + +const useDataTableTranslations = () => { + const { t } = useTranslation() + + const paginationTranslations = { + of: t("general.of"), + results: t("general.results"), + pages: t("general.pages"), + prev: t("general.prev"), + next: t("general.next"), + } + + const toolbarTranslations = { + clearAll: t("actions.clearAll"), + } + + return { + pagination: paginationTranslations, + toolbar: toolbarTranslations, + } +} + +const DataTableAction = ({ + label, + disabled, + ...props +}: DataTableActionProps) => { + const buttonProps = { + size: "small" as const, + disabled: disabled ?? false, + type: "button" as const, + variant: "secondary" as const, + } + + if ("to" in props) { + return ( + + ) + } + + return ( + + ) +} diff --git a/packages/admin/dashboard/src/components/data-table/index.ts b/packages/admin/dashboard/src/components/data-table/index.ts new file mode 100644 index 0000000000000..8e2d5f828a450 --- /dev/null +++ b/packages/admin/dashboard/src/components/data-table/index.ts @@ -0,0 +1 @@ +export * from "./data-table" diff --git a/packages/admin/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx b/packages/admin/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx index 9244823e20579..38c1ce84e97df 100644 --- a/packages/admin/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx +++ b/packages/admin/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx @@ -7,8 +7,8 @@ import { useTranslation } from "react-i18next" import { useSelectedParams } from "../hooks" import { useDataTableFilterContext } from "./context" -import { IFilter } from "./types" import FilterChip from "./filter-chip" +import { IFilter } from "./types" interface SelectFilterProps extends IFilter { options: { label: string; value: unknown }[] @@ -41,7 +41,9 @@ export const SelectFilter = ({ .map((v) => options.find((o) => o.value === v)?.label) .filter(Boolean) as string[] - const [previousValue, setPreviousValue] = useState(labelValues) + const [previousValue, setPreviousValue] = useState< + string | string[] | undefined + >(labelValues) const handleRemove = () => { selectedParams.delete() @@ -84,8 +86,16 @@ export const SelectFilter = ({ } } - const normalizedValues = labelValues ? (Array.isArray(labelValues) ? labelValues : [labelValues]) : null - const normalizedPrev = previousValue ? (Array.isArray(previousValue) ? previousValue : [previousValue]) : null + const normalizedValues = labelValues + ? Array.isArray(labelValues) + ? labelValues + : [labelValues] + : null + const normalizedPrev = previousValue + ? Array.isArray(previousValue) + ? previousValue + : [previousValue] + : null return ( diff --git a/packages/admin/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx b/packages/admin/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx index 3be55a83718a4..3a6ef2a59d0df 100644 --- a/packages/admin/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx +++ b/packages/admin/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx @@ -176,7 +176,7 @@ export const DataTableRoot = ({ : undefined, }} className={clx({ - "bg-ui-bg-base sticky left-0 after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']": + "bg-ui-bg-subtle sticky left-0 after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']": isStickyHeader, "left-[68px]": isStickyHeader && hasSelect && !isSelectHeader, diff --git a/packages/admin/dashboard/src/components/table/data-table/data-table.tsx b/packages/admin/dashboard/src/components/table/data-table/data-table.tsx index e4719ba01ea75..9c741d0812e86 100644 --- a/packages/admin/dashboard/src/components/table/data-table/data-table.tsx +++ b/packages/admin/dashboard/src/components/table/data-table/data-table.tsx @@ -18,7 +18,10 @@ interface DataTableProps // const MemoizedDataTableRoot = memo(DataTableRoot) as typeof DataTableRoot const MemoizedDataTableQuery = memo(DataTableQuery) as typeof DataTableQuery -export const DataTable = ({ +/** + * @deprecated Use the DataTable component from "/components/data-table" instead + */ +export const _DataTable = ({ table, columns, pagination, diff --git a/packages/admin/dashboard/src/hooks/api/customer-groups.tsx b/packages/admin/dashboard/src/hooks/api/customer-groups.tsx index b435b366722de..30340a4cfbe8d 100644 --- a/packages/admin/dashboard/src/hooks/api/customer-groups.tsx +++ b/packages/admin/dashboard/src/hooks/api/customer-groups.tsx @@ -40,7 +40,7 @@ export const useCustomerGroup = ( } export const useCustomerGroups = ( - query?: Record, + query?: HttpTypes.AdminGetCustomerGroupsParams, options?: Omit< UseQueryOptions< HttpTypes.AdminGetCustomerGroupsParams, @@ -127,6 +127,29 @@ export const useDeleteCustomerGroup = ( }) } +export const useDeleteCustomerGroupLazy = ( + options?: UseMutationOptions< + HttpTypes.AdminCustomerGroupDeleteResponse, + FetchError, + { id: string } + > +) => { + return useMutation({ + mutationFn: ({ id }) => sdk.admin.customerGroup.delete(id), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: customerGroupsQueryKeys.lists(), + }) + queryClient.invalidateQueries({ + queryKey: customerGroupsQueryKeys.detail(variables.id), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + export const useAddCustomersToGroup = ( id: string, options?: UseMutationOptions< diff --git a/packages/admin/dashboard/src/hooks/api/products.tsx b/packages/admin/dashboard/src/hooks/api/products.tsx index eee11f83d89c1..f054cbd5d6017 100644 --- a/packages/admin/dashboard/src/hooks/api/products.tsx +++ b/packages/admin/dashboard/src/hooks/api/products.tsx @@ -241,6 +241,32 @@ export const useDeleteVariant = ( }) } +export const useDeleteVariantLazy = ( + productId: string, + options?: UseMutationOptions< + HttpTypes.AdminProductVariantDeleteResponse, + FetchError, + { variantId: string } + > +) => { + return useMutation({ + mutationFn: ({ variantId }) => + sdk.admin.product.deleteVariant(productId, variantId), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: variantsQueryKeys.lists() }) + queryClient.invalidateQueries({ + queryKey: variantsQueryKeys.detail(variables.variantId), + }) + queryClient.invalidateQueries({ + queryKey: productsQueryKeys.detail(productId), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + export const useProduct = ( id: string, query?: Record, diff --git a/packages/admin/dashboard/src/hooks/filters/use-date-filter-options.tsx b/packages/admin/dashboard/src/hooks/filters/use-date-filter-options.tsx new file mode 100644 index 0000000000000..0de57f8d5d52f --- /dev/null +++ b/packages/admin/dashboard/src/hooks/filters/use-date-filter-options.tsx @@ -0,0 +1,55 @@ +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +export const useDateFilterOptions = () => { + const { t } = useTranslation() + + const today = useMemo(() => { + const date = new Date() + date.setHours(0, 0, 0, 0) + return date + }, []) + + return useMemo(() => { + return [ + { + label: t("filters.date.today"), + value: { + $gte: today.toISOString(), + }, + }, + { + label: t("filters.date.lastSevenDays"), + value: { + $gte: new Date( + today.getTime() - 7 * 24 * 60 * 60 * 1000 + ).toISOString(), // 7 days ago + }, + }, + { + label: t("filters.date.lastThirtyDays"), + value: { + $gte: new Date( + today.getTime() - 30 * 24 * 60 * 60 * 1000 + ).toISOString(), // 30 days ago + }, + }, + { + label: t("filters.date.lastNinetyDays"), + value: { + $gte: new Date( + today.getTime() - 90 * 24 * 60 * 60 * 1000 + ).toISOString(), // 90 days ago + }, + }, + { + label: t("filters.date.lastTwelveMonths"), + value: { + $gte: new Date( + today.getTime() - 365 * 24 * 60 * 60 * 1000 + ).toISOString(), // 365 days ago + }, + }, + ] + }, [today, t]) +} diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index 698a73917ddcb..8dcf61defa5be 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -1220,6 +1220,15 @@ "filters": { "type": "object", "properties": { + "sortLabel": { + "type": "string" + }, + "filterLabel": { + "type": "string" + }, + "searchLabel": { + "type": "string" + }, "date": { "type": "object", "properties": { @@ -1246,6 +1255,12 @@ }, "to": { "type": "string" + }, + "starting": { + "type": "string" + }, + "ending": { + "type": "string" } }, "required": [ @@ -1256,7 +1271,9 @@ "lastTwelveMonths", "custom", "from", - "to" + "to", + "starting", + "ending" ], "additionalProperties": false }, @@ -1296,13 +1313,66 @@ ], "additionalProperties": false }, + "sorting": { + "type": "object", + "properties": { + "alphabeticallyAsc": { + "type": "string" + }, + "alphabeticallyDesc": { + "type": "string" + }, + "dateAsc": { + "type": "string" + }, + "dateDesc": { + "type": "string" + } + }, + "required": [ + "alphabeticallyAsc", + "alphabeticallyDesc", + "dateAsc", + "dateDesc" + ], + "additionalProperties": false + }, + "radio": { + "type": "object", + "properties": { + "yes": { + "type": "string" + }, + "no": { + "type": "string" + }, + "true": { + "type": "string" + }, + "false": { + "type": "string" + } + }, + "required": [ + "yes", + "no", + "true", + "false" + ], + "additionalProperties": false + }, "addFilter": { "type": "string" } }, "required": [ + "sortLabel", + "filterLabel", + "searchLabel", "date", "compare", + "sorting", + "radio", "addFilter" ], "additionalProperties": false @@ -1921,7 +1991,50 @@ "type": "string" }, "variants": { - "type": "string" + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "empty": { + "type": "object", + "properties": { + "heading": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "heading", + "description" + ], + "additionalProperties": false + }, + "filtered": { + "type": "object", + "properties": { + "heading": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "heading", + "description" + ], + "additionalProperties": false + } + }, + "required": [ + "header", + "empty", + "filtered" + ], + "additionalProperties": false }, "attributes": { "type": "string" @@ -2671,11 +2784,19 @@ "type": "string" } }, - "required": ["alreadyManaged", "alreadyManagedWithSku"], + "required": [ + "alreadyManaged", + "alreadyManagedWithSku" + ], "additionalProperties": false } }, - "required": ["heading", "description", "loading", "tooltips"], + "required": [ + "heading", + "description", + "loading", + "tooltips" + ], "additionalProperties": false }, "toasts": { @@ -3689,6 +3810,48 @@ "subtitle": { "type": "string" }, + "list": { + "type": "object", + "properties": { + "empty": { + "type": "object", + "properties": { + "heading": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "heading", + "description" + ], + "additionalProperties": false + }, + "filtered": { + "type": "object", + "properties": { + "heading": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "heading", + "description" + ], + "additionalProperties": false + } + }, + "required": [ + "empty", + "filtered" + ], + "additionalProperties": false + }, "create": { "type": "object", "properties": { @@ -3829,6 +3992,7 @@ "required": [ "domain", "subtitle", + "list", "create", "edit", "delete", diff --git a/packages/admin/dashboard/src/i18n/translations/de.json b/packages/admin/dashboard/src/i18n/translations/de.json index 5114dabd4489c..de6ca508021d9 100644 --- a/packages/admin/dashboard/src/i18n/translations/de.json +++ b/packages/admin/dashboard/src/i18n/translations/de.json @@ -316,6 +316,12 @@ "greaterThanLabel": "größer als {{value}}", "andLabel": "Und" }, + "radio": { + "yes": "Ja", + "no": "Nein", + "true": "Wahr", + "false": "Falsch" + }, "addFilter": "Filter hinzufügen" }, "errorBoundary": { @@ -467,7 +473,17 @@ } }, "deleteWarning": "Sie sind dabei, das Produkt {{title}} zu löschen. Diese Aktion kann nicht rückgängig gemacht werden.", - "variants": "Varianten", + "variants": { + "header": "Varianten", + "empty": { + "heading": "Keine Varianten", + "description": "Es gibt keine Varianten, um angezeigt zu werden." + }, + "filtered": { + "heading": "Keine Ergebnisse", + "description": "Keine Varianten stimmen mit den aktuellen Filterkriterien überein." + } + }, "attributes": "Attribute", "editAttributes": "Attribute bearbeiten", "editOptions": "Optionen bearbeiten", diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index f4192da83f311..3cd502ee3d162 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -298,6 +298,9 @@ } }, "filters": { + "sortLabel": "Sort", + "filterLabel": "Filter", + "searchLabel": "Search", "date": { "today": "Today", "lastSevenDays": "Last 7 days", @@ -306,7 +309,9 @@ "lastTwelveMonths": "Last 12 months", "custom": "Custom", "from": "From", - "to": "To" + "to": "To", + "starting": "Starting", + "ending": "Ending" }, "compare": { "lessThan": "Less than", @@ -317,6 +322,18 @@ "greaterThanLabel": "greater than {{value}}", "andLabel": "and" }, + "sorting": { + "alphabeticallyAsc": "A to Z", + "alphabeticallyDesc": "Z to A", + "dateAsc": "Newest first", + "dateDesc": "Oldest first" + }, + "radio": { + "yes": "Yes", + "no": "No", + "true": "True", + "false": "False" + }, "addFilter": "Add filter" }, "errorBoundary": { @@ -468,7 +485,17 @@ } }, "deleteWarning": "You are about to delete the product {{title}}. This action cannot be undone.", - "variants": "Variants", + "variants": { + "header": "Variants", + "empty": { + "heading": "No variants", + "description": "There are no variants to display." + }, + "filtered": { + "heading": "No results", + "description": "No variants match the current filter criteria." + } + }, "attributes": "Attributes", "editAttributes": "Edit Attributes", "editOptions": "Edit Options", @@ -891,6 +918,16 @@ "customerGroups": { "domain": "Customer Groups", "subtitle": "Organize customers into groups. Groups can have different promotions and prices.", + "list": { + "empty": { + "heading": "No customer groups", + "description": "There are no customer groups to display." + }, + "filtered": { + "heading": "No results", + "description": "No customer groups match the current filter criteria." + } + }, "create": { "header": "Create Customer Group", "hint": "Create a new customer group to segment your customers.", diff --git a/packages/admin/dashboard/src/i18n/translations/es.json b/packages/admin/dashboard/src/i18n/translations/es.json index 7250824079a20..7504cf2157204 100644 --- a/packages/admin/dashboard/src/i18n/translations/es.json +++ b/packages/admin/dashboard/src/i18n/translations/es.json @@ -316,6 +316,12 @@ "greaterThanLabel": "mayor que {{value}}", "andLabel": "y" }, + "radio": { + "yes": "Sí", + "no": "No", + "true": "Verdadero", + "false": "Falso" + }, "addFilter": "Agregar filtro" }, "errorBoundary": { @@ -467,7 +473,17 @@ } }, "deleteWarning": "Estás a punto de eliminar el producto {{title}}. Esta acción no puede deshacerse.", - "variants": "Variantes", + "variants": { + "header": "Variantes", + "empty": { + "heading": "No hay variantes", + "description": "No hay variantes para mostrar." + }, + "filtered": { + "heading": "No hay resultados", + "description": "No hay variantes que coincidan con los criterios de filtro actuales." + } + }, "attributes": "Atributos", "editAttributes": "Editar Atributos", "editOptions": "Editar Opciones", diff --git a/packages/admin/dashboard/src/i18n/translations/fr.json b/packages/admin/dashboard/src/i18n/translations/fr.json index 300bfc73afc40..1246457f4d594 100644 --- a/packages/admin/dashboard/src/i18n/translations/fr.json +++ b/packages/admin/dashboard/src/i18n/translations/fr.json @@ -1,381 +1,387 @@ { - "$schema": "./$schema.json", - "general": { - "ascending": "Ascendant", - "descending": "Descendant", - "add": "Ajouter", - "start": "Début", - "end": "Fin", - "open": "Ouvrir", - "close": "Fermer", - "apply": "Appliquer", - "range": "Plage", - "search": "Rechercher", - "of": "de", - "results": "résultats", - "pages": "pages", - "next": "Suivant", - "prev": "Précédent", - "is": "est", - "timeline": "Chronologie", - "success": "Succès", - "warning": "Attention", - "tip": "Astuce", - "error": "Erreur", - "select": "Sélectionner", - "selected": "Sélectionné", - "enabled": "Activé", - "disabled": "Désactivé", - "expired": "Expiré", - "active": "Actif", - "revoked": "Revoqué", - "new": "Nouveau", - "modified": "Modifié", - "added": "Ajouté", - "removed": "Supprimé", - "admin": "Administrateur", - "store": "Boutique", - "details": "Détails", - "items_one": "{{count}} item", - "items_other": "{{count}} items", - "countSelected": "{{count}} sélectionné", - "countOfTotalSelected": "{{count}} sur {{total}} sélectionnés", - "plusCount": "+ {{count}}", - "plusCountMore": "+ {{count}} plus", - "areYouSure": "Êtes-vous sûr ?", - "noRecordsFound": "Aucun enregistrement trouvé", - "typeToConfirm": "Veuillez entrer {{val}} pour confirmer :", - "noResultsTitle": "Aucun résultat", - "noResultsMessage": "Essayez de changer les filtres ou la requête de recherche", - "noSearchResults": "Aucun résultat de recherche", - "noSearchResultsFor": "Aucun résultat de recherche pour <0>'{{query}}'", - "noRecordsTitle": "Aucun enregistrement", - "noRecordsMessage": "Il n'y a pas d'enregistrement à afficher", - "unsavedChangesTitle": "Êtes-vous sûr de vouloir quitter ce formulaire ?", - "unsavedChangesDescription": "Vous avez des modifications non enregistrées qui seront perdues si vous quittez ce formulaire.", - "includesTaxTooltip": "Les prix de cette colonne sont TTC (toutes taxes comprises).", - "excludesTaxTooltip": "Les prix de cette colonne sont HT (Hors Taxes).", - "noMoreData": "Aucune donnée supplémentaire" - }, - "json": { - "header": "JSON", - "numberOfKeys_one": "{{count}} clé", - "numberOfKeys_other": "{{count}} clés", - "drawer": { - "header_one": "JSON <0>· {{count}} clé", - "header_other": "JSON <0>· {{count}} clés", - "description": "Voir les données JSON pour cet objet." - } - }, - "metadata": { - "header": "Metadata", - "numberOfKeys_one": "{{count}} clé", - "numberOfKeys_other": "{{count}} clés", - "edit": { - "header": "Modifier les métadonnées", - "description": "Modifier les métadonnées pour cet objet.", - "successToast": "Les métadonnées ont été mises à jour avec succès.", - "actions": { - "insertRowAbove": "Insérer une ligne au-dessus", - "insertRowBelow": "Insérer une ligne en dessous", - "deleteRow": "Supprimer la ligne" - }, - "labels": { - "key": "Clé", - "value": "Valeur" - }, - "complexRow": { - "label": "Certaines lignes sont désactivées", - "description": "Cet objet contient des métadonnées non-primitives, telles que des tableaux ou des objets, qui ne peuvent pas être modifiés ici. Pour modifier les lignes désactivées, utilisez l'API directement.", - "tooltip": "Cette ligne est désactivée car elle contient des données non primitives." - } - } - }, - "validation": { - "mustBeInt": "La valeur doit être un nombre entier.", - "mustBePositive": "La valeur doit être un nombre positif." - }, - "actions": { - "save": "Enregistrer", - "saveAsDraft": "Enregistrer comme brouillon", - "copy": "Copier", - "copied": "Copié", - "duplicate": "Dupliquer", - "publish": "Publier", - "create": "Créer", - "delete": "Supprimer", - "remove": "Retirer", - "revoke": "Révoquer", - "cancel": "Annuler", - "forceConfirm": "Forcer la confirmation", - "continueEdit": "Continuer l'édition", - "enable": "Activer", - "disable": "Désactiver", - "undo": "Annuler", - "complete": "Terminer", - "viewDetails": "Voir les détails", - "back": "Retour", - "close": "Fermer", - "showMore": "Voir plus", - "continue": "Continuer", - "continueWithEmail": "Continuer avec l'email", - "idCopiedToClipboard": "ID copié dans le presse-papier", - "addReason": "Ajouter une raison", - "addNote": "Ajouter une note", - "reset": "Réinitialiser", - "confirm": "Confirmer", - "edit": "Modifier", - "addItems": "Ajouter des éléments", - "download": "Télécharger", - "clear": "Effacer", - "clearAll": "Tout effacer", - "apply": "Appliquer", - "add": "Ajouter", - "select": "Sélectionner", - "browse": "Parcourir", - "logout": "Déconnexion", - "hide": "Masquer", - "export": "Exporter", - "import": "Importer", - "cannotUndo": "Cette action ne peut pas être annulée" - }, - "operators": { - "in": "inclus" - }, - "app": { - "search": { - "label": "Rechercher", - "title": "Recherche", - "description": "Recherchez dans l'ensemble de votre boutique, y compris les commandes, les produits, les clients et plus encore.", - "allAreas": "Toutes les zones", - "navigation": "Navigation", - "openResult": "Ouvrir le résultat", - "showMore": "Voir plus", - "placeholder": "Accéder à ou rechercher n'importe quoi...", - "noResultsTitle": "Aucun résultat trouvé", - "noResultsMessage": "Nous n'avons trouvé aucun élément correspondant à votre recherche.", - "emptySearchTitle": "Tapez pour rechercher", - "emptySearchMessage": "Entrez un mot-clé ou une phrase pour explorer.", - "loadMore": "Charger {{count}} de plus", - "groups": { - "all": "Toutes les zones", - "customer": "Clients", - "customerGroup": "Groupes de clients", - "product": "Produits", - "productVariant": "Variantes de produits", - "inventory": "Inventaire", - "reservation": "Réservations", - "category": "Catégories", - "collection": "Collections", - "order": "Commandes", - "promotion": "Promotions", - "campaign": "Campagnes", - "priceList": "Listes de prix", - "user": "Utilisateurs", - "region": "Régions", - "taxRegion": "Régions fiscales", - "returnReason": "Motifs de retour", - "salesChannel": "Canaux de vente", - "productType": "Types de produits", - "productTag": "Tags de produits", - "location": "Emplacements", - "shippingProfile": "Profils d'expédition", - "publishableApiKey": "Clés API publiques", - "secretApiKey": "Clés API secrètes", - "command": "Commandes", - "navigation": "Navigation" - } - }, - "keyboardShortcuts": { - "pageShortcut": "Sauter à", - "settingShortcut": "Paramètres", - "commandShortcut": "Commandes", - "then": "alors", - "navigation": { - "goToOrders": "Commandes", - "goToProducts": "Produits", - "goToCollections": "Collections", - "goToCategories": "Catégories", - "goToCustomers": "Clients", - "goToCustomerGroups": "Groupes de clients", - "goToInventory": "Inventaire", - "goToReservations": "Réservations", - "goToPriceLists": "Listes de prix", - "goToPromotions": "Promotions", - "goToCampaigns": "Campagnes" - }, - "settings": { - "goToSettings": "Paramètres", - "goToStore": "Boutique", - "goToUsers": "Utilisateurs", - "goToRegions": "Régions", - "goToTaxRegions": "Régions fiscales", - "goToSalesChannels": "Canaux de vente", - "goToProductTypes": "Types de produits", - "goToLocations": "Emplacements", - "goToPublishableApiKeys": "Clés API publiques", - "goToSecretApiKeys": "Clés API secrètes", - "goToWorkflows": "Workflows", - "goToProfile": "Profil", - "goToReturnReasons": "Motifs de retour" - } - }, - "menus": { - "user": { - "documentation": "Documentation", - "changelog": "Changelog", - "shortcuts": "Raccourcis", - "profileSettings": "Paramètres du profil", - "theme": { - "label": "Thème", - "dark": "Dark", - "light": "Light", - "system": "Système" - } - }, - "store": { - "label": "Boutique", - "storeSettings": "Paramètres de la boutique" - }, - "actions": { - "logout": "Déconnexion" - } - }, - "nav": { - "accessibility": { - "title": "Navigation", - "description": "Menu de navigation pour le tableau de bord." - }, - "common": { - "extensions": "Extensions" - }, - "main": { - "store": "Boutique", - "storeSettings": "Paramètres de la boutique" - }, - "settings": { - "header": "Paramètres", - "general": "General", - "developer": "Développeur", - "myAccount": "Mon compte" - } - } - }, - "dataGrid": { - "columns": { - "view": "Affichage", - "resetToDefault": "Réinitialiser à la valeur par défaut", - "disabled": "Le changement de colonnes visibles est désactivé." - }, - "shortcuts": { - "label": "Raccourcis", - "commands": { - "undo": "Annuler", - "redo": "Retour", - "copy": "Copier", - "paste": "Coller", - "edit": "Modifier", - "delete": "Supprimer", - "clear": "Effacer", - "moveUp": "Déplacer vers le haut", - "moveDown": "Déplacer vers le bas", - "moveLeft": "Déplacer vers la gauche", - "moveRight": "Déplacer vers la droite", - "moveTop": "Déplacer en haut", - "moveBottom": "Déplacer en bas", - "selectDown": "Sélectionner vers le bas", - "selectUp": "Sélectionner vers le haut", - "selectColumnDown": "Sélectionner la colonne vers le bas", - "selectColumnUp": "Sélectionner la colonne vers le haut", - "focusToolbar": "Focus barre d'outils", - "focusCancel": "Annuler le focus" - } - }, - "errors": { - "fixError": "Corriger l'erreur", - "count_one": "{{count}} erreur", - "count_other": "{{count}} erreurs" - } - }, - "filters": { - "date": { - "today": "Aujourd'hui", - "lastSevenDays": "7 derniers jours", - "lastThirtyDays": "30 derniers jours", - "lastNinetyDays": "90 derniers jours", - "lastTwelveMonths": "12 derniers mois", - "custom": "Personnalisé", - "from": "De", - "to": "À" - }, - "compare": { - "lessThan": "Moins de", - "greaterThan": "Plus grand que", - "exact": "Exact", - "range": "Plage", - "lessThanLabel": "moins de {{value}}", - "greaterThanLabel": "plus grand que {{value}}", - "andLabel": "et" - }, - "addFilter": "Ajouter un filtre" - }, - "errorBoundary": { - "badRequestTitle": "400 - Requête incorrecte", - "badRequestMessage": "La requête n'a pas pu être interprétée par le serveur en raison d'une syntaxe incorrecte.", - "notFoundTitle": "404 - Il n'y a pas de page à cette adresse", - "notFoundMessage": "Vérifiez l'URL et réessayez, ou utilisez la barre de recherche pour trouver ce que vous cherchez.", - "internalServerErrorTitle": "500 - Erreur interne du serveur", - "internalServerErrorMessage": "Une erreur inattendue est survenue sur le serveur. Veuillez réessayer plus tard.", - "defaultTitle": "Une erreur est survenue", - "defaultMessage": "Une erreur inattendue est survenue lors du rendu de cette page.", - "noMatchMessage": "La page que vous recherchez n'existe pas.", - "backToDashboard": "Retour au tableau de bord" - }, - "addresses": { - "shippingAddress": { - "header": "Adresse de livraison", - "editHeader": "Modifier l'adresse de livraison", - "editLabel": "Adresse de livraison", - "label": "Adresse de livraison" - }, - "billingAddress": { - "header": "Adresse de facturation", - "editHeader": "Modifier l'adresse de facturation", - "editLabel": "Adresse de facturation", - "label": "Adresse de facturation", - "sameAsShipping": "Identique à l'adresse de livraison" - }, - "contactHeading": "Contact", - "locationHeading": "Emplacement" - }, - "email": { - "editHeader": "Modifier l'email", - "editLabel": "Email", - "label": "Email" - }, - "transferOwnership": { - "header": "Transférer la propriété", - "label": "Transférer la propriété", - "details": { - "order": "Détails de la commande", - "draft": "Détails du brouillon" - }, - "currentOwner": { - "label": "Propriétaire actuel", - "hint": "Le propriétaire actuel de la commande." - }, - "newOwner": { - "label": "Nouveau propriétaire", - "hint": "Le nouveau propriétaire à transférer la commande à." - }, - "validation": { - "mustBeDifferent": "Le nouveau propriétaire doit être différent du propriétaire actuel.", - "required": "Le nouveau propriétaire est requis." - } - }, - "sales_channels": { - "availableIn": "Disponible dans <0>{{x}} canaux de vente sur <1>{{y}}" - }, + "$schema": "./$schema.json", + "general": { + "ascending": "Ascendant", + "descending": "Descendant", + "add": "Ajouter", + "start": "Début", + "end": "Fin", + "open": "Ouvrir", + "close": "Fermer", + "apply": "Appliquer", + "range": "Plage", + "search": "Rechercher", + "of": "de", + "results": "résultats", + "pages": "pages", + "next": "Suivant", + "prev": "Précédent", + "is": "est", + "timeline": "Chronologie", + "success": "Succès", + "warning": "Attention", + "tip": "Astuce", + "error": "Erreur", + "select": "Sélectionner", + "selected": "Sélectionné", + "enabled": "Activé", + "disabled": "Désactivé", + "expired": "Expiré", + "active": "Actif", + "revoked": "Revoqué", + "new": "Nouveau", + "modified": "Modifié", + "added": "Ajouté", + "removed": "Supprimé", + "admin": "Administrateur", + "store": "Boutique", + "details": "Détails", + "items_one": "{{count}} item", + "items_other": "{{count}} items", + "countSelected": "{{count}} sélectionné", + "countOfTotalSelected": "{{count}} sur {{total}} sélectionnés", + "plusCount": "+ {{count}}", + "plusCountMore": "+ {{count}} plus", + "areYouSure": "Êtes-vous sûr ?", + "noRecordsFound": "Aucun enregistrement trouvé", + "typeToConfirm": "Veuillez entrer {{val}} pour confirmer :", + "noResultsTitle": "Aucun résultat", + "noResultsMessage": "Essayez de changer les filtres ou la requête de recherche", + "noSearchResults": "Aucun résultat de recherche", + "noSearchResultsFor": "Aucun résultat de recherche pour <0>'{{query}}'", + "noRecordsTitle": "Aucun enregistrement", + "noRecordsMessage": "Il n'y a pas d'enregistrement à afficher", + "unsavedChangesTitle": "Êtes-vous sûr de vouloir quitter ce formulaire ?", + "unsavedChangesDescription": "Vous avez des modifications non enregistrées qui seront perdues si vous quittez ce formulaire.", + "includesTaxTooltip": "Les prix de cette colonne sont TTC (toutes taxes comprises).", + "excludesTaxTooltip": "Les prix de cette colonne sont HT (Hors Taxes).", + "noMoreData": "Aucune donnée supplémentaire" + }, + "json": { + "header": "JSON", + "numberOfKeys_one": "{{count}} clé", + "numberOfKeys_other": "{{count}} clés", + "drawer": { + "header_one": "JSON <0>· {{count}} clé", + "header_other": "JSON <0>· {{count}} clés", + "description": "Voir les données JSON pour cet objet." + } + }, + "metadata": { + "header": "Metadata", + "numberOfKeys_one": "{{count}} clé", + "numberOfKeys_other": "{{count}} clés", + "edit": { + "header": "Modifier les métadonnées", + "description": "Modifier les métadonnées pour cet objet.", + "successToast": "Les métadonnées ont été mises à jour avec succès.", + "actions": { + "insertRowAbove": "Insérer une ligne au-dessus", + "insertRowBelow": "Insérer une ligne en dessous", + "deleteRow": "Supprimer la ligne" + }, + "labels": { + "key": "Clé", + "value": "Valeur" + }, + "complexRow": { + "label": "Certaines lignes sont désactivées", + "description": "Cet objet contient des métadonnées non-primitives, telles que des tableaux ou des objets, qui ne peuvent pas être modifiés ici. Pour modifier les lignes désactivées, utilisez l'API directement.", + "tooltip": "Cette ligne est désactivée car elle contient des données non primitives." + } + } + }, + "validation": { + "mustBeInt": "La valeur doit être un nombre entier.", + "mustBePositive": "La valeur doit être un nombre positif." + }, + "actions": { + "save": "Enregistrer", + "saveAsDraft": "Enregistrer comme brouillon", + "copy": "Copier", + "copied": "Copié", + "duplicate": "Dupliquer", + "publish": "Publier", + "create": "Créer", + "delete": "Supprimer", + "remove": "Retirer", + "revoke": "Révoquer", + "cancel": "Annuler", + "forceConfirm": "Forcer la confirmation", + "continueEdit": "Continuer l'édition", + "enable": "Activer", + "disable": "Désactiver", + "undo": "Annuler", + "complete": "Terminer", + "viewDetails": "Voir les détails", + "back": "Retour", + "close": "Fermer", + "showMore": "Voir plus", + "continue": "Continuer", + "continueWithEmail": "Continuer avec l'email", + "idCopiedToClipboard": "ID copié dans le presse-papier", + "addReason": "Ajouter une raison", + "addNote": "Ajouter une note", + "reset": "Réinitialiser", + "confirm": "Confirmer", + "edit": "Modifier", + "addItems": "Ajouter des éléments", + "download": "Télécharger", + "clear": "Effacer", + "clearAll": "Tout effacer", + "apply": "Appliquer", + "add": "Ajouter", + "select": "Sélectionner", + "browse": "Parcourir", + "logout": "Déconnexion", + "hide": "Masquer", + "export": "Exporter", + "import": "Importer", + "cannotUndo": "Cette action ne peut pas être annulée" + }, + "operators": { + "in": "inclus" + }, + "app": { + "search": { + "label": "Rechercher", + "title": "Recherche", + "description": "Recherchez dans l'ensemble de votre boutique, y compris les commandes, les produits, les clients et plus encore.", + "allAreas": "Toutes les zones", + "navigation": "Navigation", + "openResult": "Ouvrir le résultat", + "showMore": "Voir plus", + "placeholder": "Accéder à ou rechercher n'importe quoi...", + "noResultsTitle": "Aucun résultat trouvé", + "noResultsMessage": "Nous n'avons trouvé aucun élément correspondant à votre recherche.", + "emptySearchTitle": "Tapez pour rechercher", + "emptySearchMessage": "Entrez un mot-clé ou une phrase pour explorer.", + "loadMore": "Charger {{count}} de plus", + "groups": { + "all": "Toutes les zones", + "customer": "Clients", + "customerGroup": "Groupes de clients", + "product": "Produits", + "productVariant": "Variantes de produits", + "inventory": "Inventaire", + "reservation": "Réservations", + "category": "Catégories", + "collection": "Collections", + "order": "Commandes", + "promotion": "Promotions", + "campaign": "Campagnes", + "priceList": "Listes de prix", + "user": "Utilisateurs", + "region": "Régions", + "taxRegion": "Régions fiscales", + "returnReason": "Motifs de retour", + "salesChannel": "Canaux de vente", + "productType": "Types de produits", + "productTag": "Tags de produits", + "location": "Emplacements", + "shippingProfile": "Profils d'expédition", + "publishableApiKey": "Clés API publiques", + "secretApiKey": "Clés API secrètes", + "command": "Commandes", + "navigation": "Navigation" + } + }, + "keyboardShortcuts": { + "pageShortcut": "Sauter à", + "settingShortcut": "Paramètres", + "commandShortcut": "Commandes", + "then": "alors", + "navigation": { + "goToOrders": "Commandes", + "goToProducts": "Produits", + "goToCollections": "Collections", + "goToCategories": "Catégories", + "goToCustomers": "Clients", + "goToCustomerGroups": "Groupes de clients", + "goToInventory": "Inventaire", + "goToReservations": "Réservations", + "goToPriceLists": "Listes de prix", + "goToPromotions": "Promotions", + "goToCampaigns": "Campagnes" + }, + "settings": { + "goToSettings": "Paramètres", + "goToStore": "Boutique", + "goToUsers": "Utilisateurs", + "goToRegions": "Régions", + "goToTaxRegions": "Régions fiscales", + "goToSalesChannels": "Canaux de vente", + "goToProductTypes": "Types de produits", + "goToLocations": "Emplacements", + "goToPublishableApiKeys": "Clés API publiques", + "goToSecretApiKeys": "Clés API secrètes", + "goToWorkflows": "Workflows", + "goToProfile": "Profil", + "goToReturnReasons": "Motifs de retour" + } + }, + "menus": { + "user": { + "documentation": "Documentation", + "changelog": "Changelog", + "shortcuts": "Raccourcis", + "profileSettings": "Paramètres du profil", + "theme": { + "label": "Thème", + "dark": "Dark", + "light": "Light", + "system": "Système" + } + }, + "store": { + "label": "Boutique", + "storeSettings": "Paramètres de la boutique" + }, + "actions": { + "logout": "Déconnexion" + } + }, + "nav": { + "accessibility": { + "title": "Navigation", + "description": "Menu de navigation pour le tableau de bord." + }, + "common": { + "extensions": "Extensions" + }, + "main": { + "store": "Boutique", + "storeSettings": "Paramètres de la boutique" + }, + "settings": { + "header": "Paramètres", + "general": "General", + "developer": "Développeur", + "myAccount": "Mon compte" + } + } + }, + "dataGrid": { + "columns": { + "view": "Affichage", + "resetToDefault": "Réinitialiser à la valeur par défaut", + "disabled": "Le changement de colonnes visibles est désactivé." + }, + "shortcuts": { + "label": "Raccourcis", + "commands": { + "undo": "Annuler", + "redo": "Retour", + "copy": "Copier", + "paste": "Coller", + "edit": "Modifier", + "delete": "Supprimer", + "clear": "Effacer", + "moveUp": "Déplacer vers le haut", + "moveDown": "Déplacer vers le bas", + "moveLeft": "Déplacer vers la gauche", + "moveRight": "Déplacer vers la droite", + "moveTop": "Déplacer en haut", + "moveBottom": "Déplacer en bas", + "selectDown": "Sélectionner vers le bas", + "selectUp": "Sélectionner vers le haut", + "selectColumnDown": "Sélectionner la colonne vers le bas", + "selectColumnUp": "Sélectionner la colonne vers le haut", + "focusToolbar": "Focus barre d'outils", + "focusCancel": "Annuler le focus" + } + }, + "errors": { + "fixError": "Corriger l'erreur", + "count_one": "{{count}} erreur", + "count_other": "{{count}} erreurs" + } + }, + "filters": { + "date": { + "today": "Aujourd'hui", + "lastSevenDays": "7 derniers jours", + "lastThirtyDays": "30 derniers jours", + "lastNinetyDays": "90 derniers jours", + "lastTwelveMonths": "12 derniers mois", + "custom": "Personnalisé", + "from": "De", + "to": "À" + }, + "compare": { + "lessThan": "Moins de", + "greaterThan": "Plus grand que", + "exact": "Exact", + "range": "Plage", + "lessThanLabel": "moins de {{value}}", + "greaterThanLabel": "plus grand que {{value}}", + "andLabel": "et" + }, + "radio": { + "yes": "Oui", + "no": "Non", + "true": "Vrai", + "false": "Faux" + }, + "addFilter": "Ajouter un filtre" + }, + "errorBoundary": { + "badRequestTitle": "400 - Requête incorrecte", + "badRequestMessage": "La requête n'a pas pu être interprétée par le serveur en raison d'une syntaxe incorrecte.", + "notFoundTitle": "404 - Il n'y a pas de page à cette adresse", + "notFoundMessage": "Vérifiez l'URL et réessayez, ou utilisez la barre de recherche pour trouver ce que vous cherchez.", + "internalServerErrorTitle": "500 - Erreur interne du serveur", + "internalServerErrorMessage": "Une erreur inattendue est survenue sur le serveur. Veuillez réessayer plus tard.", + "defaultTitle": "Une erreur est survenue", + "defaultMessage": "Une erreur inattendue est survenue lors du rendu de cette page.", + "noMatchMessage": "La page que vous recherchez n'existe pas.", + "backToDashboard": "Retour au tableau de bord" + }, + "addresses": { + "shippingAddress": { + "header": "Adresse de livraison", + "editHeader": "Modifier l'adresse de livraison", + "editLabel": "Adresse de livraison", + "label": "Adresse de livraison" + }, + "billingAddress": { + "header": "Adresse de facturation", + "editHeader": "Modifier l'adresse de facturation", + "editLabel": "Adresse de facturation", + "label": "Adresse de facturation", + "sameAsShipping": "Identique à l'adresse de livraison" + }, + "contactHeading": "Contact", + "locationHeading": "Emplacement" + }, + "email": { + "editHeader": "Modifier l'email", + "editLabel": "Email", + "label": "Email" + }, + "transferOwnership": { + "header": "Transférer la propriété", + "label": "Transférer la propriété", + "details": { + "order": "Détails de la commande", + "draft": "Détails du brouillon" + }, + "currentOwner": { + "label": "Propriétaire actuel", + "hint": "Le propriétaire actuel de la commande." + }, + "newOwner": { + "label": "Nouveau propriétaire", + "hint": "Le nouveau propriétaire à transférer la commande à." + }, + "validation": { + "mustBeDifferent": "Le nouveau propriétaire doit être différent du propriétaire actuel.", + "required": "Le nouveau propriétaire est requis." + } + }, + "sales_channels": { + "availableIn": "Disponible dans <0>{{x}} canaux de vente sur <1>{{y}}" + }, "products": { "domain": "Produits", "list": { @@ -468,465 +474,475 @@ } }, "deleteWarning": "Vous êtes sur le point de supprimer le produit {{title}}. Cette action ne peut être annulée.", - "variants": "Variantes", - "attributes": "Attributs", - "editAttributes": "Modifier les attributs", - "editOptions": "Modifier les options", - "editPrices": "Modifier les prix", - "media": { - "label": "Media", - "editHint": "Ajoutez des médias au produit pour le mettre en avant dans votre boutique.", - "makeThumbnail": "Faire de la vignette", - "uploadImagesLabel": "Télécharger des images", - "uploadImagesHint": "Glisser-déposer des images ici ou cliquer pour télécharger.", - "invalidFileType": "'{{name}}' n'est pas un type de fichier supporté. Les types de fichiers supportés sont : {{types}}.", - "failedToUpload": "Erreur lors du téléchargement des médias ajoutés. Veuillez réessayer.", - "deleteWarning_one": "Vous êtes sur le point de supprimer {{count}} image. Cette action ne peut être annulée.", - "deleteWarning_other": "Vous êtes sur le point de supprimer {{count}} images. Cette action ne peut être annulée.", - "deleteWarningWithThumbnail_one": "Vous êtes sur le point de supprimer {{count}} image incluant la vignette. Cette action ne peut être annulée.", - "deleteWarningWithThumbnail_other": "Vous êtes sur le point de supprimer {{count}} images incluant la vignette. Cette action ne peut être annulée.", - "thumbnailTooltip": "Vignette", - "galleryLabel": "Galerie", - "downloadImageLabel": "Télécharger l'image actuelle", - "deleteImageLabel": "Supprimer l'image actuelle", - "emptyState": { - "header": "Aucun média pour le moment", - "description": "Ajoutez des médias au produit pour le mettre en avant dans votre boutique.", - "action": "Ajouter des médias" - }, - "successToast": "Le média à été mis à jour avec succès." - }, - "discountableHint": "Lorsque la case n'est pas cochée, les remises ne seront pas appliquées à ce produit.", - "noSalesChannels": "Non disponible dans aucun canal de vente", - "variantCount_one": "{{count}} variante", - "variantCount_other": "{{count}} variantes", - "deleteVariantWarning": "Vous êtes sur le point de supprimer la variante {{title}}. Cette action ne peut être annulée.", - "productStatus": { - "draft": "Brouillon", - "published": "Publié", - "proposed": "Proposé", - "rejected": "Rejeté" - }, - "fields": { - "title": { - "label": "Titre", - "hint": "Donnez à votre produit un titre court et clair.<0/>50-60 caractères est la longueur recommandée pour les moteurs de recherche." - }, - "subtitle": { - "label": "Sous-titre" - }, - "handle": { - "label": "Handle", - "tooltip": "Le handle est utilisé pour faire référence au produit dans votre boutique. Si non spécifié, le handle sera généré à partir du titre du produit." - }, - "description": { - "label": "Description", - "hint": "Donnez à votre produit une description courte et claire.<0/>120-160 caractères est la longueur recommandée pour les moteurs de recherche." - }, - "discountable": { - "label": "Discountable", - "hint": "Lorsque la case n'est pas cochée, les remises ne seront pas appliquées à ce produit" - }, - "type": { - "label": "Type" - }, - "collection": { - "label": "Collection" - }, - "categories": { - "label": "Catégories" - }, - "tags": { - "label": "Tags" - }, - "sales_channels": { - "label": "Canaux de vente", - "hint": "Ce produit ne sera disponible que dans le canal de vente par défaut si vous ne le modifiez pas." - }, - "countryOrigin": { - "label": "Pays d'origine" - }, - "material": { - "label": "Matériau" - }, - "width": { - "label": "Largeur" - }, - "length": { - "label": "Longueur" - }, - "height": { - "label": "Hauteur" - }, - "weight": { - "label": "Poids" - }, - "options": { - "label": "Options de produit", - "hint": "Les options sont utilisées pour définir la couleur, la taille, etc. du produit", - "add": "Ajouter une option", - "optionTitle": "Titre de l'option", - "optionTitlePlaceholder": "Couleur", - "variations": "Variations (séparées par des virgules)", - "variantionsPlaceholder": "Rouge, Bleu, Vert" - }, - "variants": { - "label": "Variantes de produit", - "hint": "Les variantes non cochées ne seront pas créées, ce classement affectera l'ordre des variantes dans votre boutique." - }, - "mid_code": { - "label": "Code MID" - }, - "hs_code": { - "label": "Code HS" - } - }, - "variant": { - "edit": { - "header": "Modifier la variante", - "success": "Variante de produit modifiée avec succès" - }, - "create": { - "header": "Variant details" - }, - "deleteWarning": "Êtes-vous sûr de vouloir supprimer cette variante ?", - "pricesPagination": "1 - {{current}} de {{total}} prix", - "tableItemAvailable": "{{availableCount}} disponible", - "tableItem_one": "{{availableCount}} disponible à {{locationCount}} emplacement", - "tableItem_other": "{{availableCount}} disponible à {{locationCount}} emplacements", - "inventory": { - "notManaged": "Not managed", - "manageItems": "Gérer les articles d'inventaire", - "notManagedDesc": "L'inventaire n'est pas géré pour cette variante. Activez ‘Gérer l'inventaire’ pour suivre l'inventaire de la variante.", - "manageKit": "Gérer l'inventaire du kit", - "navigateToItem": "Aller à l'article d'inventaire", - "actions": { - "inventoryItems": "Aller à l'article d'inventaire", - "inventoryKit": "Afficher les articles d'inventaire" - }, - "inventoryKit": "Inventaire du kit", - "inventoryKitHint": "Cette variante est composée de plusieurs articles d'inventaire ?", - "validation": { - "itemId": "Veuillez sélectionner un article d'inventaire.", - "quantity": "La quantité est requise. Veuillez entrer un nombre positif." - }, - "header": "Stock & Inventaire", - "editItemDetails": "Modifier les détails de l'article", - "manageInventoryLabel": "Gérer l'inventaire", - "manageInventoryHint": "Lorsque activé, nous ajusterons la quantité d'inventaire pour vous lorsque les commandes et les retours sont créés.", - "allowBackordersLabel": "Autoriser les commandes partielles", - "allowBackordersHint": "Lorsque activé, les clients peuvent acheter la variante même s'il n'y a pas de quantité disponible.", - "toast": { - "levelsBatch": "Inventaire mis à jour.", - "update": "Article d'inventaire mis à jour avec succès.", - "updateLevel": "Inventaire mis à jour avec succès.", - "itemsManageSuccess": "Articles d'inventaire mis à jour avec succès." - } - } - }, - "options": { - "header": "Options", - "edit": { - "header": "Modifier l'option", - "successToast": "Option {{title}} modifiée avec succès." - }, - "create": { - "header": "Créer une option", - "successToast": "Option {{title}} créée avec succès." - }, - "deleteWarning": "Vous êtes sur le point de supprimer l'option : {{title}}. Cette action ne peut être annulée." - }, - "organization": { - "header": "Organisation", - "edit": { - "header": "Modifier l'organisation", - "toasts": { - "success": "Organisation de {{title}} modifiée avec succès." - } - } - }, - "toasts": { - "delete": { - "success": { - "header": "Produit supprimé", - "description": "{{title}} a été supprimé avec succès." - }, - "error": { - "header": "Erreur lors de la suppression du produit" - } - } - } - }, - "collections": { - "domain": "Collections", - "subtitle": "Organisez vos produits en collections.", - "createCollection": "Créer une collection", - "createCollectionHint": "Créer une nouvelle collection pour organiser vos produits.", - "createSuccess": "Collection créée avec succès.", - "editCollection": "Modifier la collection", - "handleTooltip": "Le handle est utilisé pour faire référence à la collection dans votre boutique. Si non spécifié, le handle sera généré à partir du titre de la collection.", - "deleteWarning": "Vous êtes sur le point de supprimer la collection {{title}}. Cette action ne peut être annulée.", - "removeSingleProductWarning": "Vous êtes sur le point de supprimer le produit {{title}} de la collection. Cette action ne peut être annulée.", - "removeProductsWarning_one": "Vous êtes sur le point de supprimer {{count}} produit de la collection. Cette action ne peut être annulée.", - "removeProductsWarning_other": "Vous êtes sur le point de supprimer {{count}} produits de la collection. Cette action ne peut être annulée.", - "products": { - "list": { - "noRecordsMessage": "Il n'y a pas de produits dans la collection." - }, - "add": { - "successToast_one": "Produit ajouté à la collection avec succès.", - "successToast_other": "Produits ajoutés à la collection avec succès." - }, - "remove": { - "successToast_one": "Produit retiré de la collection avec succès.", - "successToast_other": "Produits retirés de la collection avec succès." - } - } - }, - "categories": { - "domain": "Categories", - "subtitle": "Organisez vos produits en catégories, et gérez l'ordre et la hiérarchie de ces catégories.", - "create": { - "header": "Créer une catégorie", - "hint": "Create a new category to organize your products.", - "tabs": { - "details": "Détails", - "organize": "Organiser par ordre" - }, - "successToast": "Catégorie {{name}} créée avec succès." - }, - "edit": { - "header": "Editer une catégorie", - "description": "Modifier la catégorie pour mettre à jour ses détails.", - "successToast": "Catégorie modifiée avec succès." - }, - "delete": { - "confirmation": "Vous êtes sur le point de supprimer la catégorie {{name}}. Cette action ne peut être annulée.", - "successToast": "Catégorie {{name}} supprimée avec succès." - }, - "products": { - "add": { - "disabledTooltip": "Le produit est déjà dans cette catégorie.", - "successToast_one": "Produit ajouté à la catégorie avec succès.", - "successToast_other": "Produits ajoutés à la catégorie avec succès." - }, - "remove": { - "confirmation_one": "Vous êtes sur le point de supprimer {{count}} produit de la catégorie. Cette action ne peut être annulée.", - "confirmation_other": "Vous êtes sur le point de supprimer {{count}} produits de la catégorie. Cette action ne peut être annulée.", - "successToast_one": "Produit retiré de la catégorie avec succès.", - "successToast_other": "Produits retirés de la catégorie avec succès." - }, - "list": { - "noRecordsMessage": "Il n'y a pas de produits dans la catégorie." - } - }, - "organize": { - "header": "Organiser", - "action": "Modifier l'ordre" - }, - "fields": { - "visibility": { - "label": "Visibility", - "internal": "Interne", - "public": "Publique" - }, - "status": { - "label": "Statut", - "active": "Active", - "inactive": "Inactive" - }, - "path": { - "label": "Chemin", - "tooltip": "Afficher le chemin complet de la catégorie." - }, - "children": { - "label": "Enfants" - }, - "new": { - "label": "Nouveau" - } - } - }, - "inventory": { - "domain": "Inventaire", - "subtitle": "Gérer vos articles d'inventaire", - "reserved": "Réservé", - "available": "Disponible", - "locationLevels": "Emplacements", - "associatedVariants": "Variantes associées", - "manageLocations": "Gérer les emplacements", - "deleteWarning": "Vous êtes sur le point de supprimer un article d'inventaire. Cette action ne peut être annulée.", - "editItemDetails": "Modifier les détails de l'article", - "create": { - "title": "Créer un article d'inventaire", - "details": "Details", - "availability": "Disponibilité", - "locations": "Emplacements", - "attributes": "Attributs", - "requiresShipping": "Nécessite l'expédition", - "requiresShippingHint": "Le produit d'inventaire nécessite-t-il une expédition ?", - "successToast": "Article d'inventaire créé avec succès." - }, - "reservation": { - "header": "Reservation of {{itemName}}", - "editItemDetails": "Modifier la réservation", - "lineItemId": "ID de ligne", - "orderID": "ID de la commande", - "description": "Description", - "location": "Emplacement", - "inStockAtLocation": "En stock à cet emplacement", - "availableAtLocation": "Disponible à cet emplacement", - "reservedAtLocation": "Réservé à cet emplacement", - "reservedAmount": "Montant réservé", - "create": "Créer une réservation", - "itemToReserve": "Article à réserver", - "quantityPlaceholder": "Combien voulez-vous réserver ?", - "descriptionPlaceholder": "Quel type de réservation est celle-ci ?", - "successToast": "Réservation créée avec succès.", - "updateSuccessToast": "Réservation modifiée avec succès.", - "deleteSuccessToast": "Réservation supprimée avec succès.", - "errors": { - "noAvaliableQuantity": "Emplacement de stock sans quantité disponible.", - "quantityOutOfRange": "Quantité minimale est 1 et maximum est {{max}}" - } - }, - "toast": { - "updateLocations": "Locations updated successfully.", - "updateLevel": "Inventaire mis à jour avec succès.", - "updateItem": "Article d'inventaire mis à jour avec succès." - } - }, - "giftCards": { - "domain": "Gift Cards", - "editGiftCard": "Modifier la carte cadeau", - "createGiftCard": "Créer une carte cadeau", - "createGiftCardHint": "Créer manuellement une carte cadeau qui peut être utilisée comme moyen de paiement dans votre boutique.", - "selectRegionFirst": "Sélectionnez une région d'abord", - "deleteGiftCardWarning": "Vous êtes sur le point de supprimer la carte cadeau {{code}}. Cette action ne peut être annulée.", - "balanceHigherThanValue": "Le solde ne peut être supérieur au montant initial.", - "balanceLowerThanZero": "Le solde ne peut être négatif.", - "expiryDateHint": "Les pays ont des lois différentes concernant les dates d'expiration des cartes cadeaux. Assurez-vous de vérifier les régulations locales avant de définir une date d'expiration.", - "regionHint": "Changer la région de la carte cadeau changera également sa devise, potentiellement affectant sa valeur monétaire.", - "enabledHint": "Spécifiez si la carte cadeau est activée ou désactivée.", - "balance": "Solde", - "currentBalance": "Solde actuel", - "initialBalance": "Solde initial", - "personalMessage": "Message personnel", - "recipient": "Destinataire" - }, - "customers": { - "domain": "Clients", - "list": { - "noRecordsMessage": "Vos clients apparaîtront ici." - }, - "create": { - "header": "Créer un client", - "hint": "Créer un nouveau client et gérer leurs détails.", - "successToast": "Client {{email}} créé avec succès." - }, - "groups": { - "label": "Groupes de clients", - "remove": "Êtes-vous sûr de vouloir supprimer le client de \"{{name}}\" groupe de clients ?", - "removeMany": "Êtes-vous sûr de vouloir supprimer le client de les groupes de clients suivants : {{groups}} ?", - "alreadyAddedTooltip": "Le client est déjà dans ce groupe de clients.", - "list": { - "noRecordsMessage": "Ce client n'appartient à aucun groupe." - }, - "add": { - "success": "Client ajouté au groupe : {{groups}}.", - "list": { - "noRecordsMessage": "Veuillez créer un groupe de clients d'abord." - } - }, - "removed": { - "success": "Client retiré du groupe : {{groups}}.", - "list": { - "noRecordsMessage": "Veuillez créer un groupe de clients d'abord." - } - } - }, - "edit": { - "header": "Editer un client", - "emailDisabledTooltip": "Le courriel ne peut être modifié pour les clients enregistrés.", - "successToast": "Client {{email}} modifié avec succès." - }, - "delete": { - "title": "Supprimer un client", - "description": "Vous êtes sur le point de supprimer le client {{email}}. Cette action ne peut être annulée.", - "successToast": "Client {{email}} supprimé avec succès." - }, - "fields": { - "guest": "Invité", - "registered": "Enregistré", - "groups": "Groupes" - }, - "registered": "Enregistré", - "guest": "Invité", - "hasAccount": "A un compte" - }, - "customerGroups": { - "domain": "Groupes de clients", - "subtitle": "Organisez les clients en groupes. Les groupes peuvent avoir des promotions et des prix différents.", - "create": { - "header": "Créer un groupe de clients", - "hint": "Créer un nouveau groupe de clients pour segmenter vos clients.", - "successToast": "Groupe de clients {{name}} créé avec succès." - }, - "edit": { - "header": "Editer un groupe de clients", - "successToast": "Groupe de clients {{name}} modifié avec succès." - }, - "delete": { - "title": "Delete Customer Group", - "description": "Vous êtes sur le point de supprimer le groupe de clients {{name}}. Cette action ne peut être annulée.", - "successToast": "Groupe de clients {{name}} supprimé avec succès." - }, - "customers": { - "alreadyAddedTooltip": "The customer has already been added to the group.", - "add": { - "successToast_one": "Client ajouté au groupe avec succès.", - "successToast_other": "Clients ajoutés au groupe avec succès.", - "list": { - "noRecordsMessage": "Créer un client d'abord." - } - }, - "remove": { - "title_one": "Supprimer un client", - "title_other": "Supprimer des clients", - "description_one": "Vous êtes sur le point de supprimer {{count}} client du groupe de clients. Cette action ne peut être annulée.", - "description_other": "Vous êtes sur le point de supprimer {{count}} clients du groupe de clients. Cette action ne peut être annulée." - }, - "list": { - "noRecordsMessage": "Ce groupe n'a pas de clients." - } - } - }, - "orders": { - "domain": "Commandes", - "claim": "Réclamation", - "exchange": "Échange", - "return": "Retour", - "cancelWarning": "Vous êtes sur le point de supprimer la commande {{id}}. Cette action ne peut être annulée.", - "onDateFromSalesChannel": "{{date}} de {{salesChannel}}", - "list": { - "noRecordsMessage": "Vos commandes apparaîtront ici." - }, - "summary": { - "requestReturn": "Demander un retour", - "allocateItems": "Allouer des articles", - "editOrder": "Editer la commande", - "editOrderContinue": "Continuer l'édition de la commande", - "inventoryKit": "Consiste de {{count}}x articles d'inventaire", - "itemTotal": "Total des articles", - "shippingTotal": "Total des frais de livraison", - "discountTotal": "Total des remises", - "taxTotalIncl": "Total des taxes (inclues)", - "itemSubtotal": "Sous-total des articles", - "shippingSubtotal": "Sous-total des frais de livraison", - "discountSubtotal": "Sous-total des remises", - "taxTotal": "Total des taxes" - }, - "transfer": { + "variants": { + "header": "Variantes", + "empty": { + "heading": "Aucune variantes", + "description": "Il n'y a aucune variantes à afficher." + }, + "filtered": { + "heading": "Aucun résultat", + "description": "Aucune variantes ne correspond aux critères de filtre actuel." + } + }, + "attributes": "Attributs", + "editAttributes": "Modifier les attributs", + "editOptions": "Modifier les options", + "editPrices": "Modifier les prix", + "media": { + "label": "Media", + "editHint": "Ajoutez des médias au produit pour le mettre en avant dans votre boutique.", + "makeThumbnail": "Faire de la vignette", + "uploadImagesLabel": "Télécharger des images", + "uploadImagesHint": "Glisser-déposer des images ici ou cliquer pour télécharger.", + "invalidFileType": "'{{name}}' n'est pas un type de fichier supporté. Les types de fichiers supportés sont : {{types}}.", + "failedToUpload": "Erreur lors du téléchargement des médias ajoutés. Veuillez réessayer.", + "deleteWarning_one": "Vous êtes sur le point de supprimer {{count}} image. Cette action ne peut être annulée.", + "deleteWarning_other": "Vous êtes sur le point de supprimer {{count}} images. Cette action ne peut être annulée.", + "deleteWarningWithThumbnail_one": "Vous êtes sur le point de supprimer {{count}} image incluant la vignette. Cette action ne peut être annulée.", + "deleteWarningWithThumbnail_other": "Vous êtes sur le point de supprimer {{count}} images incluant la vignette. Cette action ne peut être annulée.", + "thumbnailTooltip": "Vignette", + "galleryLabel": "Galerie", + "downloadImageLabel": "Télécharger l'image actuelle", + "deleteImageLabel": "Supprimer l'image actuelle", + "emptyState": { + "header": "Aucun média pour le moment", + "description": "Ajoutez des médias au produit pour le mettre en avant dans votre boutique.", + "action": "Ajouter des médias" + }, + "successToast": "Le média à été mis à jour avec succès." + }, + "discountableHint": "Lorsque la case n'est pas cochée, les remises ne seront pas appliquées à ce produit.", + "noSalesChannels": "Non disponible dans aucun canal de vente", + "variantCount_one": "{{count}} variante", + "variantCount_other": "{{count}} variantes", + "deleteVariantWarning": "Vous êtes sur le point de supprimer la variante {{title}}. Cette action ne peut être annulée.", + "productStatus": { + "draft": "Brouillon", + "published": "Publié", + "proposed": "Proposé", + "rejected": "Rejeté" + }, + "fields": { + "title": { + "label": "Titre", + "hint": "Donnez à votre produit un titre court et clair.<0/>50-60 caractères est la longueur recommandée pour les moteurs de recherche." + }, + "subtitle": { + "label": "Sous-titre" + }, + "handle": { + "label": "Handle", + "tooltip": "Le handle est utilisé pour faire référence au produit dans votre boutique. Si non spécifié, le handle sera généré à partir du titre du produit." + }, + "description": { + "label": "Description", + "hint": "Donnez à votre produit une description courte et claire.<0/>120-160 caractères est la longueur recommandée pour les moteurs de recherche." + }, + "discountable": { + "label": "Discountable", + "hint": "Lorsque la case n'est pas cochée, les remises ne seront pas appliquées à ce produit" + }, + "type": { + "label": "Type" + }, + "collection": { + "label": "Collection" + }, + "categories": { + "label": "Catégories" + }, + "tags": { + "label": "Tags" + }, + "sales_channels": { + "label": "Canaux de vente", + "hint": "Ce produit ne sera disponible que dans le canal de vente par défaut si vous ne le modifiez pas." + }, + "countryOrigin": { + "label": "Pays d'origine" + }, + "material": { + "label": "Matériau" + }, + "width": { + "label": "Largeur" + }, + "length": { + "label": "Longueur" + }, + "height": { + "label": "Hauteur" + }, + "weight": { + "label": "Poids" + }, + "options": { + "label": "Options de produit", + "hint": "Les options sont utilisées pour définir la couleur, la taille, etc. du produit", + "add": "Ajouter une option", + "optionTitle": "Titre de l'option", + "optionTitlePlaceholder": "Couleur", + "variations": "Variations (séparées par des virgules)", + "variantionsPlaceholder": "Rouge, Bleu, Vert" + }, + "variants": { + "label": "Variantes de produit", + "hint": "Les variantes non cochées ne seront pas créées, ce classement affectera l'ordre des variantes dans votre boutique." + }, + "mid_code": { + "label": "Code MID" + }, + "hs_code": { + "label": "Code HS" + } + }, + "variant": { + "edit": { + "header": "Modifier la variante", + "success": "Variante de produit modifiée avec succès" + }, + "create": { + "header": "Variant details" + }, + "deleteWarning": "Êtes-vous sûr de vouloir supprimer cette variante ?", + "pricesPagination": "1 - {{current}} de {{total}} prix", + "tableItemAvailable": "{{availableCount}} disponible", + "tableItem_one": "{{availableCount}} disponible à {{locationCount}} emplacement", + "tableItem_other": "{{availableCount}} disponible à {{locationCount}} emplacements", + "inventory": { + "notManaged": "Not managed", + "manageItems": "Gérer les articles d'inventaire", + "notManagedDesc": "L'inventaire n'est pas géré pour cette variante. Activez ‘Gérer l'inventaire’ pour suivre l'inventaire de la variante.", + "manageKit": "Gérer l'inventaire du kit", + "navigateToItem": "Aller à l'article d'inventaire", + "actions": { + "inventoryItems": "Aller à l'article d'inventaire", + "inventoryKit": "Afficher les articles d'inventaire" + }, + "inventoryKit": "Inventaire du kit", + "inventoryKitHint": "Cette variante est composée de plusieurs articles d'inventaire ?", + "validation": { + "itemId": "Veuillez sélectionner un article d'inventaire.", + "quantity": "La quantité est requise. Veuillez entrer un nombre positif." + }, + "header": "Stock & Inventaire", + "editItemDetails": "Modifier les détails de l'article", + "manageInventoryLabel": "Gérer l'inventaire", + "manageInventoryHint": "Lorsque activé, nous ajusterons la quantité d'inventaire pour vous lorsque les commandes et les retours sont créés.", + "allowBackordersLabel": "Autoriser les commandes partielles", + "allowBackordersHint": "Lorsque activé, les clients peuvent acheter la variante même s'il n'y a pas de quantité disponible.", + "toast": { + "levelsBatch": "Inventaire mis à jour.", + "update": "Article d'inventaire mis à jour avec succès.", + "updateLevel": "Inventaire mis à jour avec succès.", + "itemsManageSuccess": "Articles d'inventaire mis à jour avec succès." + } + } + }, + "options": { + "header": "Options", + "edit": { + "header": "Modifier l'option", + "successToast": "Option {{title}} modifiée avec succès." + }, + "create": { + "header": "Créer une option", + "successToast": "Option {{title}} créée avec succès." + }, + "deleteWarning": "Vous êtes sur le point de supprimer l'option : {{title}}. Cette action ne peut être annulée." + }, + "organization": { + "header": "Organisation", + "edit": { + "header": "Modifier l'organisation", + "toasts": { + "success": "Organisation de {{title}} modifiée avec succès." + } + } + }, + "toasts": { + "delete": { + "success": { + "header": "Produit supprimé", + "description": "{{title}} a été supprimé avec succès." + }, + "error": { + "header": "Erreur lors de la suppression du produit" + } + } + } + }, + "collections": { + "domain": "Collections", + "subtitle": "Organisez vos produits en collections.", + "createCollection": "Créer une collection", + "createCollectionHint": "Créer une nouvelle collection pour organiser vos produits.", + "createSuccess": "Collection créée avec succès.", + "editCollection": "Modifier la collection", + "handleTooltip": "Le handle est utilisé pour faire référence à la collection dans votre boutique. Si non spécifié, le handle sera généré à partir du titre de la collection.", + "deleteWarning": "Vous êtes sur le point de supprimer la collection {{title}}. Cette action ne peut être annulée.", + "removeSingleProductWarning": "Vous êtes sur le point de supprimer le produit {{title}} de la collection. Cette action ne peut être annulée.", + "removeProductsWarning_one": "Vous êtes sur le point de supprimer {{count}} produit de la collection. Cette action ne peut être annulée.", + "removeProductsWarning_other": "Vous êtes sur le point de supprimer {{count}} produits de la collection. Cette action ne peut être annulée.", + "products": { + "list": { + "noRecordsMessage": "Il n'y a pas de produits dans la collection." + }, + "add": { + "successToast_one": "Produit ajouté à la collection avec succès.", + "successToast_other": "Produits ajoutés à la collection avec succès." + }, + "remove": { + "successToast_one": "Produit retiré de la collection avec succès.", + "successToast_other": "Produits retirés de la collection avec succès." + } + } + }, + "categories": { + "domain": "Categories", + "subtitle": "Organisez vos produits en catégories, et gérez l'ordre et la hiérarchie de ces catégories.", + "create": { + "header": "Créer une catégorie", + "hint": "Create a new category to organize your products.", + "tabs": { + "details": "Détails", + "organize": "Organiser par ordre" + }, + "successToast": "Catégorie {{name}} créée avec succès." + }, + "edit": { + "header": "Editer une catégorie", + "description": "Modifier la catégorie pour mettre à jour ses détails.", + "successToast": "Catégorie modifiée avec succès." + }, + "delete": { + "confirmation": "Vous êtes sur le point de supprimer la catégorie {{name}}. Cette action ne peut être annulée.", + "successToast": "Catégorie {{name}} supprimée avec succès." + }, + "products": { + "add": { + "disabledTooltip": "Le produit est déjà dans cette catégorie.", + "successToast_one": "Produit ajouté à la catégorie avec succès.", + "successToast_other": "Produits ajoutés à la catégorie avec succès." + }, + "remove": { + "confirmation_one": "Vous êtes sur le point de supprimer {{count}} produit de la catégorie. Cette action ne peut être annulée.", + "confirmation_other": "Vous êtes sur le point de supprimer {{count}} produits de la catégorie. Cette action ne peut être annulée.", + "successToast_one": "Produit retiré de la catégorie avec succès.", + "successToast_other": "Produits retirés de la catégorie avec succès." + }, + "list": { + "noRecordsMessage": "Il n'y a pas de produits dans la catégorie." + } + }, + "organize": { + "header": "Organiser", + "action": "Modifier l'ordre" + }, + "fields": { + "visibility": { + "label": "Visibility", + "internal": "Interne", + "public": "Publique" + }, + "status": { + "label": "Statut", + "active": "Active", + "inactive": "Inactive" + }, + "path": { + "label": "Chemin", + "tooltip": "Afficher le chemin complet de la catégorie." + }, + "children": { + "label": "Enfants" + }, + "new": { + "label": "Nouveau" + } + } + }, + "inventory": { + "domain": "Inventaire", + "subtitle": "Gérer vos articles d'inventaire", + "reserved": "Réservé", + "available": "Disponible", + "locationLevels": "Emplacements", + "associatedVariants": "Variantes associées", + "manageLocations": "Gérer les emplacements", + "deleteWarning": "Vous êtes sur le point de supprimer un article d'inventaire. Cette action ne peut être annulée.", + "editItemDetails": "Modifier les détails de l'article", + "create": { + "title": "Créer un article d'inventaire", + "details": "Details", + "availability": "Disponibilité", + "locations": "Emplacements", + "attributes": "Attributs", + "requiresShipping": "Nécessite l'expédition", + "requiresShippingHint": "Le produit d'inventaire nécessite-t-il une expédition ?", + "successToast": "Article d'inventaire créé avec succès." + }, + "reservation": { + "header": "Reservation of {{itemName}}", + "editItemDetails": "Modifier la réservation", + "lineItemId": "ID de ligne", + "orderID": "ID de la commande", + "description": "Description", + "location": "Emplacement", + "inStockAtLocation": "En stock à cet emplacement", + "availableAtLocation": "Disponible à cet emplacement", + "reservedAtLocation": "Réservé à cet emplacement", + "reservedAmount": "Montant réservé", + "create": "Créer une réservation", + "itemToReserve": "Article à réserver", + "quantityPlaceholder": "Combien voulez-vous réserver ?", + "descriptionPlaceholder": "Quel type de réservation est celle-ci ?", + "successToast": "Réservation créée avec succès.", + "updateSuccessToast": "Réservation modifiée avec succès.", + "deleteSuccessToast": "Réservation supprimée avec succès.", + "errors": { + "noAvaliableQuantity": "Emplacement de stock sans quantité disponible.", + "quantityOutOfRange": "Quantité minimale est 1 et maximum est {{max}}" + } + }, + "toast": { + "updateLocations": "Locations updated successfully.", + "updateLevel": "Inventaire mis à jour avec succès.", + "updateItem": "Article d'inventaire mis à jour avec succès." + } + }, + "giftCards": { + "domain": "Gift Cards", + "editGiftCard": "Modifier la carte cadeau", + "createGiftCard": "Créer une carte cadeau", + "createGiftCardHint": "Créer manuellement une carte cadeau qui peut être utilisée comme moyen de paiement dans votre boutique.", + "selectRegionFirst": "Sélectionnez une région d'abord", + "deleteGiftCardWarning": "Vous êtes sur le point de supprimer la carte cadeau {{code}}. Cette action ne peut être annulée.", + "balanceHigherThanValue": "Le solde ne peut être supérieur au montant initial.", + "balanceLowerThanZero": "Le solde ne peut être négatif.", + "expiryDateHint": "Les pays ont des lois différentes concernant les dates d'expiration des cartes cadeaux. Assurez-vous de vérifier les régulations locales avant de définir une date d'expiration.", + "regionHint": "Changer la région de la carte cadeau changera également sa devise, potentiellement affectant sa valeur monétaire.", + "enabledHint": "Spécifiez si la carte cadeau est activée ou désactivée.", + "balance": "Solde", + "currentBalance": "Solde actuel", + "initialBalance": "Solde initial", + "personalMessage": "Message personnel", + "recipient": "Destinataire" + }, + "customers": { + "domain": "Clients", + "list": { + "noRecordsMessage": "Vos clients apparaîtront ici." + }, + "create": { + "header": "Créer un client", + "hint": "Créer un nouveau client et gérer leurs détails.", + "successToast": "Client {{email}} créé avec succès." + }, + "groups": { + "label": "Groupes de clients", + "remove": "Êtes-vous sûr de vouloir supprimer le client de \"{{name}}\" groupe de clients ?", + "removeMany": "Êtes-vous sûr de vouloir supprimer le client de les groupes de clients suivants : {{groups}} ?", + "alreadyAddedTooltip": "Le client est déjà dans ce groupe de clients.", + "list": { + "noRecordsMessage": "Ce client n'appartient à aucun groupe." + }, + "add": { + "success": "Client ajouté au groupe : {{groups}}.", + "list": { + "noRecordsMessage": "Veuillez créer un groupe de clients d'abord." + } + }, + "removed": { + "success": "Client retiré du groupe : {{groups}}.", + "list": { + "noRecordsMessage": "Veuillez créer un groupe de clients d'abord." + } + } + }, + "edit": { + "header": "Editer un client", + "emailDisabledTooltip": "Le courriel ne peut être modifié pour les clients enregistrés.", + "successToast": "Client {{email}} modifié avec succès." + }, + "delete": { + "title": "Supprimer un client", + "description": "Vous êtes sur le point de supprimer le client {{email}}. Cette action ne peut être annulée.", + "successToast": "Client {{email}} supprimé avec succès." + }, + "fields": { + "guest": "Invité", + "registered": "Enregistré", + "groups": "Groupes" + }, + "registered": "Enregistré", + "guest": "Invité", + "hasAccount": "A un compte" + }, + "customerGroups": { + "domain": "Groupes de clients", + "subtitle": "Organisez les clients en groupes. Les groupes peuvent avoir des promotions et des prix différents.", + "create": { + "header": "Créer un groupe de clients", + "hint": "Créer un nouveau groupe de clients pour segmenter vos clients.", + "successToast": "Groupe de clients {{name}} créé avec succès." + }, + "edit": { + "header": "Editer un groupe de clients", + "successToast": "Groupe de clients {{name}} modifié avec succès." + }, + "delete": { + "title": "Delete Customer Group", + "description": "Vous êtes sur le point de supprimer le groupe de clients {{name}}. Cette action ne peut être annulée.", + "successToast": "Groupe de clients {{name}} supprimé avec succès." + }, + "customers": { + "alreadyAddedTooltip": "The customer has already been added to the group.", + "add": { + "successToast_one": "Client ajouté au groupe avec succès.", + "successToast_other": "Clients ajoutés au groupe avec succès.", + "list": { + "noRecordsMessage": "Créer un client d'abord." + } + }, + "remove": { + "title_one": "Supprimer un client", + "title_other": "Supprimer des clients", + "description_one": "Vous êtes sur le point de supprimer {{count}} client du groupe de clients. Cette action ne peut être annulée.", + "description_other": "Vous êtes sur le point de supprimer {{count}} clients du groupe de clients. Cette action ne peut être annulée." + }, + "list": { + "noRecordsMessage": "Ce groupe n'a pas de clients." + } + } + }, + "orders": { + "domain": "Commandes", + "claim": "Réclamation", + "exchange": "Échange", + "return": "Retour", + "cancelWarning": "Vous êtes sur le point de supprimer la commande {{id}}. Cette action ne peut être annulée.", + "onDateFromSalesChannel": "{{date}} de {{salesChannel}}", + "list": { + "noRecordsMessage": "Vos commandes apparaîtront ici." + }, + "summary": { + "requestReturn": "Demander un retour", + "allocateItems": "Allouer des articles", + "editOrder": "Editer la commande", + "editOrderContinue": "Continuer l'édition de la commande", + "inventoryKit": "Consiste de {{count}}x articles d'inventaire", + "itemTotal": "Total des articles", + "shippingTotal": "Total des frais de livraison", + "discountTotal": "Total des remises", + "taxTotalIncl": "Total des taxes (inclues)", + "itemSubtotal": "Sous-total des articles", + "shippingSubtotal": "Sous-total des frais de livraison", + "discountSubtotal": "Sous-total des remises", + "taxTotal": "Total des taxes" + }, + "transfer": { "title": "Transfert de propriété", "requestSuccess": "La demande de transfert de la commande à été envoyé à: {{email}}.", "currentOwner": "Propriétaire actuel", @@ -934,1838 +950,1838 @@ "currentOwnerDescription": "Le client actuellement lié à cette commande.", "newOwnerDescription": "Le client à qui transferer cette commande." }, - "payment": { - "title": "Paiements", - "isReadyToBeCaptured": "Le paiement <0/> est prêt à être capturé.", - "totalPaidByCustomer": "Total payé par le client", - "capture": "Capture du paiement", - "capture_short": "Capture", - "refund": "Remboursement", - "markAsPaid": "Marquer comme payé", - "statusLabel": "Statut du paiement", - "statusTitle": "Statut du paiement", - "status": { - "notPaid": "Non payé", - "authorized": "Autorisé", - "partiallyAuthorized": "Partiellement autorisé", - "awaiting": "En attente", - "captured": "Capturé", - "partiallyRefunded": "Partiellement remboursé", - "partiallyCaptured": "Partiellement capturé", - "refunded": "Remboursé", - "canceled": "Annulé", - "requiresAction": "Requiert une action" - }, - "capturePayment": "Le paiement de {{amount}} sera capturé.", - "capturePaymentSuccess": "Paiement de {{amount}} capturé avec succès", - "markAsPaidPayment": "Le paiement de {{amount}} sera marqué comme payé.", - "markAsPaidPaymentSuccess": "Paiement de {{amount}} marqué comme payé avec succès", - "createRefund": "Créer un remboursement", - "refundPaymentSuccess": "Remboursement de {{amount}} réussi", - "createRefundWrongQuantity": "La quantité doit être un nombre entre 1 et {{number}}", - "refundAmount": "Remboursement de {{amount}}", - "paymentLink": "Copier le lien de paiement pour {{amount}}", - "selectPaymentToRefund": "Sélectionner le paiement à rembourser" - }, - "edits": { - "title": "Editer la commande", - "confirm": "Confirmer l'édition", - "confirmText": "Vous êtes sur le point de confirmer une édition de commande. Cette action ne peut être annulée.", - "cancel": "Annuler l'édition", - "currentItems": "Articles actuels", - "currentItemsDescription": "Ajuster la quantité des articles ou les supprimer.", - "addItemsDescription": "Vous pouvez ajouter de nouveaux articles à la commande.", - "addItems": "Ajouter des articles", - "amountPaid": "Montant payé", - "newTotal": "Nouveau total", - "differenceDue": "Différence due", - "create": "Editer la commande", - "currentTotal": "Total actuel", - "noteHint": "Ajouter une note interne pour l'édition", - "cancelSuccessToast": "Édition de commande annulée", - "createSuccessToast": "Édition de commande demandée", - "activeChangeError": "Il existe déjà une édition active sur la commande (retour, réclamation, échange etc.). Veuillez terminer ou annuler l'édition avant d'éditer la commande.", - "panel": { - "title": "Édition de commande demandée", - "titlePending": "Édition de commande en attente" - }, - "toast": { - "canceledSuccessfully": "Édition de commande annulée", - "confirmedSuccessfully": "Édition de commande confirmée" - }, - "validation": { - "quantityLowerThanFulfillment": "Ne peut pas définir la quantité à être inférieure ou égale à la quantité livrée" - } - }, - "returns": { - "create": "Créer un retour", - "confirm": "Confirmer le retour", - "confirmText": "Vous êtes sur le point de confirmer un retour. Cette action ne peut être annulée.", - "inbound": "Entrant", - "outbound": "Sortant", - "sendNotification": "Envoyer une notification", - "sendNotificationHint": "Notifier le client concernant le retour.", - "returnTotal": "Total du retour", - "inboundTotal": "Total entrant", - "refundAmount": "Montant du remboursement", - "outstandingAmount": "Montant impayé", - "reason": "Raison", - "reasonHint": "Choisir la raison pour laquelle le client souhaite retourner des articles.", - "note": "Note", - "noInventoryLevel": "Pas de niveau d'inventaire", - "noInventoryLevelDesc": "La localisation sélectionnée n'a pas de niveau d'inventaire pour les articles sélectionnés. Le retour peut être demandé mais ne peut être reçu que lorsqu'un niveau d'inventaire est créé pour la localisation sélectionnée.", - "noteHint": "Vous pouvez taper librement si vous souhaitez spécifier quelque chose.", - "location": "Emplacement", - "locationHint": "Choisir l'emplacement auquel vous souhaitez retourner les articles.", - "inboundShipping": "Frais de retour", - "inboundShippingHint": "Choisir le mode de livraison que vous souhaitez utiliser.", - "returnableQuantityLabel": "Quantité retournable", - "refundableAmountLabel": "Montant remboursable", - "returnRequestedInfo": "{{requestedItemsCount}}x articles retournés demandés", - "returnReceivedInfo": "{{requestedItemsCount}}x articles retournés reçus", - "itemReceived": "Articles reçus", - "returnRequested": "Retour demandé", - "damagedItemReceived": "Articles endommagés reçus", - "damagedItemsReturned": "{{quantity}}x articles endommagés retournés", - "activeChangeError": "Il existe déjà une édition active sur la commande (retour, réclamation, échange etc.). Veuillez terminer ou annuler l'édition avant d'éditer la commande.", - "cancel": { - "title": "Annuler le retour", - "description": "Êtes-vous sûr de vouloir annuler la demande de retour ?" - }, - "placeholders": { - "noReturnShippingOptions": { - "title": "Aucune option de retour trouvée", - "hint": "Aucune option de retour n'a été créée pour la localisation. Vous pouvez créer une option à Localisation & Livraison." - }, - "outboundShippingOptions": { - "title": "Aucune option de livraison sortante trouvée", - "hint": "Aucune option de livraison sortante n'a été créée pour la localisation. Vous pouvez créer une option à Localisation & Livraison." - } - }, - "receive": { - "action": "Recevoir des articles", - "receiveItems": "{{returnType}} {{id}}", - "restockAll": "Réapprovisionner tous les articles", - "itemsLabel": "Articles reçus", - "title": "Recevoir des articles pour #{{returnId}}", - "sendNotificationHint": "Notifier le client concernant le retour.", - "inventoryWarning": "Veuillez noter que nous ajusterons automatiquement les niveaux d'inventaire en fonction de vos entrées ci-dessus.", - "writeOffInputLabel": "Combien d'articles sont endommagés ?", - "toast": { - "success": "Retour reçu avec succès.", - "errorLargeValue": "La quantité est supérieure à la quantité d'articles demandée.", - "errorNegativeValue": "La quantité ne peut être négative.", - "errorLargeDamagedValue": "La quantité d'articles endommagés + la quantité d'articles reçus non endommagés dépasse la quantité totale d'articles sur le retour. Veuillez diminuer la quantité d'articles non endommagés." - } - }, - "toast": { - "canceledSuccessfully": "Retour annulé avec succès", - "confirmedSuccessfully": "Return confirmed successfully" - }, - "panel": { - "title": "Retour demandé", - "description": "Il existe une demande de retour à compléter" - } - }, - "claims": { - "create": "Créer une réclamation", - "confirm": "Confirmer la réclamation", - "confirmText": "Vous êtes sur le point de confirmer une réclamation. Cette action ne peut être annulée.", - "manage": "Gérer la réclamation", - "outbound": "Sortant", - "outboundItemAdded": "{{itemsCount}}x ajoutés via la réclamation", - "outboundTotal": "Total sortant", - "outboundShipping": "Frais de livraison sortante", - "outboundShippingHint": "Choisir le mode de livraison que vous souhaitez utiliser.", - "refundAmount": "Différence estimée", - "activeChangeError": "Il existe déjà une édition active sur la commande (retour, réclamation, échange etc.). Veuillez terminer ou annuler l'édition avant d'éditer la commande.", - "actions": { - "cancelClaim": { - "successToast": "Réclamation annulée avec succès." - } - }, - "cancel": { - "title": "Annuler la réclamation", - "description": "Êtes-vous sûr de vouloir annuler la réclamation ?" - }, - "tooltips": { - "onlyReturnShippingOptions": "Cette liste ne contient que des options de retour." - }, - "toast": { - "canceledSuccessfully": "Réclamation annulée avec succès", - "confirmedSuccessfully": "Réclamation confirmée avec succès" - }, - "panel": { - "title": "Réclamation demandée", - "description": "Il existe une demande de réclamation à compléter" - } - }, - "exchanges": { - "create": "Créer un échange", - "manage": "Gérer l'échange", - "confirm": "Confirmer l'échange", - "confirmText": "Vous êtes sur le point de confirmer un échange. Cette action ne peut être annulée.", - "outbound": "Sortant", - "outboundItemAdded": "{{itemsCount}}x ajoutés via l'échange", - "outboundTotal": "Total sortant", - "outboundShipping": "Frais de livraison sortante", - "outboundShippingHint": "Choose which method you want to use.", - "refundAmount": "Différence estimée", - "activeChangeError": "Il existe déjà une édition active sur la commande (retour, réclamation, échange etc.). Veuillez terminer ou annuler l'édition avant d'éditer la commande.", - "actions": { - "cancelExchange": { - "successToast": "Échange annulé avec succès." - } - }, - "cancel": { - "title": "Annuler l'échange", - "description": "Êtes-vous sûr de vouloir annuler l'échange ?" - }, - "tooltips": { - "onlyReturnShippingOptions": "Cette liste ne contient que des options de retour." - }, - "toast": { - "canceledSuccessfully": "Échange annulé avec succès", - "confirmedSuccessfully": "Échange confirmé avec succès" - }, - "panel": { - "title": "Échange demandé", - "description": "Il existe une demande d'échange à compléter" - } - }, - "reservations": { - "allocatedLabel": "Alloué", - "notAllocatedLabel": "Non alloué" - }, - "allocateItems": { - "action": "Allouer des articles", - "title": "Allouer les articles de la commande", - "locationDescription": "Choisir l'emplacement à partir duquel allouer les articles.", - "itemsToAllocate": "Articles à allouer", - "itemsToAllocateDesc": "Sélectionner le nombre d'articles que vous souhaitez allouer", - "search": "Rechercher des articles", - "consistsOf": "Consiste de {{num}}x articles d'inventaire", - "requires": "Requiert {{num}} par variant", - "toast": { - "created": "Articles alloués avec succès" - }, - "error": { - "quantityNotAllocated": "Il existe des articles non alloués." - } - }, - "shipment": { - "title": "Marquer l'expédition comme expédiée", - "trackingNumber": "Numéro de suivi", - "addTracking": "Ajouter un numéro de suivi", - "sendNotification": "Envoyer une notification", - "sendNotificationHint": "Notifier le client concernant cette expédition.", - "toastCreated": "Expédition créée avec succès." - }, - "fulfillment": { - "cancelWarning": "Vous êtes sur le point d'annuler une expédition. Cette action ne peut être annulée.", - "markAsDeliveredWarning": "Vous êtes sur le point de marquer l'expédition comme livrée. Cette action ne peut être annulée.", - "unfulfilledItems": "Articles non expédiés", - "statusLabel": "Statut de l'expédition", - "statusTitle": "Statut de l'expédition", - "fulfillItems": "Expédier des articles", - "awaitingFulfillmentBadge": "En attente d'expédition", - "requiresShipping": "Requiert une expédition", - "number": "Expédition #{{number}}", - "itemsToFulfill": "Articles à expédier", - "create": "Créer une expédition", - "available": "Disponible", - "inStock": "En stock", - "markAsShipped": "Marquer comme expédiée", - "markAsDelivered": "Marquer comme livrée", - "itemsToFulfillDesc": "Choisir les articles et les quantités à expédier", - "locationDescription": "Choisir l'emplacement à partir duquel expédier les articles.", - "sendNotificationHint": "Notifier le client concernant l'expédition créée.", - "methodDescription": "Choisir un mode de livraison différent de celui sélectionné par le client", - "error": { - "wrongQuantity": "Seul un article est disponible pour l'expédition", - "wrongQuantity_other": "La quantité doit être un nombre entre 1 et {{number}}", - "noItems": "Aucun article à expédier." - }, - "status": { - "notFulfilled": "Non expédié", - "partiallyFulfilled": "Partiellement expédié", - "fulfilled": "Expédié", - "partiallyShipped": "Partiellement expédié", - "shipped": "Expédié", - "delivered": "Livré", - "partiallyDelivered": "Partiellement livré", - "partiallyReturned": "Partiellement retourné", - "returned": "Retourné", - "canceled": "Annulé", - "requiresAction": "Requiert une action" - }, - "toast": { - "created": "Expédition créée avec succès", - "canceled": "Expédition annulée avec succès", - "fulfillmentShipped": "Ne peut annuler une expédition déjà expédiée", - "fulfillmentDelivered": "Expédition marquée comme livrée avec succès" - }, - "trackingLabel": "Suivi", - "shippingFromLabel": "Expédié depuis", - "itemsLabel": "Articles" - }, - "refund": { - "title": "Créer un remboursement", - "sendNotificationHint": "Notifier le client concernant le remboursement créé.", - "systemPayment": "Paiement système", - "systemPaymentDesc": "Un ou plusieurs de vos paiements est un paiement système. Soyez conscient que les captures et les remboursements ne sont pas gérés par Medusa pour de tels paiements.", - "error": { - "amountToLarge": "Ne peut rembourser plus que le montant de la commande originale.", - "amountNegative": "Le montant du remboursement doit être un nombre positif.", - "reasonRequired": "Veuillez sélectionner une raison de remboursement." - } - }, - "customer": { - "contactLabel": "Contact", - "editEmail": "Modifier l'email", - "transferOwnership": "Transférer la propriété", - "editBillingAddress": "Modifier l'adresse de facturation", - "editShippingAddress": "Modifier l'adresse de livraison" - }, - "activity": { - "header": "Activité", - "showMoreActivities_one": "Afficher {{count}} activités supplémentaires", - "showMoreActivities_other": "Afficher {{count}} activités supplémentaires", - "comment": { - "label": "Commentaire", - "placeholder": "Laisser un commentaire", - "addButtonText": "Ajouter un commentaire", - "deleteButtonText": "Supprimer le commentaire" - }, - "from": "Depuis", + "payment": { + "title": "Paiements", + "isReadyToBeCaptured": "Le paiement <0/> est prêt à être capturé.", + "totalPaidByCustomer": "Total payé par le client", + "capture": "Capture du paiement", + "capture_short": "Capture", + "refund": "Remboursement", + "markAsPaid": "Marquer comme payé", + "statusLabel": "Statut du paiement", + "statusTitle": "Statut du paiement", + "status": { + "notPaid": "Non payé", + "authorized": "Autorisé", + "partiallyAuthorized": "Partiellement autorisé", + "awaiting": "En attente", + "captured": "Capturé", + "partiallyRefunded": "Partiellement remboursé", + "partiallyCaptured": "Partiellement capturé", + "refunded": "Remboursé", + "canceled": "Annulé", + "requiresAction": "Requiert une action" + }, + "capturePayment": "Le paiement de {{amount}} sera capturé.", + "capturePaymentSuccess": "Paiement de {{amount}} capturé avec succès", + "markAsPaidPayment": "Le paiement de {{amount}} sera marqué comme payé.", + "markAsPaidPaymentSuccess": "Paiement de {{amount}} marqué comme payé avec succès", + "createRefund": "Créer un remboursement", + "refundPaymentSuccess": "Remboursement de {{amount}} réussi", + "createRefundWrongQuantity": "La quantité doit être un nombre entre 1 et {{number}}", + "refundAmount": "Remboursement de {{amount}}", + "paymentLink": "Copier le lien de paiement pour {{amount}}", + "selectPaymentToRefund": "Sélectionner le paiement à rembourser" + }, + "edits": { + "title": "Editer la commande", + "confirm": "Confirmer l'édition", + "confirmText": "Vous êtes sur le point de confirmer une édition de commande. Cette action ne peut être annulée.", + "cancel": "Annuler l'édition", + "currentItems": "Articles actuels", + "currentItemsDescription": "Ajuster la quantité des articles ou les supprimer.", + "addItemsDescription": "Vous pouvez ajouter de nouveaux articles à la commande.", + "addItems": "Ajouter des articles", + "amountPaid": "Montant payé", + "newTotal": "Nouveau total", + "differenceDue": "Différence due", + "create": "Editer la commande", + "currentTotal": "Total actuel", + "noteHint": "Ajouter une note interne pour l'édition", + "cancelSuccessToast": "Édition de commande annulée", + "createSuccessToast": "Édition de commande demandée", + "activeChangeError": "Il existe déjà une édition active sur la commande (retour, réclamation, échange etc.). Veuillez terminer ou annuler l'édition avant d'éditer la commande.", + "panel": { + "title": "Édition de commande demandée", + "titlePending": "Édition de commande en attente" + }, + "toast": { + "canceledSuccessfully": "Édition de commande annulée", + "confirmedSuccessfully": "Édition de commande confirmée" + }, + "validation": { + "quantityLowerThanFulfillment": "Ne peut pas définir la quantité à être inférieure ou égale à la quantité livrée" + } + }, + "returns": { + "create": "Créer un retour", + "confirm": "Confirmer le retour", + "confirmText": "Vous êtes sur le point de confirmer un retour. Cette action ne peut être annulée.", + "inbound": "Entrant", + "outbound": "Sortant", + "sendNotification": "Envoyer une notification", + "sendNotificationHint": "Notifier le client concernant le retour.", + "returnTotal": "Total du retour", + "inboundTotal": "Total entrant", + "refundAmount": "Montant du remboursement", + "outstandingAmount": "Montant impayé", + "reason": "Raison", + "reasonHint": "Choisir la raison pour laquelle le client souhaite retourner des articles.", + "note": "Note", + "noInventoryLevel": "Pas de niveau d'inventaire", + "noInventoryLevelDesc": "La localisation sélectionnée n'a pas de niveau d'inventaire pour les articles sélectionnés. Le retour peut être demandé mais ne peut être reçu que lorsqu'un niveau d'inventaire est créé pour la localisation sélectionnée.", + "noteHint": "Vous pouvez taper librement si vous souhaitez spécifier quelque chose.", + "location": "Emplacement", + "locationHint": "Choisir l'emplacement auquel vous souhaitez retourner les articles.", + "inboundShipping": "Frais de retour", + "inboundShippingHint": "Choisir le mode de livraison que vous souhaitez utiliser.", + "returnableQuantityLabel": "Quantité retournable", + "refundableAmountLabel": "Montant remboursable", + "returnRequestedInfo": "{{requestedItemsCount}}x articles retournés demandés", + "returnReceivedInfo": "{{requestedItemsCount}}x articles retournés reçus", + "itemReceived": "Articles reçus", + "returnRequested": "Retour demandé", + "damagedItemReceived": "Articles endommagés reçus", + "damagedItemsReturned": "{{quantity}}x articles endommagés retournés", + "activeChangeError": "Il existe déjà une édition active sur la commande (retour, réclamation, échange etc.). Veuillez terminer ou annuler l'édition avant d'éditer la commande.", + "cancel": { + "title": "Annuler le retour", + "description": "Êtes-vous sûr de vouloir annuler la demande de retour ?" + }, + "placeholders": { + "noReturnShippingOptions": { + "title": "Aucune option de retour trouvée", + "hint": "Aucune option de retour n'a été créée pour la localisation. Vous pouvez créer une option à Localisation & Livraison." + }, + "outboundShippingOptions": { + "title": "Aucune option de livraison sortante trouvée", + "hint": "Aucune option de livraison sortante n'a été créée pour la localisation. Vous pouvez créer une option à Localisation & Livraison." + } + }, + "receive": { + "action": "Recevoir des articles", + "receiveItems": "{{returnType}} {{id}}", + "restockAll": "Réapprovisionner tous les articles", + "itemsLabel": "Articles reçus", + "title": "Recevoir des articles pour #{{returnId}}", + "sendNotificationHint": "Notifier le client concernant le retour.", + "inventoryWarning": "Veuillez noter que nous ajusterons automatiquement les niveaux d'inventaire en fonction de vos entrées ci-dessus.", + "writeOffInputLabel": "Combien d'articles sont endommagés ?", + "toast": { + "success": "Retour reçu avec succès.", + "errorLargeValue": "La quantité est supérieure à la quantité d'articles demandée.", + "errorNegativeValue": "La quantité ne peut être négative.", + "errorLargeDamagedValue": "La quantité d'articles endommagés + la quantité d'articles reçus non endommagés dépasse la quantité totale d'articles sur le retour. Veuillez diminuer la quantité d'articles non endommagés." + } + }, + "toast": { + "canceledSuccessfully": "Retour annulé avec succès", + "confirmedSuccessfully": "Return confirmed successfully" + }, + "panel": { + "title": "Retour demandé", + "description": "Il existe une demande de retour à compléter" + } + }, + "claims": { + "create": "Créer une réclamation", + "confirm": "Confirmer la réclamation", + "confirmText": "Vous êtes sur le point de confirmer une réclamation. Cette action ne peut être annulée.", + "manage": "Gérer la réclamation", + "outbound": "Sortant", + "outboundItemAdded": "{{itemsCount}}x ajoutés via la réclamation", + "outboundTotal": "Total sortant", + "outboundShipping": "Frais de livraison sortante", + "outboundShippingHint": "Choisir le mode de livraison que vous souhaitez utiliser.", + "refundAmount": "Différence estimée", + "activeChangeError": "Il existe déjà une édition active sur la commande (retour, réclamation, échange etc.). Veuillez terminer ou annuler l'édition avant d'éditer la commande.", + "actions": { + "cancelClaim": { + "successToast": "Réclamation annulée avec succès." + } + }, + "cancel": { + "title": "Annuler la réclamation", + "description": "Êtes-vous sûr de vouloir annuler la réclamation ?" + }, + "tooltips": { + "onlyReturnShippingOptions": "Cette liste ne contient que des options de retour." + }, + "toast": { + "canceledSuccessfully": "Réclamation annulée avec succès", + "confirmedSuccessfully": "Réclamation confirmée avec succès" + }, + "panel": { + "title": "Réclamation demandée", + "description": "Il existe une demande de réclamation à compléter" + } + }, + "exchanges": { + "create": "Créer un échange", + "manage": "Gérer l'échange", + "confirm": "Confirmer l'échange", + "confirmText": "Vous êtes sur le point de confirmer un échange. Cette action ne peut être annulée.", + "outbound": "Sortant", + "outboundItemAdded": "{{itemsCount}}x ajoutés via l'échange", + "outboundTotal": "Total sortant", + "outboundShipping": "Frais de livraison sortante", + "outboundShippingHint": "Choose which method you want to use.", + "refundAmount": "Différence estimée", + "activeChangeError": "Il existe déjà une édition active sur la commande (retour, réclamation, échange etc.). Veuillez terminer ou annuler l'édition avant d'éditer la commande.", + "actions": { + "cancelExchange": { + "successToast": "Échange annulé avec succès." + } + }, + "cancel": { + "title": "Annuler l'échange", + "description": "Êtes-vous sûr de vouloir annuler l'échange ?" + }, + "tooltips": { + "onlyReturnShippingOptions": "Cette liste ne contient que des options de retour." + }, + "toast": { + "canceledSuccessfully": "Échange annulé avec succès", + "confirmedSuccessfully": "Échange confirmé avec succès" + }, + "panel": { + "title": "Échange demandé", + "description": "Il existe une demande d'échange à compléter" + } + }, + "reservations": { + "allocatedLabel": "Alloué", + "notAllocatedLabel": "Non alloué" + }, + "allocateItems": { + "action": "Allouer des articles", + "title": "Allouer les articles de la commande", + "locationDescription": "Choisir l'emplacement à partir duquel allouer les articles.", + "itemsToAllocate": "Articles à allouer", + "itemsToAllocateDesc": "Sélectionner le nombre d'articles que vous souhaitez allouer", + "search": "Rechercher des articles", + "consistsOf": "Consiste de {{num}}x articles d'inventaire", + "requires": "Requiert {{num}} par variant", + "toast": { + "created": "Articles alloués avec succès" + }, + "error": { + "quantityNotAllocated": "Il existe des articles non alloués." + } + }, + "shipment": { + "title": "Marquer l'expédition comme expédiée", + "trackingNumber": "Numéro de suivi", + "addTracking": "Ajouter un numéro de suivi", + "sendNotification": "Envoyer une notification", + "sendNotificationHint": "Notifier le client concernant cette expédition.", + "toastCreated": "Expédition créée avec succès." + }, + "fulfillment": { + "cancelWarning": "Vous êtes sur le point d'annuler une expédition. Cette action ne peut être annulée.", + "markAsDeliveredWarning": "Vous êtes sur le point de marquer l'expédition comme livrée. Cette action ne peut être annulée.", + "unfulfilledItems": "Articles non expédiés", + "statusLabel": "Statut de l'expédition", + "statusTitle": "Statut de l'expédition", + "fulfillItems": "Expédier des articles", + "awaitingFulfillmentBadge": "En attente d'expédition", + "requiresShipping": "Requiert une expédition", + "number": "Expédition #{{number}}", + "itemsToFulfill": "Articles à expédier", + "create": "Créer une expédition", + "available": "Disponible", + "inStock": "En stock", + "markAsShipped": "Marquer comme expédiée", + "markAsDelivered": "Marquer comme livrée", + "itemsToFulfillDesc": "Choisir les articles et les quantités à expédier", + "locationDescription": "Choisir l'emplacement à partir duquel expédier les articles.", + "sendNotificationHint": "Notifier le client concernant l'expédition créée.", + "methodDescription": "Choisir un mode de livraison différent de celui sélectionné par le client", + "error": { + "wrongQuantity": "Seul un article est disponible pour l'expédition", + "wrongQuantity_other": "La quantité doit être un nombre entre 1 et {{number}}", + "noItems": "Aucun article à expédier." + }, + "status": { + "notFulfilled": "Non expédié", + "partiallyFulfilled": "Partiellement expédié", + "fulfilled": "Expédié", + "partiallyShipped": "Partiellement expédié", + "shipped": "Expédié", + "delivered": "Livré", + "partiallyDelivered": "Partiellement livré", + "partiallyReturned": "Partiellement retourné", + "returned": "Retourné", + "canceled": "Annulé", + "requiresAction": "Requiert une action" + }, + "toast": { + "created": "Expédition créée avec succès", + "canceled": "Expédition annulée avec succès", + "fulfillmentShipped": "Ne peut annuler une expédition déjà expédiée", + "fulfillmentDelivered": "Expédition marquée comme livrée avec succès" + }, + "trackingLabel": "Suivi", + "shippingFromLabel": "Expédié depuis", + "itemsLabel": "Articles" + }, + "refund": { + "title": "Créer un remboursement", + "sendNotificationHint": "Notifier le client concernant le remboursement créé.", + "systemPayment": "Paiement système", + "systemPaymentDesc": "Un ou plusieurs de vos paiements est un paiement système. Soyez conscient que les captures et les remboursements ne sont pas gérés par Medusa pour de tels paiements.", + "error": { + "amountToLarge": "Ne peut rembourser plus que le montant de la commande originale.", + "amountNegative": "Le montant du remboursement doit être un nombre positif.", + "reasonRequired": "Veuillez sélectionner une raison de remboursement." + } + }, + "customer": { + "contactLabel": "Contact", + "editEmail": "Modifier l'email", + "transferOwnership": "Transférer la propriété", + "editBillingAddress": "Modifier l'adresse de facturation", + "editShippingAddress": "Modifier l'adresse de livraison" + }, + "activity": { + "header": "Activité", + "showMoreActivities_one": "Afficher {{count}} activités supplémentaires", + "showMoreActivities_other": "Afficher {{count}} activités supplémentaires", + "comment": { + "label": "Commentaire", + "placeholder": "Laisser un commentaire", + "addButtonText": "Ajouter un commentaire", + "deleteButtonText": "Supprimer le commentaire" + }, + "from": "Depuis", "to": "À", - "events": { - "common": { - "toReturn": "À retourner", - "toSend": "À expédier" - }, - "placed": { - "title": "Commande passée", - "fromSalesChannel": "depuis {{salesChannel}}" - }, - "canceled": { - "title": "Commande annulée" - }, - "payment": { - "awaiting": "En attente de paiement", - "captured": "Paiement capturé", - "canceled": "Paiement annulé", - "refunded": "Paiement remboursé" - }, - "fulfillment": { - "created": "Articles expédiés", - "canceled": "Expédition annulée", - "shipped": "Articles expédiés", - "delivered": "Articles livrés", - "items_one": "{{count}} article", - "items_other": "{{count}} articles" - }, - "return": { - "created": "Retour #{{returnId}} demandé", - "canceled": "Retour #{{returnId}} annulé", - "received": "Retour #{{returnId}} reçu", - "items_one": "{{count}} article retourné", - "items_other": "{{count}} articles retournés" - }, - "note": { - "comment": "Commentaire", - "byLine": "par {{author}}" - }, - "claim": { - "created": "Réclamation #{{claimId}} demandée", - "canceled": "Réclamation #{{claimId}} annulée", - "itemsInbound": "{{count}} article à retourner", - "itemsOutbound": "{{count}} article à expédier" - }, - "exchange": { - "created": "Échange #{{exchangeId}} demandé", - "canceled": "Échange #{{exchangeId}} annulé", - "itemsInbound": "{{count}} article à retourner", - "itemsOutbound": "{{count}} article à expédier" - }, - "edit": { - "requested": "Commande modifiée #{{editId}} demandée", - "confirmed": "Commande modifiée #{{editId}} confirmée" - }, - "transfer": { + "events": { + "common": { + "toReturn": "À retourner", + "toSend": "À expédier" + }, + "placed": { + "title": "Commande passée", + "fromSalesChannel": "depuis {{salesChannel}}" + }, + "canceled": { + "title": "Commande annulée" + }, + "payment": { + "awaiting": "En attente de paiement", + "captured": "Paiement capturé", + "canceled": "Paiement annulé", + "refunded": "Paiement remboursé" + }, + "fulfillment": { + "created": "Articles expédiés", + "canceled": "Expédition annulée", + "shipped": "Articles expédiés", + "delivered": "Articles livrés", + "items_one": "{{count}} article", + "items_other": "{{count}} articles" + }, + "return": { + "created": "Retour #{{returnId}} demandé", + "canceled": "Retour #{{returnId}} annulé", + "received": "Retour #{{returnId}} reçu", + "items_one": "{{count}} article retourné", + "items_other": "{{count}} articles retournés" + }, + "note": { + "comment": "Commentaire", + "byLine": "par {{author}}" + }, + "claim": { + "created": "Réclamation #{{claimId}} demandée", + "canceled": "Réclamation #{{claimId}} annulée", + "itemsInbound": "{{count}} article à retourner", + "itemsOutbound": "{{count}} article à expédier" + }, + "exchange": { + "created": "Échange #{{exchangeId}} demandé", + "canceled": "Échange #{{exchangeId}} annulé", + "itemsInbound": "{{count}} article à retourner", + "itemsOutbound": "{{count}} article à expédier" + }, + "edit": { + "requested": "Commande modifiée #{{editId}} demandée", + "confirmed": "Commande modifiée #{{editId}} confirmée" + }, + "transfer": { "requested": "Transfert de la commande #{{transferId}} demandé", "confirmed": "Transfert de la commande #{{transferId}} confirmé", "declined": "Transfert de la commande #{{transferId}} refusé" } - } - }, - "fields": { - "displayId": "Display ID", - "refundableAmount": "Montant remboursable", - "returnableQuantity": "Quantité retournable" - } - }, - "draftOrders": { - "domain": "Commandes à rédiger", - "deleteWarning": "Vous êtes sur le point de supprimer la commande à rédiger {{id}}. Cette action ne peut être annulée.", - "paymentLinkLabel": "Lien de paiement", - "cartIdLabel": "ID de panier", - "markAsPaid": { - "label": "Marquer comme payée", - "warningTitle": "Marquer comme payée", - "warningDescription": "Vous êtes sur le point de marquer la commande à rédiger comme payée. Cette action ne peut être annulée, et la collecte de paiement ne sera pas possible plus tard." - }, - "status": { - "open": "Ouvrir", - "completed": "Terminée" - }, - "create": { - "createDraftOrder": "Créer un brouillon de commande", - "createDraftOrderHint": "Créer un nouveau brouillon de commande pour gérer les détails d'une commande avant qu'elle ne soit passée.", - "chooseRegionHint": "Choisir une région", - "existingItemsLabel": "Articles existants", - "existingItemsHint": "Ajouter des articles existants au brouillon de commande.", - "customItemsLabel": "Articles personnalisés", - "customItemsHint": "Ajouter des articles personnalisés au brouillon de commande.", - "addExistingItemsAction": "Ajouter des articles existants", - "addCustomItemAction": "Ajouter un article personnalisé", - "noCustomItemsAddedLabel": "Aucun article personnalisé ajouté pour le moment", - "noExistingItemsAddedLabel": "Aucun article existant ajouté pour le moment", - "chooseRegionTooltip": "Choisir une région d'abord", - "useExistingCustomerLabel": "Utiliser un client existant", - "addShippingMethodsAction": "Ajouter des méthodes de livraison", - "unitPriceOverrideLabel": "Prix unitaire remplacé", - "shippingOptionLabel": "Option de livraison", - "shippingOptionHint": "Choisir l'option de livraison pour le brouillon de commande.", - "shippingPriceOverrideLabel": "Prix de livraison remplacé", - "shippingPriceOverrideHint": "Remplacer le prix de livraison pour le brouillon de commande.", - "sendNotificationLabel": "Envoyer une notification", - "sendNotificationHint": "Envoyer une notification au client lors de la création du brouillon de commande." - }, - "validation": { - "requiredEmailOrCustomer": "Email ou client requis.", - "requiredItems": "Au moins un article est requis.", - "invalidEmail": "L'email doit être une adresse email valide." - } - }, - "stockLocations": { - "domain": "Emplacements & Livraison", - "list": { - "description": "Gérer les emplacements de stock et les options de livraison de votre boutique." - }, - "create": { - "header": "Créer un emplacement de stock", - "hint": "Un emplacement de stock est un site physique où les produits sont stockés et expédiés.", - "successToast": "Emplacement {{name}} créé avec succès." - }, - "edit": { - "header": "Modifier l'emplacement de stock", - "viewInventory": "Voir le stock", - "successToast": "Emplacement {{name}} modifié avec succès." - }, - "delete": { - "confirmation": "Vous êtes sur le point de supprimer l'emplacement de stock {{name}}. Cette action ne peut être annulée." - }, - "fulfillmentProviders": { - "header": "Fournisseurs de livraison", - "shippingOptionsTooltip": "Ce menu déroulant ne contient que les fournisseurs activés pour cet emplacement. Ajoutez-les à l'emplacement si le menu déroulant est désactivé.", - "label": "Fournisseurs de livraison connectés", - "connectedTo": "Connecté à {{count}} de {{total}} fournisseurs de livraison", - "noProviders": "Cet emplacement de stock n'est connecté à aucun fournisseur de livraison.", - "action": "Connecter les fournisseurs", - "successToast": "Fournisseurs de livraison pour l'emplacement de stock mis à jour avec succès." - }, - "fulfillmentSets": { - "pickup": { - "header": "Retrait" - }, - "shipping": { - "header": "Livraison" - }, - "disable": { - "confirmation": "Êtes-vous sûr de vouloir désactiver {{name}} ? Cette action supprimera tous les zones de service et les options de livraison associées et ne peut être annulée.", - "pickup": "Retrait désactivé avec succès.", - "shipping": "Livraison désactivée avec succès." - }, - "enable": { - "pickup": "Retrait activé avec succès.", - "shipping": "Livraison activée avec succès." - } - }, - "sidebar": { - "header": "Configuration de la livraison", - "shippingProfiles": { - "label": "Profils de livraison", - "description": "Grouper les produits par besoins de livraison" - } - }, - "salesChannels": { - "header": "Canaux de vente", - "label": "Canaux de vente connectés", - "connectedTo": "Connecté à {{count}} de {{total}} canaux de vente", - "noChannels": "L'emplacement n'est connecté à aucun canal de vente.", - "action": "Connecter les canaux de vente", - "successToast": "Canaux de vente mis à jour avec succès." - }, - "shippingOptions": { - "create": { - "shipping": { - "header": "Créer une option de livraison pour {{zone}}", - "hint": "Créer une nouvelle option de livraison pour définir comment les produits sont expédiés depuis cet emplacement.", - "label": "Options de livraison", - "successToast": "Option de livraison {{name}} créée avec succès." - }, - "returns": { - "header": "Créer une option de retour pour {{zone}}", - "hint": "Créer une nouvelle option de retour pour définir comment les produits sont retournés à cet emplacement.", - "label": "Options de retour", - "successToast": "Option de retour {{name}} créée avec succès." - }, - "tabs": { - "details": "Détails", - "prices": "Prix" - }, - "action": "Créer une option" - }, - "delete": { - "confirmation": "Vous êtes sur le point de supprimer l'option de livraison {{name}}. Cette action ne peut être annulée.", - "successToast": "Option de livraison {{name}} supprimée avec succès." - }, - "edit": { - "header": "Modifier l'option de livraison", - "action": "Modifier l'option", - "successToast": "Option de livraison {{name}} modifiée avec succès." - }, - "pricing": { - "action": "Modifier les prix" - }, - "fields": { - "count": { - "shipping_one": "{{count}} shipping option", - "shipping_other": "{{count}} options de livraison", - "returns_one": "{{count}} option de retour", - "returns_other": "{{count}} options de retour" - }, - "priceType": { - "label": "Type de prix", - "options": { - "fixed": { - "label": "Fixe", - "hint": "Le prix de l'option de livraison est fixe et ne change pas en fonction du contenu de la commande." - }, - "calculated": { - "label": "Calculé", - "hint": "Le prix de l'option de livraison est calculé par le fournisseur de livraison lors de la passation de commande." - } - } - }, - "enableInStore": { - "label": "Activer en magasin", - "hint": "Indique si les clients peuvent utiliser cette option lors de la passation de commande." - }, - "provider": "Fournisseur de livraison", - "profile": "Profil de livraison" - } - }, - "serviceZones": { - "create": { - "headerPickup": "Créer une zone de service pour le retrait depuis {{location}}", - "headerShipping": "Créer une zone de service pour la livraison depuis {{location}}", - "action": "Créer une zone de service", - "successToast": "Zone de service {{name}} créée avec succès." - }, - "edit": { - "header": "Modifier la zone de service", - "successToast": "La zone de service {{name}} a été modifiée avec succès." - }, - "delete": { - "confirmation": "Vous êtes sur le point de supprimer la zone de service {{name}}. Cette action ne peut être annulée.", - "successToast": "La zone de service {{name}} a été supprimée avec succès." - }, - "manageAreas": { - "header": "Gérer les zones pour {{name}}", - "action": "Gérer les zones", - "label": "Zones", - "hint": "Sélectionner les zones géographiques que la zone de service couvre.", - "successToast": "Les zones pour {{name}} ont été mises à jour avec succès." - }, - "fields": { - "noRecords": "Il n'y a pas de zones de service pour ajouter des options de livraison.", - "tip": "Une zone de service est une collection de zones géographiques ou d'aires. Elle est utilisée pour restreindre les options de livraison disponibles à un ensemble de lieux défini." - } - } - }, - "shippingProfile": { - "domain": "Profils de livraison", - "subtitle": "Regrouper les produits avec des besoins de livraison similaires dans des profils.", - "create": { - "header": "Créer un profil de livraison", - "hint": "Créer un nouveau profil de livraison pour regrouper les produits avec des besoins de livraison similaires.", - "successToast": "Le profil de livraison {{name}} a été créé avec succès." - }, - "delete": { - "title": "Supprimer le profil de livraison", - "description": "Vous êtes sur le point de supprimer le profil de livraison {{name}}. Cette action ne peut être annulée.", - "successToast": "Le profil de livraison {{name}} a été supprimé avec succès." - }, - "tooltip": { - "type": "Entrer le type de profil de livraison, par exemple : Poids lourd, Surdimensionné, Livraison exclusive, etc." - } - }, - "taxRegions": { - "domain": "Taxes par régions", - "list": { - "hint": "Gérer les taxes que vous facturez à vos clients lorsqu'ils achètent depuis différents pays et régions." - }, - "delete": { - "confirmation": "Vous êtes sur le point de supprimer une taxe. Cette action ne peut être annulée.", - "successToast": "La taxe a été supprimée avec succès." - }, - "create": { - "header": "Créer une taxe", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un pays spécifique.", - "errors": { - "rateIsRequired": "Le taux de taxe est requis lors de la création d'une taxe par défaut.", - "nameIsRequired": "Le nom est requis lors de la création d'une taxe par défaut." - }, - "successToast": "La taxe a été créée avec succès." - }, - "province": { - "header": "Région", - "create": { - "header": "Créer une taxe pour la région", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour une province spécifique." - } - }, - "state": { - "header": "États", - "create": { - "header": "Créer une taxe pour les états", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un état spécifique." - } - }, - "stateOrTerritory": { - "header": "États ou territoires", - "create": { - "header": "Créer une taxe pour les états ou territoires", - "hint": "Create a new tax region to define tax rates for a specific state/territory." - } - }, - "county": { - "header": "Comtés", - "create": { - "header": "Créer une taxe pour les comtés", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un comté spécifique." - } - }, - "region": { - "header": "Régions", - "create": { - "header": "Créer une taxe pour les régions", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour une région spécifique." - } - }, - "department": { - "header": "Départements", - "create": { - "header": "Créer une taxe pour les départements", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un département spécifique." - } - }, - "territory": { - "header": "Territories", - "create": { - "header": "Créer une taxe pour les territoires", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un territoire spécifique." - } - }, - "prefecture": { - "header": "Préfectures", - "create": { - "header": "Créer une taxe pour les préfectures", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour une préfecture spécifique." - } - }, - "district": { - "header": "Quartiers", - "create": { - "header": "Créer une taxe pour les quartiers", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un district spécifique." - } - }, - "governorate": { - "header": "Gouvernorats", - "create": { - "header": "Créer une taxe pour les gouvernorats", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un gouvernorat spécifique." - } - }, - "canton": { - "header": "Cantons", - "create": { - "header": "Créer une taxe pour les cantons", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un canton spécifique." - } - }, - "emirate": { - "header": "Emirates", - "create": { - "header": "Créer une taxe pour les émirats", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un émirat spécifique." - } - }, - "sublevel": { - "header": "Niveaux subordonnés", - "create": { - "header": "Créer une taxe pour les niveaux subordonnés", - "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un niveau subordonné spécifique." - } - }, - "taxOverrides": { - "header": "Overrides", - "create": { - "header": "Créer une surcharge", - "hint": "Créer une taxe qui surcharge les taux de taxe par défaut pour les conditions sélectionnées." - }, - "edit": { - "header": "Modifier la surcharge", - "hint": "Modifier la taxe qui surcharge les taux de taxe par défaut pour les conditions sélectionnées." - } - }, - "taxRates": { - "create": { - "header": "Créer un taux d'imposition", - "hint": "Créer une nouvelle taxe pour définir le taux de taxe pour une région.", - "successToast": "Taxe créée avec succès." - }, - "edit": { - "header": "Modifier la taxe", - "hint": "Modifier la taxe pour définir le taux de taxe pour une région.", - "successToast": "Taxe modifiée avec succès." - }, - "delete": { - "confirmation": "Vous êtes sur le point de supprimer la taxe {{name}}. Cette action ne peut être annulée.", - "successToast": "La taxe a été supprimée avec succès." - } - }, - "fields": { - "isCombinable": { - "label": "Combinable", - "hint": "Indique si cette taxe peut être combinée avec le taux de taxe par défaut de la région.", - "true": "Combinable", - "false": "Non combinable" - }, - "defaultTaxRate": { - "label": "Taux de taxe par défaut", - "tooltip": "Le taux de taxe par défaut pour cette région. Un exemple est le taux de TVA standard pour un pays ou une région.", - "action": "Créer un taux de taxe par défaut" - }, - "taxRate": "Taux de taxe", - "taxCode": "Code de taxe", - "targets": { - "label": "Cibles", - "hint": "Sélectionner les cibles à laquelle cette taxe s'appliquera.", - "options": { - "product": "Produits", - "productCollection": "Collections de produits", - "productTag": "Tags de produits", - "productType": "Types de produits", - "customerGroup": "Groupes de clients" - }, - "operators": { - "in": "dans", - "on": "sur", - "and": "et" - }, - "placeholders": { - "product": "Rechercher des produits", - "productCollection": "Rechercher des collections de produits", - "productTag": "Rechercher des tags de produits", - "productType": "Rechercher des types de produits", - "customerGroup": "Rechercher des groupes de clients" - }, - "tags": { - "product": "Produit", - "productCollection": "Collection de produits", - "productTag": "Tag de produit", - "productType": "Type de produit", - "customerGroup": "Groupe de clients" - }, - "modal": { - "header": "Ajouter des cibles" - }, - "values_one": "{{count}} valeur", - "values_other": "{{count}} valeurs", - "numberOfTargets_one": "{{count}} cible", - "numberOfTargets_other": "{{count}} cibles", - "additionalValues_one": "et {{count}} plus de valeur", - "additionalValues_other": "et {{count}} plus de valeurs", - "action": "Ajouter une cible" - }, - "sublevels": { - "labels": { - "province": "Province", - "state": "État", - "region": "Région", - "stateOrTerritory": "État/Territoire", - "department": "Département", - "county": "Comté", - "territory": "Territoire", - "prefecture": "Préfecture", - "district": "Quartier", - "governorate": "Gouvernorat", - "emirate": "Émirat", - "canton": "Canton", - "sublevel": "Code de niveau subordonné" - }, - "placeholders": { - "province": "Select province", - "state": "Sélectionner l'état", - "region": "Sélectionner la région", - "stateOrTerritory": "Sélectionner l'état/territoire", - "department": "Sélectionner le département", - "county": "Sélectionner le comté", - "territory": "Sélectionner le territoire", - "prefecture": "Sélectionner la préfecture", - "district": "Sélectionner le quartier", - "governorate": "Sélectionner le gouvernorat", - "emirate": "Sélectionner l'émirat", - "canton": "Sélectionner le canton" - }, - "tooltips": { - "sublevel": "Entrer le code ISO 3166-2 pour la région de taxe subordonnée.", - "notPartOfCountry": "{{province}} ne semble pas être une partie de {{country}}. Veuillez vérifier si c'est correct." - }, - "alert": { - "header": "Les régions subordonnées sont désactivées pour cette taxe", - "description": "Les régions subordonnées sont désactivées pour cette région par défaut. Vous pouvez les activer pour créer des régions subordonnées comme des provinces, des états ou des territoires.", - "action": "Activer les régions subordonnées" - } - }, - "noDefaultRate": { - "label": "Pas de taux de taxe par défaut", - "tooltip": "Cette région de taxe n'a pas de taux de taxe par défaut. Si un taux standard est appliqué, comme la TVA d'un pays, veuillez l'ajouter à cette région." - } - } - }, - "promotions": { - "domain": "Promotions", - "sections": { - "details": "Détails de la promotion" - }, - "tabs": { - "template": "Type", - "details": "Détails", - "campaign": "Campagne" - }, - "fields": { - "type": "Type", - "value_type": "Type de valeur", - "value": "Valeur", - "campaign": "Campagne", - "method": "Méthode", - "allocation": "Allocation", - "addCondition": "Ajouter une condition", - "clearAll": "Effacer tout", - "amount": { - "tooltip": "Sélectionner le code de devise pour activer la définition de la valeur" - }, - "conditions": { - "rules": { - "title": "Qui peut utiliser ce code?", - "description": "Quel client est autorisé à utiliser le code de promotion? Le code de promotion peut être utilisé par tous les clients si laissé tel quel." - }, - "target-rules": { - "title": "Quels articles seront appliqués à la promotion?", - "description": "La promotion sera appliquée aux articles qui correspondent aux conditions suivantes." - }, - "buy-rules": { - "title": "Quels éléments doivent être dans le panier pour déverrouiller la promotion?", - "description": "Si ces conditions correspondent, nous activons la promotion sur les articles cibles." - } - } - }, - "tooltips": { - "campaignType": "Le code de devise doit être sélectionné dans la promotion pour définir un budget de dépense." - }, - "errors": { - "requiredField": "Champ requis", - "promotionTabError": "Corriger les erreurs dans la promotion avant de continuer" - }, - "toasts": { - "promotionCreateSuccess": "Promotion ({{code}}) a été créée avec succès." - }, - "create": {}, - "edit": { - "title": "Modifier les détails de la promotion", - "rules": { - "title": "Modifier les conditions d'utilisation" - }, - "target-rules": { - "title": "Modifier les conditions des articles" - }, - "buy-rules": { - "title": "Modifier les conditions d'achat" - } - }, - "campaign": { - "header": "Campaign", - "edit": { - "header": "Modifier la campagne", - "successToast": "La campagne de la promotion a été mise à jour avec succès." - }, - "actions": { - "goToCampaign": "Aller à la campagne" - } - }, - "campaign_currency": { - "tooltip": "C'est la devise de la promotion. Changez-la depuis l'onglet Détails." - }, - "form": { - "required": "Requis", - "and": "ET", - "selectAttribute": "Sélectionner un attribut", - "campaign": { - "existing": { - "title": "Campagne existante", - "description": "Ajouter la promotion à une campagne existante.", - "placeholder": { - "title": "Aucune campagne existante", - "desc": "Vous pouvez créer une pour suivre plusieurs promotions et définir des limites de budget." - } - }, - "new": { - "title": "Nouvelle campagne", - "description": "Créer une nouvelle campagne pour cette promotion." - }, - "none": { - "title": "Sans campagne", - "description": "Continuer sans associer la promotion à une campagne" - } - }, - "status": { - "title": "Status" - }, - "method": { - "label": "Method", - "code": { - "title": "Code de promotion", - "description": "Les clients doivent entrer ce code lors de la caisse" - }, - "automatic": { - "title": "Automatique", - "description": "Les clients verront cette promotion lors de la caisse" - } - }, - "max_quantity": { - "title": "Quantité maximum", - "description": "Quantité maximum d'articles à laquelle cette promotion s'applique." - }, - "type": { - "standard": { - "title": "Standard", - "description": "Une promotion standard" - }, - "buyget": { - "title": "Acheter X, obtenez Y", - "description": "Achat X, obtenez Y" - } - }, - "allocation": { - "each": { - "title": "Chaque", - "description": "Applique la valeur sur chaque article" - }, - "across": { - "title": "Sur tous les articles", - "description": "Applique la valeur sur tous les articles" - } - }, - "code": { - "title": "Code", - "description": "Le code que les clients entreront lors de la caisse." - }, - "value": { - "title": "Valeur de la promotion" - }, - "value_type": { - "fixed": { - "title": "Valeur de la promotion", - "description": "La somme à être remise. Exemple : 100" - }, - "percentage": { - "title": "Valeur de la promotion", - "description": "Le pourcentage à remiser. Exemple : 8%" - } - } - }, - "deleteWarning": "Vous êtes sur le point de supprimer la promotion {{code}}. Cette action ne peut être annulée.", - "createPromotionTitle": "Créer une promotion", - "type": "Type de promotion", - "conditions": { - "add": "Add condition", - "list": { - "noRecordsMessage": "Ajouter une condition pour restreindre les articles à laquelle la promotion s'applique." - } - } - }, - "campaigns": { - "domain": "Campagnes", - "details": "Détails de la campagne", - "status": { - "active": "Active", - "expired": "Expirée", - "scheduled": "Programmée" - }, - "delete": { - "title": "Êtes-vous sûr?", - "description": "Vous êtes sur le point de supprimer la campagne '{{name}}'. Cette action ne peut être annulée.", - "successToast": "La campagne '{{name}}' a été supprimée avec succès." - }, - "edit": { - "header": "Modifier la campagne", - "description": "Modifier les détails de la campagne.", - "successToast": "La campagne '{{name}}' a été mise à jour avec succès." - }, - "configuration": { - "header": "Configuration", - "edit": { - "header": "Modifier la configuration de la campagne", - "description": "Modifier la configuration de la campagne.", - "successToast": "La configuration de la campagne a été mise à jour avec succès." - } - }, - "create": { - "title": "Créer une campagne", - "description": "Créer une campagne promotionnelle.", - "hint": "Créer une campagne promotionnelle.", - "header": "Créer une campagne", - "successToast": "La campagne '{{name}}' a été créée avec succès." - }, - "fields": { - "name": "Nom", - "identifier": "Identifiant", - "start_date": "Date de début", - "end_date": "Date de fin", - "total_spend": "Budget dépensé", - "total_used": "Budget utilisé", - "budget_limit": "Limite de budget", - "campaign_id": { - "hint": "Seules les campagnes avec le même code de devise que la promotion sont affichées dans cette liste." - } - }, - "budget": { - "create": { - "hint": "Créer un budget pour la campagne.", - "header": "Budget de la campagne" - }, - "details": "Budget de la campagne", - "fields": { - "type": "Type", - "currency": "Devise", - "limit": "Limite", - "used": "Utilisé" - }, - "type": { - "spend": { - "title": "Dépense", - "description": "Définir une limite sur le montant total des remises de toutes les utilisations de la promotion." - }, - "usage": { - "title": "Usage", - "description": "Définir une limite sur le nombre de fois que la promotion peut être utilisée." - } - }, - "edit": { - "header": "Modifier le budget de la campagne" - } - }, - "promotions": { - "remove": { - "title": "Supprimer la promotion de la campagne", - "description": "Vous êtes sur le point de supprimer {{count}} promotion(s) de la campagne. Cette action ne peut être annulée." - }, - "alreadyAdded": "This promotion has already been added to the campaign.", - "alreadyAddedDiffCampaign": "Cette promotion a déjà été ajoutée à une autre campagne ({{name}}).", - "currencyMismatch": "La devise de la promotion et de la campagne ne correspondent pas", - "toast": { - "success": "Promotion(s) ajoutée(s) à la campagne avec succès" - }, - "add": { - "list": { - "noRecordsMessage": "Créer une promotion d'abord." - } - }, - "list": { - "noRecordsMessage": "Il n'y a pas de promotions dans la campagne." - } - }, - "deleteCampaignWarning": "Vous êtes sur le point de supprimer la campagne {{name}}. Cette action ne peut être annulée.", - "totalSpend": "<0>{{amount}} <1>{{currency}}" - }, - "priceLists": { - "domain": "Listes de prix", - "subtitle": "Créer des prix de vente ou des prix de remplacement pour des conditions spécifiques.", - "delete": { - "confirmation": "Vous êtes sur le point de supprimer la liste de prix {{title}}. Cette action ne peut être annulée.", - "successToast": "La liste de prix {{title}} a été supprimée avec succès." - }, - "create": { - "header": "Créer une liste de prix", - "subheader": "Créer une nouvelle liste de prix pour gérer les prix de vos produits.", - "tabs": { - "details": "Détails", - "products": "Produits", - "prices": "Prix" - }, - "successToast": "La liste de prix {{title}} a été créée avec succès.", - "products": { - "list": { - "noRecordsMessage": "Créer un produit d'abord." - } - } - }, - "edit": { - "header": "Modifier la liste de prix", - "successToast": "La liste de prix {{title}} a été mise à jour avec succès." - }, - "configuration": { - "header": "Configuration", - "edit": { - "header": "Modifier la configuration de la liste de prix", - "description": "Modifier la configuration de la liste de prix.", - "successToast": "La configuration de la liste de prix a été mise à jour avec succès." - } - }, - "products": { - "header": "Produits", - "actions": { - "addProducts": "Ajouter des produits", - "editPrices": "Modifier les prix" - }, - "delete": { - "confirmation_one": "Vous êtes sur le point de supprimer les prix pour {{count}} produit dans la liste de prix. Cette action ne peut être annulée.", - "confirmation_other": "Vous êtes sur le point de supprimer les prix pour {{count}} produits dans la liste de prix. Cette action ne peut être annulée.", - "successToast_one": "Les prix pour {{count}} produit ont été supprimés avec succès.", - "successToast_other": "Les prix pour {{count}} produits ont été supprimés avec succès." - }, - "add": { - "successToast": "Les prix ont été ajoutés à la liste de prix avec succès." - }, - "edit": { - "successToast": "Les prix ont été mis à jour avec succès." - } - }, - "fields": { - "priceOverrides": { - "label": "Prix de remplacement", - "header": "Prix de remplacement" - }, - "status": { - "label": "Status", - "options": { - "active": "Active", - "draft": "Brouillon", - "expired": "Expiré", - "scheduled": "Programmé" - } - }, - "type": { - "label": "Type", - "hint": "Choisir le type de liste de prix que vous souhaitez créer.", - "options": { - "sale": { - "label": "Prix de vente", - "description": "Les prix de vente sont des changements de prix temporaires pour les produits." - }, - "override": { - "label": "Remplacement", - "description": "Les remplacements sont généralement utilisés pour créer des prix spécifiques aux clients." - } - } - }, - "startsAt": { - "label": "La liste de prix a une date de début?", - "hint": "Planifier la liste de prix pour l'activation dans le futur." - }, - "endsAt": { - "label": "La liste de prix a une date d'expiration?", - "hint": "Planifier la liste de prix pour la désactivation dans le futur." - }, - "customerAvailability": { - "header": "Choisir les groupes de clients", - "label": "Disponibilité des clients", - "hint": "Choisir les groupes de clients auxquels la liste de prix doit s'appliquer.", - "placeholder": "Rechercher les groupes de clients", - "attribute": "Groupes de clients" - } - } - }, - "profile": { - "domain": "Profil", - "manageYourProfileDetails": "Gérer les détails de votre profil.", - "fields": { - "languageLabel": "Langue", - "usageInsightsLabel": "Données d'utilisation" - }, - "edit": { - "header": "Modifier le profil", - "languageHint": "La langue que vous souhaitez utiliser dans le tableau de bord administratif. Cela ne change pas la langue de votre boutique.", - "languagePlaceholder": "Sélectionner la langue", - "usageInsightsHint": "Partager les données d'utilisation et nous aider à améliorer Medusa. Vous pouvez en savoir plus sur ce que nous collectons et comment nous l'utilisons dans notre <0>documentation." - }, - "toast": { - "edit": "Modifications du profil enregistrées" - } - }, - "users": { - "domain": "Utilisateurs", - "editUser": "Modifier l'utilisateur", - "inviteUser": "Inviter un utilisateur", - "inviteUserHint": "Inviter un nouvel utilisateur à votre boutique.", - "sendInvite": "Envoyer l'invitation", - "pendingInvites": "Invitations en attente", - "deleteInviteWarning": "Vous êtes sur le point de supprimer l'invitation pour {{email}}. Cette action ne peut être annulée.", - "resendInvite": "Renvoyer l'invitation", - "copyInviteLink": "Copier le lien d'invitation", - "expiredOnDate": "Expiré le {{date}}", - "validFromUntil": "Valide de <0>{{from}} - <1>{{until}}", - "acceptedOnDate": "Accepté le {{date}}", - "inviteStatus": { - "accepted": "Accepté", - "pending": "En attente", - "expired": "Expiré" - }, - "roles": { - "admin": "Admin", - "developer": "Développeur", - "member": "Membre" - }, - "deleteUserWarning": "Vous êtes sur le point de supprimer l'utilisateur {{name}}. Cette action ne peut être annulée.", - "invite": "Inviter" - }, - "store": { - "domain": "Boutique", - "manageYourStoresDetails": "Gérer les détails de votre boutique.", - "editStore": "Modifier la boutique", - "defaultCurrency": "Devise par défaut", - "defaultRegion": "Région par défaut", - "swapLinkTemplate": "Lien de remplacement", - "paymentLinkTemplate": "Lien de paiement", - "inviteLinkTemplate": "Lien d'invitation", - "currencies": "Devises", - "addCurrencies": "Ajouter des devises", - "enableTaxInclusivePricing": "Activer le prix taxe inclusive", - "disableTaxInclusivePricing": "Désactiver le prix taxe inclusive", - "removeCurrencyWarning_one": "Vous êtes sur le point de supprimer {{count}} devise de votre boutique. Assurez-vous de supprimer tous les prix utilisant la devise avant de continuer.", - "removeCurrencyWarning_other": "Vous êtes sur le point de supprimer {{count}} devises de votre boutique. Assurez-vous de supprimer tous les prix utilisant les devises avant de continuer.", - "currencyAlreadyAdded": "La devise a déjà été ajoutée à votre boutique.", - "edit": { - "header": "Modifier la boutique" - }, - "toast": { - "update": "Boutique mise à jour avec succès", - "currenciesUpdated": "Devises mises à jour avec succès", - "currenciesRemoved": "Devises supprimées de la boutique avec succès", - "updatedTaxInclusivitySuccessfully": "Prix taxe inclusive mis à jour avec succès" - } - }, - "regions": { - "domain": "Régions", - "subtitle": "Une région est une zone dans laquelle vous vendez des produits. Elle peut couvrir plusieurs pays, et a des taux de taxe, fournisseurs et devises différents.", - "createRegion": "Créer une région", - "createRegionHint": "Gérer les taux de taxe et les fournisseurs pour un ensemble de pays.", - "addCountries": "Ajouter des pays", - "editRegion": "Modifier la région", - "countriesHint": "Ajouter les pays inclus dans cette région.", - "deleteRegionWarning": "Vous êtes sur le point de supprimer la région {{name}}. Cette action ne peut être annulée.", - "removeCountriesWarning_one": "Vous êtes sur le point de supprimer {{count}} pays de la région. Cette action ne peut être annulée.", - "removeCountriesWarning_other": "Vous êtes sur le point de supprimer {{count}} pays de la région. Cette action ne peut être annulée.", - "removeCountryWarning": "Vous êtes sur le point de supprimer le pays {{name}} de la région. Cette action ne peut être annulée.", - "automaticTaxesHint": "Lorsqu'elle est activée, les taxes ne sont calculées qu'à la caisse en fonction de l'adresse de livraison.", - "taxInclusiveHint": "Lorsqu'elle est activée, les prix dans la région sont taxe inclusive.", - "providersHint": "Ajouter les fournisseurs de paiement disponibles dans cette région.", - "shippingOptions": "Options de livraison", - "deleteShippingOptionWarning": "Vous êtes sur le point de supprimer l'option de livraison {{name}}. Cette action ne peut être annulée.", - "return": "Retour", - "outbound": "Sortant", - "priceType": "Type de prix", - "flatRate": "Tarif fixe", - "calculated": "Calculé", - "list": { - "noRecordsMessage": "Créer une région pour les zones dans lesquelles vous vendez." - }, - "toast": { - "delete": "Région supprimée avec succès", - "edit": "Région modifiée avec succès", - "create": "Région créée avec succès", - "countries": "Pays de la région mis à jour avec succès" - }, - "shippingOption": { - "createShippingOption": "Créer une option de livraison", - "createShippingOptionHint": "Créer une nouvelle option de livraison pour la région.", - "editShippingOption": "Modifier l'option de livraison", - "fulfillmentMethod": "Méthode de livraison", - "type": { - "outbound": "Outbound", - "outboundHint": "Utiliser cette option si vous créez une option de livraison pour envoyer des produits au client.", - "return": "Retour", - "returnHint": "Utiliser cette option si vous créez une option de livraison pour que le client retourne des produits à vous." - }, - "priceType": { - "label": "Type de prix", - "flatRate": "Tarif fixe", - "calculated": "Calculé" - }, - "availability": { - "adminOnly": "Admin uniquement", - "adminOnlyHint": "Lorsqu'elle est activée, l'option de livraison ne sera disponible que dans le tableau de bord administratif et pas dans la boutique." - }, - "taxInclusiveHint": "Lorsqu'elle est activée, le prix de l'option de livraison sera taxe inclusive.", - "requirements": { - "label": "Conditions", - "hint": "Spécifier les conditions pour l'option de livraison." - } - } - }, - "taxes": { - "domain": "Régions fiscales", - "domainDescription": "Gérer votre région fiscale", - "countries": { - "taxCountriesHint": "Les paramètres fiscaux s'appliquent aux pays listés." - }, - "settings": { - "editTaxSettings": "Modifier les paramètres fiscaux", - "taxProviderLabel": "Fournisseur", - "systemTaxProviderLabel": "Fournisseur de taxes système", - "calculateTaxesAutomaticallyLabel": "Calculer les taxes automatiquement", - "calculateTaxesAutomaticallyHint": "Lorsqu'elle est activée, les taux de taxes seront calculés automatiquement et appliqués aux paniers. Lorsqu'elle est désactivée, les taxes doivent être calculées manuellement à la caisse. Les taxes manuelles sont recommandées pour l'utilisation avec des fournisseurs de taxes tiers.", - "applyTaxesOnGiftCardsLabel": "Appliquer les taxes sur les cartes-cad", - "applyTaxesOnGiftCardsHint": "Lorsqu'elle est activée, les taxes seront appliquées aux cartes-cad à la caisse. Dans certains pays, les régulations fiscales requièrent l'application de taxes aux cartes-cad lors de l'achat.", - "defaultTaxRateLabel": "Taux de taxe par défaut", - "defaultTaxCodeLabel": "Code de taxe par défaut" - }, - "defaultRate": { - "sectionTitle": "Taux de taxe par défaut" - }, - "taxRate": { - "sectionTitle": "Taux de taxe", - "createTaxRate": "Créer un taux de taxe", - "createTaxRateHint": "Créer un nouveau taux de taxe pour la région.", - "deleteRateDescription": "Vous êtes sur le point de supprimer le taux de taxe {{name}}. Cette action ne peut être annulée.", - "editTaxRate": "Modifier le taux de taxe", - "editRateAction": "Modifier le taux", - "editOverridesAction": "Modifier les remplacements", - "editOverridesTitle": "Modifier les remplacements", - "editOverridesHint": "Spécifier les remplacements pour le taux de taxe.", - "deleteTaxRateWarning": "Vous êtes sur le point de supprimer le taux de taxe {{name}}. Cette action ne peut être annulée.", - "productOverridesLabel": "Remplacements de produits", - "productOverridesHint": "Spécifier les remplacements de produits pour le taux de taxe.", - "addProductOverridesAction": "Ajouter des remplacements de produits", - "productTypeOverridesLabel": "Remplacements de types de produits", - "productTypeOverridesHint": "Spécifier les remplacements de types de produits pour le taux de taxe.", - "addProductTypeOverridesAction": "Ajouter des remplacements de types de produits", - "shippingOptionOverridesLabel": "Remplacements d'options de livraison", - "shippingOptionOverridesHint": "Spécifier les remplacements d'options de livraison pour le taux de taxe.", - "addShippingOptionOverridesAction": "Ajouter des remplacements d'options de livraison", - "productOverridesHeader": "Produits", - "productTypeOverridesHeader": "Types de produits", - "shippingOptionOverridesHeader": "Options de livraison" - } - }, - "locations": { - "domain": "Localisations", - "editLocation": "Modifier la localisation", - "addSalesChannels": "Ajouter des canaux de vente", - "noLocationsFound": "Aucune localisation trouvée", - "selectLocations": "Sélectionner les localisations qui stockent l'article.", - "deleteLocationWarning": "Vous êtes sur le point de supprimer la localisation {{name}}. Cette action ne peut être annulée.", - "removeSalesChannelsWarning_one": "Vous êtes sur le point de supprimer {{count}} canal de vente de la localisation.", - "removeSalesChannelsWarning_other": "Vous êtes sur le point de supprimer {{count}} canaux de vente de la localisation.", - "toast": { - "create": "Localisation créée avec succès", - "update": "Localisation mise à jour avec succès", - "removeChannel": "Canal de vente supprimé avec succès" - } - }, - "reservations": { - "domain": "Réservations", - "subtitle": "Gérer la quantité réservée des articles en stock.", - "deleteWarning": "Vous êtes sur le point de supprimer une réservation. Cette action ne peut être annulée." - }, - "salesChannels": { - "domain": "Canaux de vente", - "subtitle": "Gérer les canaux de vente en ligne et hors ligne.", - "createSalesChannel": "Créer un canal de vente", - "createSalesChannelHint": "Créer un nouveau canal de vente pour vendre vos produits.", - "enabledHint": "Spécifier si le canal de vente est activé.", - "removeProductsWarning_one": "Vous êtes sur le point de supprimer {{count}} produit de {{sales_channel}}.", - "removeProductsWarning_other": "Vous êtes sur le point de supprimer {{count}} produits de {{sales_channel}}.", - "addProducts": "Ajouter des produits", - "editSalesChannel": "Modifier le canal de vente", - "productAlreadyAdded": "Le produit a déjà été ajouté au canal de vente.", - "deleteSalesChannelWarning": "Vous êtes sur le point de supprimer le canal de vente {{name}}. Cette action ne peut être annulée.", - "toast": { - "create": "Canal de vente créé avec succès", - "update": "Canal de vente mis à jour avec succès", - "delete": "Canal de vente supprimé avec succès" - }, - "products": { - "list": { - "noRecordsMessage": "Il n'y a aucun produit dans le canal de vente." - }, - "add": { - "list": { - "noRecordsMessage": "Créer un produit d'abord." - } - } - } - }, - "apiKeyManagement": { - "domain": { - "publishable": "Publishable API Keys", - "secret": "Secret API Keys" - }, - "subtitle": { - "publishable": "Gérer les clés API utilisées dans le storefront pour limiter la portée des requêtes aux canaux de vente spécifiques.", - "secret": "Gérer les clés API utilisées pour authentifier les utilisateurs administratifs dans les applications administratives." - }, - "status": { - "active": "Active", - "revoked": "Revoqué" - }, - "type": { - "publishable": "Publique", - "secret": "Secrète" - }, - "create": { - "createPublishableHeader": "Créer une clé API publique", - "createPublishableHint": "Créer une nouvelle clé API publique pour limiter la portée des requêtes aux canaux de vente spécifiques.", - "createSecretHeader": "Créer une clé API secrète", - "createSecretHint": "Créer une nouvelle clé API secrète pour accéder à l'API Medusa en tant qu'utilisateur administratif authentifié.", - "secretKeyCreatedHeader": "Clé secrète créée", - "secretKeyCreatedHint": "Votre nouvelle clé secrète a été générée. Copiez et stockez-la maintenant. C'est la seule fois où elle sera affichée.", - "copySecretTokenSuccess": "Clé secrète copiée dans le presse-papiers.", - "copySecretTokenFailure": "Erreur lors de la copie de la clé secrète dans le presse-papiers.", - "successToast": "Clé API créée avec succès." - }, - "edit": { - "header": "Modifier la clé API", - "description": "Modifier le titre de la clé API.", - "successToast": "Clé API {{title}} mise à jour avec succès." - }, - "salesChannels": { - "title": "Ajouter des canaux de vente", - "description": "Ajouter les canaux de vente auxquels la clé API doit être limitée.", - "successToast_one": "{{count}} canal de vente a été ajouté à la clé API.", - "successToast_other": "{{count}} canaux de vente ont été ajoutés à la clé API.", - "alreadyAddedTooltip": "Le canal de vente a déjà été ajouté à la clé API.", - "list": { - "noRecordsMessage": "Il n'y a aucun canal de vente dans la portée de la clé API publique." - } - }, - "delete": { - "warning": "Vous êtes sur le point de supprimer la clé API {{title}}. Cette action ne peut être annulée.", - "successToast": "Clé API {{title}} supprimée avec succès." - }, - "revoke": { - "warning": "Vous êtes sur le point de révoquer la clé API {{title}}. Cette action ne peut être annulée.", - "successToast": "Clé API {{title}} révoquée avec succès." - }, - "addSalesChannels": { - "list": { - "noRecordsMessage": "Créer un canal de vente d'abord." - } - }, - "removeSalesChannel": { - "warning": "Vous êtes sur le point de supprimer le canal de vente {{name}} de la clé API. Cette action ne peut être annulée.", - "warningBatch_one": "Vous êtes sur le point de supprimer {{count}} canal de vente de la clé API. Cette action ne peut être annulée.", - "warningBatch_other": "Vous êtes sur le point de supprimer {{count}} canaux de vente de la clé API. Cette action ne peut être annulée.", - "successToast": "Canal de vente supprimé avec succès de la clé API.", - "successToastBatch_one": "{{count}} canal de vente a été supprimé de la clé API.", - "successToastBatch_other": "{{count}} canaux de vente ont été supprimés de la clé API." - }, - "actions": { - "revoke": "Révoquer la clé API", - "copy": "Copier la clé API", - "copySuccessToast": "Clé API copiée dans le presse-papiers." - }, - "table": { - "lastUsedAtHeader": "Dernière utilisation", - "createdAtHeader": "Annuler à" - }, - "fields": { - "lastUsedAtLabel": "Dernière utilisation", - "revokedByLabel": "Annuler par", - "revokedAtLabel": "Annuler à", - "createdByLabel": "Créée par" - } - }, - "returnReasons": { - "domain": "Raison de retour", - "subtitle": "Gérer les raisons des retours.", - "calloutHint": "Gérer les raisons pour classer les retours.", - "editReason": "Modifier la raison de retour", - "create": { - "header": "Ajouter une raison de retour", - "subtitle": "Spécifier les raisons les plus courantes des retours.", - "hint": "Créer une nouvelle raison de retour pour classer les retours.", - "successToast": "Raison de retour {{label}} créée avec succès." - }, - "edit": { - "header": "Modifier la raison de retour", - "subtitle": "Modifier la valeur de la raison de retour.", - "successToast": "Raison de retour {{label}} mise à jour avec succès." - }, - "delete": { - "confirmation": "Vous êtes sur le point de supprimer la raison de retour {{label}}. Cette action ne peut être annulée.", - "successToast": "Raison de retour {{label}} supprimée avec succès." - }, - "fields": { - "value": { - "label": "Valeur", - "placeholder": "mauvaise_taille", - "tooltip": "La valeur doit être un identifiant unique pour la raison de retour." - }, - "label": { "label": "Label", "placeholder": "Mauvaise taille" }, - "description": { - "label": "Description", - "placeholder": "Le client a reçu la mauvaise taille" - } - } - }, - "login": { - "forgotPassword": "Mot de passe oublié? - <0>Réinitialiser", - "title": "Bienvenue sur Medusa", - "hint": "Connectez-vous pour accéder à l'espace administratif" - }, - "invite": { - "title": "Bienvenue sur Medusa", - "hint": "Créez votre compte ci-dessous", - "backToLogin": "Retour à la connexion", - "createAccount": "Créer un compte", - "alreadyHaveAccount": "Vous avez déjà un compte? - <0>Se connecter", - "emailTooltip": "Votre email ne peut être modifié. Si vous souhaitez utiliser un autre email, un nouveau lien d'invitation doit être envoyé.", - "invalidInvite": "Le lien d'invitation est invalide ou a expiré.", - "successTitle": "Votre compte a été enregistré", - "successHint": "Commencez avec Medusa Admin dès maintenant.", - "successAction": "Commencer Medusa Admin", - "invalidTokenTitle": "Votre lien d'invitation est invalide", - "invalidTokenHint": "Essayez de demander un nouveau lien d'invitation.", - "passwordMismatch": "Les mots de passe ne correspondent pas", - "toast": { - "accepted": "Invitation acceptée avec succès" - } - }, - "resetPassword": { - "title": "Réinitialiser le mot de passe", - "hint": "Entrez votre email ci-dessous, et nous vous enverrons des instructions sur la façon de réinitialiser votre mot de passe.", - "email": "Email", - "sendResetInstructions": "Envoyer les instructions de réinitialisation", - "backToLogin": "<0>Retour à la connexion", - "newPasswordHint": "Choisissez un nouveau mot de passe ci-dessous.", - "invalidTokenTitle": "Votre lien de réinitialisation est invalide", - "invalidTokenHint": "Essayez de demander un nouveau lien de réinitialisation.", - "expiredTokenTitle": "Votre lien de réinitialisation a expiré", - "goToResetPassword": "Aller à la réinitialisation du mot de passe", - "resetPassword": "Réinitialiser le mot de passe", - "newPassword": "Nouveau mot de passe", - "repeatNewPassword": "Répéter le nouveau mot de passe", - "tokenExpiresIn": "Le lien expire dans <0>{{time}} minutes", - "successfulRequestTitle": "Un email vous a été envoyé", - "successfulRequest": "Nous vous avons envoyé un email que vous pouvez utiliser pour réinitialiser votre mot de passe. Vérifiez votre dossier de spam si vous n'avez pas reçu l'email après quelques minutes.", - "successfulResetTitle": "Réinitialisation du mot de passe réussie", - "successfulReset": "Veuillez vous connecter sur la page de connexion.", - "passwordMismatch": "Les mots de passe ne correspondent pas", - "invalidLinkTitle": "Votre lien de réinitialisation est invalide", - "invalidLinkHint": "Essayez de réinitialiser votre mot de passe à nouveau." - }, - "workflowExecutions": { - "domain": "Workflows", - "subtitle": "Voir et suivre les exécutions des workflows dans votre application Medusa.", - "transactionIdLabel": "ID de la transaction", - "workflowIdLabel": "ID du workflow", - "progressLabel": "Progression", - "stepsCompletedLabel_one": "{{completed}} de {{count}} étape", - "stepsCompletedLabel_other": "{{completed}} de {{count}} étapes", - "list": { - "noRecordsMessage": "Aucun workflow n'a été exécuté, pour le moment." - }, - "history": { - "sectionTitle": "Historique", - "runningState": "En cours...", - "awaitingState": "En attente", - "failedState": "Échec", - "skippedState": "Ignoré", - "skippedFailureState": "Ignoré (Échec)", - "definitionLabel": "Définition", - "outputLabel": "Sortie", - "compensateInputLabel": "Entrée de compensation", - "revertedLabel": "Inversé", - "errorLabel": "Erreur" - }, - "state": { - "done": "Terminé", - "failed": "Échec", - "reverted": "Inversé", - "invoking": "Déclencher", - "compensating": "Compenser", - "notStarted": "Non démarré" - }, - "transaction": { - "state": { - "waitingToCompensate": "En attente de compensation" - } - }, - "step": { - "state": { - "skipped": "Ignoré", - "skippedFailure": "Ignoré (Échec)", - "dormant": "Dormant", - "timeout": "Timeout" - } - } - }, - "productTypes": { - "domain": "Types de produits", - "subtitle": "Organiser vos produits en types.", - "create": { - "header": "Créer un type de produit", - "hint": "Créer un nouveau type de produit pour classer vos produits.", - "successToast": "Type de produit {{value}} créé avec succès." - }, - "edit": { - "header": "Modifier le type de produit", - "successToast": "Type de produit {{value}} mis à jour avec succès." - }, - "delete": { - "confirmation": "Vous êtes sur le point de supprimer le type de produit {{value}}. Cette action ne peut être annulée.", - "successToast": "Type de produit {{value}} supprimé avec succès." - }, - "fields": { - "value": "Valeur" - } - }, - "productTags": { - "domain": "Tags de produits", - "create": { - "header": "Créer un tag de produit", - "subtitle": "Créer un nouveau tag de produit pour classer vos produits.", - "successToast": "Tag de produit {{value}} créé avec succès." - }, - "edit": { - "header": "Modifier le tag de produit", - "subtitle": "Modifier la valeur du tag de produit.", - "successToast": "Tag de produit {{value}} mis à jour avec succès." - }, - "delete": { - "confirmation": "Vous êtes sur le point de supprimer le tag de produit {{value}}. Cette action ne peut être annulée.", - "successToast": "Tag de produit {{value}} supprimé avec succès." - }, - "fields": { - "value": "Valeur" - } - }, - "notifications": { - "domain": "Notifications", - "emptyState": { - "title": "Aucune notification", - "description": "Vous n'avez aucune notification pour le moment, mais elles apparaîtront ici une fois que vous les aurez." - }, - "accessibility": { - "description": "Les notifications concernant les activités Medusa seront listées ici." - } - }, - "errors": { - "serverError": "Erreur serveur - Réessayez plus tard.", - "invalidCredentials": "Mauvais email ou mot de passe" - }, - "statuses": { - "scheduled": "Programmé", - "expired": "Expiré", - "active": "Actif", - "enabled": "Activé", - "disabled": "Désactivé" - }, - "labels": { - "productVariant": "Variante de produit", - "prices": "Prix", - "available": "Disponible", - "inStock": "En stock", - "added": "Ajouté", - "removed": "Supprimé" - }, - "fields": { - "amount": "Montant", - "refundAmount": "Montant de remboursement", - "name": "Nom", - "default": "Par défaut", - "lastName": "Nom de famille", - "firstName": "Prénom", - "title": "Titre", - "customTitle": "Titre personnalisé", - "manageInventory": "Gérer l'inventaire", - "inventoryKit": "Kit d'inventaire", - "inventoryItems": "Articles en inventaire", - "inventoryItem": "Article en inventaire", - "requiredQuantity": "Quantité requise", - "description": "Description", - "email": "Email", - "password": "Mot de passe", - "repeatPassword": "Répéter le mot de passe", - "confirmPassword": "Confirmer le mot de passe", - "newPassword": "Nouveau mot de passe", - "repeatNewPassword": "Répéter le nouveau mot de passe", - "categories": "Catégories", - "shippingMethod": "Méthode de livraison", - "configurations": "Configurations", - "conditions": "Conditions", - "category": "Catégorie", - "collection": "Collection", - "discountable": "Remise", - "handle": "Identifiant", - "subtitle": "Sous-titre", - "item": "Article", - "qty": "Qté", - "limit": "Limite", - "tags": "Tags", - "type": "Type", - "reason": "Raison", - "none": "Aucun", - "all": "Tous", - "search": "Rechercher", - "percentage": "Pourcentage", - "sales_channels": "Canaux de vente", - "customer_groups": "Groupes de clients", - "product_tags": "Tags de produits", - "product_types": "Types de produits", - "product_collections": "Collections de produits", - "status": "Statut", - "code": "Code", - "value": "Valeur", - "disabled": "Désactivé", - "dynamic": "Dynamique", - "normal": "Normal", - "years": "Années", - "months": "Mois", - "days": "Jours", - "hours": "Heures", - "minutes": "Minutes", - "totalRedemptions": "Total d'utilisations", - "countries": "Pays", - "paymentProviders": "Fournisseurs de paiement", - "refundReason": "Raison de remboursement", - "fulfillmentProviders": "Fournisseurs de livraison", - "fulfillmentProvider": "Fournisseur de livraison", - "providers": "Fournisseurs", - "availability": "Disponibilité", - "inventory": "Inventaire", - "optional": "Optionnel", - "note": "Note", - "automaticTaxes": "Taxes automatiques", - "taxInclusivePricing": "Prix incluant les taxes", - "currency": "Monnaie", - "address": "Adresse", - "address2": "Appartement, suite, etc.", - "city": "Ville", - "postalCode": "Code postal", - "country": "Pays", - "state": "État", - "province": "Province", - "company": "Entreprise", - "phone": "Téléphone", - "metadata": "Métadonnées", - "selectCountry": "Sélectionner un pays", - "products": "Produits", - "variants": "Variantes", - "orders": "Commandes", - "account": "Compte", - "total": "Total de la commande", - "paidTotal": "Total capturé", - "totalExclTax": "Total hors taxes", - "subtotal": "Sous-total", - "shipping": "Livraison", - "outboundShipping": "Outbound Shipping", - "returnShipping": "Return Shipping", - "tax": "Taxe", - "created": "Créé", - "key": "Clé", - "customer": "Client", - "date": "Date", - "order": "Commande", - "fulfillment": "Livraison", - "provider": "Fournisseur", - "payment": "Paiement", - "items": "Articles", - "salesChannel": "Canal de vente", - "region": "Région", - "discount": "Remise", - "role": "Rôle", - "sent": "Envoyé", - "salesChannels": "Canaux de vente", - "product": "Produit", - "createdAt": "Créé", - "updatedAt": "Mis à jour", - "revokedAt": "Annulé à", - "true": "Vrai", - "false": "Faux", - "giftCard": "Carte cadeau", - "tag": "Tag", - "dateIssued": "Date d'émission", - "issuedDate": "Date d'émission", - "expiryDate": "Date d'expiration", - "price": "Prix", - "priceTemplate": "Prix {{regionOrCurrency}}", - "height": "Hauteur", - "width": "Largeur", - "length": "Longueur", - "weight": "Poids", - "midCode": "MID code", - "hsCode": "HS code", - "ean": "EAN", - "upc": "UPC", - "inventoryQuantity": "Quantité en inventaire", - "barcode": "Code barre", - "countryOfOrigin": "Pays d'origine", - "material": "Matière", - "thumbnail": "Miniature", - "sku": "SKU", - "managedInventory": "Inventaire géré", - "allowBackorder": "Autoriser les commandes en rupture de stock", - "inStock": "En stock", - "location": "Emplacement", - "quantity": "Quantité", - "variant": "Variante", - "id": "ID", - "parent": "Parent", - "minSubtotal": "Min. Sous-total", - "maxSubtotal": "Max. Sous-total", - "shippingProfile": "Profil de livraison", - "summary": "Résumé", - "details": "Détails", - "label": "Label", - "rate": "Taux", - "requiresShipping": "Requiert une livraison", - "unitPrice": "Prix unitaire", - "startDate": "Date de début", - "endDate": "Date de fin", - "draft": "Brouillon", - "values": "Valeurs" - }, - "dateTime": { - "years_one": "Année", - "years_other": "Années", - "months_one": "Mois", - "months_other": "Mois", - "weeks_one": "Semaine", - "weeks_other": "Semaines", - "days_one": "Jour", - "days_other": "Jours", - "hours_one": "Heure", - "hours_other": "Heures", - "minutes_one": "Minute", - "minutes_other": "Minutes", - "seconds_one": "Seconde", - "seconds_other": "Secondes" - } + } + }, + "fields": { + "displayId": "Display ID", + "refundableAmount": "Montant remboursable", + "returnableQuantity": "Quantité retournable" + } + }, + "draftOrders": { + "domain": "Commandes à rédiger", + "deleteWarning": "Vous êtes sur le point de supprimer la commande à rédiger {{id}}. Cette action ne peut être annulée.", + "paymentLinkLabel": "Lien de paiement", + "cartIdLabel": "ID de panier", + "markAsPaid": { + "label": "Marquer comme payée", + "warningTitle": "Marquer comme payée", + "warningDescription": "Vous êtes sur le point de marquer la commande à rédiger comme payée. Cette action ne peut être annulée, et la collecte de paiement ne sera pas possible plus tard." + }, + "status": { + "open": "Ouvrir", + "completed": "Terminée" + }, + "create": { + "createDraftOrder": "Créer un brouillon de commande", + "createDraftOrderHint": "Créer un nouveau brouillon de commande pour gérer les détails d'une commande avant qu'elle ne soit passée.", + "chooseRegionHint": "Choisir une région", + "existingItemsLabel": "Articles existants", + "existingItemsHint": "Ajouter des articles existants au brouillon de commande.", + "customItemsLabel": "Articles personnalisés", + "customItemsHint": "Ajouter des articles personnalisés au brouillon de commande.", + "addExistingItemsAction": "Ajouter des articles existants", + "addCustomItemAction": "Ajouter un article personnalisé", + "noCustomItemsAddedLabel": "Aucun article personnalisé ajouté pour le moment", + "noExistingItemsAddedLabel": "Aucun article existant ajouté pour le moment", + "chooseRegionTooltip": "Choisir une région d'abord", + "useExistingCustomerLabel": "Utiliser un client existant", + "addShippingMethodsAction": "Ajouter des méthodes de livraison", + "unitPriceOverrideLabel": "Prix unitaire remplacé", + "shippingOptionLabel": "Option de livraison", + "shippingOptionHint": "Choisir l'option de livraison pour le brouillon de commande.", + "shippingPriceOverrideLabel": "Prix de livraison remplacé", + "shippingPriceOverrideHint": "Remplacer le prix de livraison pour le brouillon de commande.", + "sendNotificationLabel": "Envoyer une notification", + "sendNotificationHint": "Envoyer une notification au client lors de la création du brouillon de commande." + }, + "validation": { + "requiredEmailOrCustomer": "Email ou client requis.", + "requiredItems": "Au moins un article est requis.", + "invalidEmail": "L'email doit être une adresse email valide." + } + }, + "stockLocations": { + "domain": "Emplacements & Livraison", + "list": { + "description": "Gérer les emplacements de stock et les options de livraison de votre boutique." + }, + "create": { + "header": "Créer un emplacement de stock", + "hint": "Un emplacement de stock est un site physique où les produits sont stockés et expédiés.", + "successToast": "Emplacement {{name}} créé avec succès." + }, + "edit": { + "header": "Modifier l'emplacement de stock", + "viewInventory": "Voir le stock", + "successToast": "Emplacement {{name}} modifié avec succès." + }, + "delete": { + "confirmation": "Vous êtes sur le point de supprimer l'emplacement de stock {{name}}. Cette action ne peut être annulée." + }, + "fulfillmentProviders": { + "header": "Fournisseurs de livraison", + "shippingOptionsTooltip": "Ce menu déroulant ne contient que les fournisseurs activés pour cet emplacement. Ajoutez-les à l'emplacement si le menu déroulant est désactivé.", + "label": "Fournisseurs de livraison connectés", + "connectedTo": "Connecté à {{count}} de {{total}} fournisseurs de livraison", + "noProviders": "Cet emplacement de stock n'est connecté à aucun fournisseur de livraison.", + "action": "Connecter les fournisseurs", + "successToast": "Fournisseurs de livraison pour l'emplacement de stock mis à jour avec succès." + }, + "fulfillmentSets": { + "pickup": { + "header": "Retrait" + }, + "shipping": { + "header": "Livraison" + }, + "disable": { + "confirmation": "Êtes-vous sûr de vouloir désactiver {{name}} ? Cette action supprimera tous les zones de service et les options de livraison associées et ne peut être annulée.", + "pickup": "Retrait désactivé avec succès.", + "shipping": "Livraison désactivée avec succès." + }, + "enable": { + "pickup": "Retrait activé avec succès.", + "shipping": "Livraison activée avec succès." + } + }, + "sidebar": { + "header": "Configuration de la livraison", + "shippingProfiles": { + "label": "Profils de livraison", + "description": "Grouper les produits par besoins de livraison" + } + }, + "salesChannels": { + "header": "Canaux de vente", + "label": "Canaux de vente connectés", + "connectedTo": "Connecté à {{count}} de {{total}} canaux de vente", + "noChannels": "L'emplacement n'est connecté à aucun canal de vente.", + "action": "Connecter les canaux de vente", + "successToast": "Canaux de vente mis à jour avec succès." + }, + "shippingOptions": { + "create": { + "shipping": { + "header": "Créer une option de livraison pour {{zone}}", + "hint": "Créer une nouvelle option de livraison pour définir comment les produits sont expédiés depuis cet emplacement.", + "label": "Options de livraison", + "successToast": "Option de livraison {{name}} créée avec succès." + }, + "returns": { + "header": "Créer une option de retour pour {{zone}}", + "hint": "Créer une nouvelle option de retour pour définir comment les produits sont retournés à cet emplacement.", + "label": "Options de retour", + "successToast": "Option de retour {{name}} créée avec succès." + }, + "tabs": { + "details": "Détails", + "prices": "Prix" + }, + "action": "Créer une option" + }, + "delete": { + "confirmation": "Vous êtes sur le point de supprimer l'option de livraison {{name}}. Cette action ne peut être annulée.", + "successToast": "Option de livraison {{name}} supprimée avec succès." + }, + "edit": { + "header": "Modifier l'option de livraison", + "action": "Modifier l'option", + "successToast": "Option de livraison {{name}} modifiée avec succès." + }, + "pricing": { + "action": "Modifier les prix" + }, + "fields": { + "count": { + "shipping_one": "{{count}} shipping option", + "shipping_other": "{{count}} options de livraison", + "returns_one": "{{count}} option de retour", + "returns_other": "{{count}} options de retour" + }, + "priceType": { + "label": "Type de prix", + "options": { + "fixed": { + "label": "Fixe", + "hint": "Le prix de l'option de livraison est fixe et ne change pas en fonction du contenu de la commande." + }, + "calculated": { + "label": "Calculé", + "hint": "Le prix de l'option de livraison est calculé par le fournisseur de livraison lors de la passation de commande." + } + } + }, + "enableInStore": { + "label": "Activer en magasin", + "hint": "Indique si les clients peuvent utiliser cette option lors de la passation de commande." + }, + "provider": "Fournisseur de livraison", + "profile": "Profil de livraison" + } + }, + "serviceZones": { + "create": { + "headerPickup": "Créer une zone de service pour le retrait depuis {{location}}", + "headerShipping": "Créer une zone de service pour la livraison depuis {{location}}", + "action": "Créer une zone de service", + "successToast": "Zone de service {{name}} créée avec succès." + }, + "edit": { + "header": "Modifier la zone de service", + "successToast": "La zone de service {{name}} a été modifiée avec succès." + }, + "delete": { + "confirmation": "Vous êtes sur le point de supprimer la zone de service {{name}}. Cette action ne peut être annulée.", + "successToast": "La zone de service {{name}} a été supprimée avec succès." + }, + "manageAreas": { + "header": "Gérer les zones pour {{name}}", + "action": "Gérer les zones", + "label": "Zones", + "hint": "Sélectionner les zones géographiques que la zone de service couvre.", + "successToast": "Les zones pour {{name}} ont été mises à jour avec succès." + }, + "fields": { + "noRecords": "Il n'y a pas de zones de service pour ajouter des options de livraison.", + "tip": "Une zone de service est une collection de zones géographiques ou d'aires. Elle est utilisée pour restreindre les options de livraison disponibles à un ensemble de lieux défini." + } + } + }, + "shippingProfile": { + "domain": "Profils de livraison", + "subtitle": "Regrouper les produits avec des besoins de livraison similaires dans des profils.", + "create": { + "header": "Créer un profil de livraison", + "hint": "Créer un nouveau profil de livraison pour regrouper les produits avec des besoins de livraison similaires.", + "successToast": "Le profil de livraison {{name}} a été créé avec succès." + }, + "delete": { + "title": "Supprimer le profil de livraison", + "description": "Vous êtes sur le point de supprimer le profil de livraison {{name}}. Cette action ne peut être annulée.", + "successToast": "Le profil de livraison {{name}} a été supprimé avec succès." + }, + "tooltip": { + "type": "Entrer le type de profil de livraison, par exemple : Poids lourd, Surdimensionné, Livraison exclusive, etc." + } + }, + "taxRegions": { + "domain": "Taxes par régions", + "list": { + "hint": "Gérer les taxes que vous facturez à vos clients lorsqu'ils achètent depuis différents pays et régions." + }, + "delete": { + "confirmation": "Vous êtes sur le point de supprimer une taxe. Cette action ne peut être annulée.", + "successToast": "La taxe a été supprimée avec succès." + }, + "create": { + "header": "Créer une taxe", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un pays spécifique.", + "errors": { + "rateIsRequired": "Le taux de taxe est requis lors de la création d'une taxe par défaut.", + "nameIsRequired": "Le nom est requis lors de la création d'une taxe par défaut." + }, + "successToast": "La taxe a été créée avec succès." + }, + "province": { + "header": "Région", + "create": { + "header": "Créer une taxe pour la région", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour une province spécifique." + } + }, + "state": { + "header": "États", + "create": { + "header": "Créer une taxe pour les états", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un état spécifique." + } + }, + "stateOrTerritory": { + "header": "États ou territoires", + "create": { + "header": "Créer une taxe pour les états ou territoires", + "hint": "Create a new tax region to define tax rates for a specific state/territory." + } + }, + "county": { + "header": "Comtés", + "create": { + "header": "Créer une taxe pour les comtés", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un comté spécifique." + } + }, + "region": { + "header": "Régions", + "create": { + "header": "Créer une taxe pour les régions", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour une région spécifique." + } + }, + "department": { + "header": "Départements", + "create": { + "header": "Créer une taxe pour les départements", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un département spécifique." + } + }, + "territory": { + "header": "Territories", + "create": { + "header": "Créer une taxe pour les territoires", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un territoire spécifique." + } + }, + "prefecture": { + "header": "Préfectures", + "create": { + "header": "Créer une taxe pour les préfectures", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour une préfecture spécifique." + } + }, + "district": { + "header": "Quartiers", + "create": { + "header": "Créer une taxe pour les quartiers", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un district spécifique." + } + }, + "governorate": { + "header": "Gouvernorats", + "create": { + "header": "Créer une taxe pour les gouvernorats", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un gouvernorat spécifique." + } + }, + "canton": { + "header": "Cantons", + "create": { + "header": "Créer une taxe pour les cantons", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un canton spécifique." + } + }, + "emirate": { + "header": "Emirates", + "create": { + "header": "Créer une taxe pour les émirats", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un émirat spécifique." + } + }, + "sublevel": { + "header": "Niveaux subordonnés", + "create": { + "header": "Créer une taxe pour les niveaux subordonnés", + "hint": "Créer une nouvelle taxe pour définir les taux de taxe pour un niveau subordonné spécifique." + } + }, + "taxOverrides": { + "header": "Overrides", + "create": { + "header": "Créer une surcharge", + "hint": "Créer une taxe qui surcharge les taux de taxe par défaut pour les conditions sélectionnées." + }, + "edit": { + "header": "Modifier la surcharge", + "hint": "Modifier la taxe qui surcharge les taux de taxe par défaut pour les conditions sélectionnées." + } + }, + "taxRates": { + "create": { + "header": "Créer un taux d'imposition", + "hint": "Créer une nouvelle taxe pour définir le taux de taxe pour une région.", + "successToast": "Taxe créée avec succès." + }, + "edit": { + "header": "Modifier la taxe", + "hint": "Modifier la taxe pour définir le taux de taxe pour une région.", + "successToast": "Taxe modifiée avec succès." + }, + "delete": { + "confirmation": "Vous êtes sur le point de supprimer la taxe {{name}}. Cette action ne peut être annulée.", + "successToast": "La taxe a été supprimée avec succès." + } + }, + "fields": { + "isCombinable": { + "label": "Combinable", + "hint": "Indique si cette taxe peut être combinée avec le taux de taxe par défaut de la région.", + "true": "Combinable", + "false": "Non combinable" + }, + "defaultTaxRate": { + "label": "Taux de taxe par défaut", + "tooltip": "Le taux de taxe par défaut pour cette région. Un exemple est le taux de TVA standard pour un pays ou une région.", + "action": "Créer un taux de taxe par défaut" + }, + "taxRate": "Taux de taxe", + "taxCode": "Code de taxe", + "targets": { + "label": "Cibles", + "hint": "Sélectionner les cibles à laquelle cette taxe s'appliquera.", + "options": { + "product": "Produits", + "productCollection": "Collections de produits", + "productTag": "Tags de produits", + "productType": "Types de produits", + "customerGroup": "Groupes de clients" + }, + "operators": { + "in": "dans", + "on": "sur", + "and": "et" + }, + "placeholders": { + "product": "Rechercher des produits", + "productCollection": "Rechercher des collections de produits", + "productTag": "Rechercher des tags de produits", + "productType": "Rechercher des types de produits", + "customerGroup": "Rechercher des groupes de clients" + }, + "tags": { + "product": "Produit", + "productCollection": "Collection de produits", + "productTag": "Tag de produit", + "productType": "Type de produit", + "customerGroup": "Groupe de clients" + }, + "modal": { + "header": "Ajouter des cibles" + }, + "values_one": "{{count}} valeur", + "values_other": "{{count}} valeurs", + "numberOfTargets_one": "{{count}} cible", + "numberOfTargets_other": "{{count}} cibles", + "additionalValues_one": "et {{count}} plus de valeur", + "additionalValues_other": "et {{count}} plus de valeurs", + "action": "Ajouter une cible" + }, + "sublevels": { + "labels": { + "province": "Province", + "state": "État", + "region": "Région", + "stateOrTerritory": "État/Territoire", + "department": "Département", + "county": "Comté", + "territory": "Territoire", + "prefecture": "Préfecture", + "district": "Quartier", + "governorate": "Gouvernorat", + "emirate": "Émirat", + "canton": "Canton", + "sublevel": "Code de niveau subordonné" + }, + "placeholders": { + "province": "Select province", + "state": "Sélectionner l'état", + "region": "Sélectionner la région", + "stateOrTerritory": "Sélectionner l'état/territoire", + "department": "Sélectionner le département", + "county": "Sélectionner le comté", + "territory": "Sélectionner le territoire", + "prefecture": "Sélectionner la préfecture", + "district": "Sélectionner le quartier", + "governorate": "Sélectionner le gouvernorat", + "emirate": "Sélectionner l'émirat", + "canton": "Sélectionner le canton" + }, + "tooltips": { + "sublevel": "Entrer le code ISO 3166-2 pour la région de taxe subordonnée.", + "notPartOfCountry": "{{province}} ne semble pas être une partie de {{country}}. Veuillez vérifier si c'est correct." + }, + "alert": { + "header": "Les régions subordonnées sont désactivées pour cette taxe", + "description": "Les régions subordonnées sont désactivées pour cette région par défaut. Vous pouvez les activer pour créer des régions subordonnées comme des provinces, des états ou des territoires.", + "action": "Activer les régions subordonnées" + } + }, + "noDefaultRate": { + "label": "Pas de taux de taxe par défaut", + "tooltip": "Cette région de taxe n'a pas de taux de taxe par défaut. Si un taux standard est appliqué, comme la TVA d'un pays, veuillez l'ajouter à cette région." + } + } + }, + "promotions": { + "domain": "Promotions", + "sections": { + "details": "Détails de la promotion" + }, + "tabs": { + "template": "Type", + "details": "Détails", + "campaign": "Campagne" + }, + "fields": { + "type": "Type", + "value_type": "Type de valeur", + "value": "Valeur", + "campaign": "Campagne", + "method": "Méthode", + "allocation": "Allocation", + "addCondition": "Ajouter une condition", + "clearAll": "Effacer tout", + "amount": { + "tooltip": "Sélectionner le code de devise pour activer la définition de la valeur" + }, + "conditions": { + "rules": { + "title": "Qui peut utiliser ce code?", + "description": "Quel client est autorisé à utiliser le code de promotion? Le code de promotion peut être utilisé par tous les clients si laissé tel quel." + }, + "target-rules": { + "title": "Quels articles seront appliqués à la promotion?", + "description": "La promotion sera appliquée aux articles qui correspondent aux conditions suivantes." + }, + "buy-rules": { + "title": "Quels éléments doivent être dans le panier pour déverrouiller la promotion?", + "description": "Si ces conditions correspondent, nous activons la promotion sur les articles cibles." + } + } + }, + "tooltips": { + "campaignType": "Le code de devise doit être sélectionné dans la promotion pour définir un budget de dépense." + }, + "errors": { + "requiredField": "Champ requis", + "promotionTabError": "Corriger les erreurs dans la promotion avant de continuer" + }, + "toasts": { + "promotionCreateSuccess": "Promotion ({{code}}) a été créée avec succès." + }, + "create": {}, + "edit": { + "title": "Modifier les détails de la promotion", + "rules": { + "title": "Modifier les conditions d'utilisation" + }, + "target-rules": { + "title": "Modifier les conditions des articles" + }, + "buy-rules": { + "title": "Modifier les conditions d'achat" + } + }, + "campaign": { + "header": "Campaign", + "edit": { + "header": "Modifier la campagne", + "successToast": "La campagne de la promotion a été mise à jour avec succès." + }, + "actions": { + "goToCampaign": "Aller à la campagne" + } + }, + "campaign_currency": { + "tooltip": "C'est la devise de la promotion. Changez-la depuis l'onglet Détails." + }, + "form": { + "required": "Requis", + "and": "ET", + "selectAttribute": "Sélectionner un attribut", + "campaign": { + "existing": { + "title": "Campagne existante", + "description": "Ajouter la promotion à une campagne existante.", + "placeholder": { + "title": "Aucune campagne existante", + "desc": "Vous pouvez créer une pour suivre plusieurs promotions et définir des limites de budget." + } + }, + "new": { + "title": "Nouvelle campagne", + "description": "Créer une nouvelle campagne pour cette promotion." + }, + "none": { + "title": "Sans campagne", + "description": "Continuer sans associer la promotion à une campagne" + } + }, + "status": { + "title": "Status" + }, + "method": { + "label": "Method", + "code": { + "title": "Code de promotion", + "description": "Les clients doivent entrer ce code lors de la caisse" + }, + "automatic": { + "title": "Automatique", + "description": "Les clients verront cette promotion lors de la caisse" + } + }, + "max_quantity": { + "title": "Quantité maximum", + "description": "Quantité maximum d'articles à laquelle cette promotion s'applique." + }, + "type": { + "standard": { + "title": "Standard", + "description": "Une promotion standard" + }, + "buyget": { + "title": "Acheter X, obtenez Y", + "description": "Achat X, obtenez Y" + } + }, + "allocation": { + "each": { + "title": "Chaque", + "description": "Applique la valeur sur chaque article" + }, + "across": { + "title": "Sur tous les articles", + "description": "Applique la valeur sur tous les articles" + } + }, + "code": { + "title": "Code", + "description": "Le code que les clients entreront lors de la caisse." + }, + "value": { + "title": "Valeur de la promotion" + }, + "value_type": { + "fixed": { + "title": "Valeur de la promotion", + "description": "La somme à être remise. Exemple : 100" + }, + "percentage": { + "title": "Valeur de la promotion", + "description": "Le pourcentage à remiser. Exemple : 8%" + } + } + }, + "deleteWarning": "Vous êtes sur le point de supprimer la promotion {{code}}. Cette action ne peut être annulée.", + "createPromotionTitle": "Créer une promotion", + "type": "Type de promotion", + "conditions": { + "add": "Add condition", + "list": { + "noRecordsMessage": "Ajouter une condition pour restreindre les articles à laquelle la promotion s'applique." + } + } + }, + "campaigns": { + "domain": "Campagnes", + "details": "Détails de la campagne", + "status": { + "active": "Active", + "expired": "Expirée", + "scheduled": "Programmée" + }, + "delete": { + "title": "Êtes-vous sûr?", + "description": "Vous êtes sur le point de supprimer la campagne '{{name}}'. Cette action ne peut être annulée.", + "successToast": "La campagne '{{name}}' a été supprimée avec succès." + }, + "edit": { + "header": "Modifier la campagne", + "description": "Modifier les détails de la campagne.", + "successToast": "La campagne '{{name}}' a été mise à jour avec succès." + }, + "configuration": { + "header": "Configuration", + "edit": { + "header": "Modifier la configuration de la campagne", + "description": "Modifier la configuration de la campagne.", + "successToast": "La configuration de la campagne a été mise à jour avec succès." + } + }, + "create": { + "title": "Créer une campagne", + "description": "Créer une campagne promotionnelle.", + "hint": "Créer une campagne promotionnelle.", + "header": "Créer une campagne", + "successToast": "La campagne '{{name}}' a été créée avec succès." + }, + "fields": { + "name": "Nom", + "identifier": "Identifiant", + "start_date": "Date de début", + "end_date": "Date de fin", + "total_spend": "Budget dépensé", + "total_used": "Budget utilisé", + "budget_limit": "Limite de budget", + "campaign_id": { + "hint": "Seules les campagnes avec le même code de devise que la promotion sont affichées dans cette liste." + } + }, + "budget": { + "create": { + "hint": "Créer un budget pour la campagne.", + "header": "Budget de la campagne" + }, + "details": "Budget de la campagne", + "fields": { + "type": "Type", + "currency": "Devise", + "limit": "Limite", + "used": "Utilisé" + }, + "type": { + "spend": { + "title": "Dépense", + "description": "Définir une limite sur le montant total des remises de toutes les utilisations de la promotion." + }, + "usage": { + "title": "Usage", + "description": "Définir une limite sur le nombre de fois que la promotion peut être utilisée." + } + }, + "edit": { + "header": "Modifier le budget de la campagne" + } + }, + "promotions": { + "remove": { + "title": "Supprimer la promotion de la campagne", + "description": "Vous êtes sur le point de supprimer {{count}} promotion(s) de la campagne. Cette action ne peut être annulée." + }, + "alreadyAdded": "This promotion has already been added to the campaign.", + "alreadyAddedDiffCampaign": "Cette promotion a déjà été ajoutée à une autre campagne ({{name}}).", + "currencyMismatch": "La devise de la promotion et de la campagne ne correspondent pas", + "toast": { + "success": "Promotion(s) ajoutée(s) à la campagne avec succès" + }, + "add": { + "list": { + "noRecordsMessage": "Créer une promotion d'abord." + } + }, + "list": { + "noRecordsMessage": "Il n'y a pas de promotions dans la campagne." + } + }, + "deleteCampaignWarning": "Vous êtes sur le point de supprimer la campagne {{name}}. Cette action ne peut être annulée.", + "totalSpend": "<0>{{amount}} <1>{{currency}}" + }, + "priceLists": { + "domain": "Listes de prix", + "subtitle": "Créer des prix de vente ou des prix de remplacement pour des conditions spécifiques.", + "delete": { + "confirmation": "Vous êtes sur le point de supprimer la liste de prix {{title}}. Cette action ne peut être annulée.", + "successToast": "La liste de prix {{title}} a été supprimée avec succès." + }, + "create": { + "header": "Créer une liste de prix", + "subheader": "Créer une nouvelle liste de prix pour gérer les prix de vos produits.", + "tabs": { + "details": "Détails", + "products": "Produits", + "prices": "Prix" + }, + "successToast": "La liste de prix {{title}} a été créée avec succès.", + "products": { + "list": { + "noRecordsMessage": "Créer un produit d'abord." + } + } + }, + "edit": { + "header": "Modifier la liste de prix", + "successToast": "La liste de prix {{title}} a été mise à jour avec succès." + }, + "configuration": { + "header": "Configuration", + "edit": { + "header": "Modifier la configuration de la liste de prix", + "description": "Modifier la configuration de la liste de prix.", + "successToast": "La configuration de la liste de prix a été mise à jour avec succès." + } + }, + "products": { + "header": "Produits", + "actions": { + "addProducts": "Ajouter des produits", + "editPrices": "Modifier les prix" + }, + "delete": { + "confirmation_one": "Vous êtes sur le point de supprimer les prix pour {{count}} produit dans la liste de prix. Cette action ne peut être annulée.", + "confirmation_other": "Vous êtes sur le point de supprimer les prix pour {{count}} produits dans la liste de prix. Cette action ne peut être annulée.", + "successToast_one": "Les prix pour {{count}} produit ont été supprimés avec succès.", + "successToast_other": "Les prix pour {{count}} produits ont été supprimés avec succès." + }, + "add": { + "successToast": "Les prix ont été ajoutés à la liste de prix avec succès." + }, + "edit": { + "successToast": "Les prix ont été mis à jour avec succès." + } + }, + "fields": { + "priceOverrides": { + "label": "Prix de remplacement", + "header": "Prix de remplacement" + }, + "status": { + "label": "Status", + "options": { + "active": "Active", + "draft": "Brouillon", + "expired": "Expiré", + "scheduled": "Programmé" + } + }, + "type": { + "label": "Type", + "hint": "Choisir le type de liste de prix que vous souhaitez créer.", + "options": { + "sale": { + "label": "Prix de vente", + "description": "Les prix de vente sont des changements de prix temporaires pour les produits." + }, + "override": { + "label": "Remplacement", + "description": "Les remplacements sont généralement utilisés pour créer des prix spécifiques aux clients." + } + } + }, + "startsAt": { + "label": "La liste de prix a une date de début?", + "hint": "Planifier la liste de prix pour l'activation dans le futur." + }, + "endsAt": { + "label": "La liste de prix a une date d'expiration?", + "hint": "Planifier la liste de prix pour la désactivation dans le futur." + }, + "customerAvailability": { + "header": "Choisir les groupes de clients", + "label": "Disponibilité des clients", + "hint": "Choisir les groupes de clients auxquels la liste de prix doit s'appliquer.", + "placeholder": "Rechercher les groupes de clients", + "attribute": "Groupes de clients" + } + } + }, + "profile": { + "domain": "Profil", + "manageYourProfileDetails": "Gérer les détails de votre profil.", + "fields": { + "languageLabel": "Langue", + "usageInsightsLabel": "Données d'utilisation" + }, + "edit": { + "header": "Modifier le profil", + "languageHint": "La langue que vous souhaitez utiliser dans le tableau de bord administratif. Cela ne change pas la langue de votre boutique.", + "languagePlaceholder": "Sélectionner la langue", + "usageInsightsHint": "Partager les données d'utilisation et nous aider à améliorer Medusa. Vous pouvez en savoir plus sur ce que nous collectons et comment nous l'utilisons dans notre <0>documentation." + }, + "toast": { + "edit": "Modifications du profil enregistrées" + } + }, + "users": { + "domain": "Utilisateurs", + "editUser": "Modifier l'utilisateur", + "inviteUser": "Inviter un utilisateur", + "inviteUserHint": "Inviter un nouvel utilisateur à votre boutique.", + "sendInvite": "Envoyer l'invitation", + "pendingInvites": "Invitations en attente", + "deleteInviteWarning": "Vous êtes sur le point de supprimer l'invitation pour {{email}}. Cette action ne peut être annulée.", + "resendInvite": "Renvoyer l'invitation", + "copyInviteLink": "Copier le lien d'invitation", + "expiredOnDate": "Expiré le {{date}}", + "validFromUntil": "Valide de <0>{{from}} - <1>{{until}}", + "acceptedOnDate": "Accepté le {{date}}", + "inviteStatus": { + "accepted": "Accepté", + "pending": "En attente", + "expired": "Expiré" + }, + "roles": { + "admin": "Admin", + "developer": "Développeur", + "member": "Membre" + }, + "deleteUserWarning": "Vous êtes sur le point de supprimer l'utilisateur {{name}}. Cette action ne peut être annulée.", + "invite": "Inviter" + }, + "store": { + "domain": "Boutique", + "manageYourStoresDetails": "Gérer les détails de votre boutique.", + "editStore": "Modifier la boutique", + "defaultCurrency": "Devise par défaut", + "defaultRegion": "Région par défaut", + "swapLinkTemplate": "Lien de remplacement", + "paymentLinkTemplate": "Lien de paiement", + "inviteLinkTemplate": "Lien d'invitation", + "currencies": "Devises", + "addCurrencies": "Ajouter des devises", + "enableTaxInclusivePricing": "Activer le prix taxe inclusive", + "disableTaxInclusivePricing": "Désactiver le prix taxe inclusive", + "removeCurrencyWarning_one": "Vous êtes sur le point de supprimer {{count}} devise de votre boutique. Assurez-vous de supprimer tous les prix utilisant la devise avant de continuer.", + "removeCurrencyWarning_other": "Vous êtes sur le point de supprimer {{count}} devises de votre boutique. Assurez-vous de supprimer tous les prix utilisant les devises avant de continuer.", + "currencyAlreadyAdded": "La devise a déjà été ajoutée à votre boutique.", + "edit": { + "header": "Modifier la boutique" + }, + "toast": { + "update": "Boutique mise à jour avec succès", + "currenciesUpdated": "Devises mises à jour avec succès", + "currenciesRemoved": "Devises supprimées de la boutique avec succès", + "updatedTaxInclusivitySuccessfully": "Prix taxe inclusive mis à jour avec succès" + } + }, + "regions": { + "domain": "Régions", + "subtitle": "Une région est une zone dans laquelle vous vendez des produits. Elle peut couvrir plusieurs pays, et a des taux de taxe, fournisseurs et devises différents.", + "createRegion": "Créer une région", + "createRegionHint": "Gérer les taux de taxe et les fournisseurs pour un ensemble de pays.", + "addCountries": "Ajouter des pays", + "editRegion": "Modifier la région", + "countriesHint": "Ajouter les pays inclus dans cette région.", + "deleteRegionWarning": "Vous êtes sur le point de supprimer la région {{name}}. Cette action ne peut être annulée.", + "removeCountriesWarning_one": "Vous êtes sur le point de supprimer {{count}} pays de la région. Cette action ne peut être annulée.", + "removeCountriesWarning_other": "Vous êtes sur le point de supprimer {{count}} pays de la région. Cette action ne peut être annulée.", + "removeCountryWarning": "Vous êtes sur le point de supprimer le pays {{name}} de la région. Cette action ne peut être annulée.", + "automaticTaxesHint": "Lorsqu'elle est activée, les taxes ne sont calculées qu'à la caisse en fonction de l'adresse de livraison.", + "taxInclusiveHint": "Lorsqu'elle est activée, les prix dans la région sont taxe inclusive.", + "providersHint": "Ajouter les fournisseurs de paiement disponibles dans cette région.", + "shippingOptions": "Options de livraison", + "deleteShippingOptionWarning": "Vous êtes sur le point de supprimer l'option de livraison {{name}}. Cette action ne peut être annulée.", + "return": "Retour", + "outbound": "Sortant", + "priceType": "Type de prix", + "flatRate": "Tarif fixe", + "calculated": "Calculé", + "list": { + "noRecordsMessage": "Créer une région pour les zones dans lesquelles vous vendez." + }, + "toast": { + "delete": "Région supprimée avec succès", + "edit": "Région modifiée avec succès", + "create": "Région créée avec succès", + "countries": "Pays de la région mis à jour avec succès" + }, + "shippingOption": { + "createShippingOption": "Créer une option de livraison", + "createShippingOptionHint": "Créer une nouvelle option de livraison pour la région.", + "editShippingOption": "Modifier l'option de livraison", + "fulfillmentMethod": "Méthode de livraison", + "type": { + "outbound": "Outbound", + "outboundHint": "Utiliser cette option si vous créez une option de livraison pour envoyer des produits au client.", + "return": "Retour", + "returnHint": "Utiliser cette option si vous créez une option de livraison pour que le client retourne des produits à vous." + }, + "priceType": { + "label": "Type de prix", + "flatRate": "Tarif fixe", + "calculated": "Calculé" + }, + "availability": { + "adminOnly": "Admin uniquement", + "adminOnlyHint": "Lorsqu'elle est activée, l'option de livraison ne sera disponible que dans le tableau de bord administratif et pas dans la boutique." + }, + "taxInclusiveHint": "Lorsqu'elle est activée, le prix de l'option de livraison sera taxe inclusive.", + "requirements": { + "label": "Conditions", + "hint": "Spécifier les conditions pour l'option de livraison." + } + } + }, + "taxes": { + "domain": "Régions fiscales", + "domainDescription": "Gérer votre région fiscale", + "countries": { + "taxCountriesHint": "Les paramètres fiscaux s'appliquent aux pays listés." + }, + "settings": { + "editTaxSettings": "Modifier les paramètres fiscaux", + "taxProviderLabel": "Fournisseur", + "systemTaxProviderLabel": "Fournisseur de taxes système", + "calculateTaxesAutomaticallyLabel": "Calculer les taxes automatiquement", + "calculateTaxesAutomaticallyHint": "Lorsqu'elle est activée, les taux de taxes seront calculés automatiquement et appliqués aux paniers. Lorsqu'elle est désactivée, les taxes doivent être calculées manuellement à la caisse. Les taxes manuelles sont recommandées pour l'utilisation avec des fournisseurs de taxes tiers.", + "applyTaxesOnGiftCardsLabel": "Appliquer les taxes sur les cartes-cad", + "applyTaxesOnGiftCardsHint": "Lorsqu'elle est activée, les taxes seront appliquées aux cartes-cad à la caisse. Dans certains pays, les régulations fiscales requièrent l'application de taxes aux cartes-cad lors de l'achat.", + "defaultTaxRateLabel": "Taux de taxe par défaut", + "defaultTaxCodeLabel": "Code de taxe par défaut" + }, + "defaultRate": { + "sectionTitle": "Taux de taxe par défaut" + }, + "taxRate": { + "sectionTitle": "Taux de taxe", + "createTaxRate": "Créer un taux de taxe", + "createTaxRateHint": "Créer un nouveau taux de taxe pour la région.", + "deleteRateDescription": "Vous êtes sur le point de supprimer le taux de taxe {{name}}. Cette action ne peut être annulée.", + "editTaxRate": "Modifier le taux de taxe", + "editRateAction": "Modifier le taux", + "editOverridesAction": "Modifier les remplacements", + "editOverridesTitle": "Modifier les remplacements", + "editOverridesHint": "Spécifier les remplacements pour le taux de taxe.", + "deleteTaxRateWarning": "Vous êtes sur le point de supprimer le taux de taxe {{name}}. Cette action ne peut être annulée.", + "productOverridesLabel": "Remplacements de produits", + "productOverridesHint": "Spécifier les remplacements de produits pour le taux de taxe.", + "addProductOverridesAction": "Ajouter des remplacements de produits", + "productTypeOverridesLabel": "Remplacements de types de produits", + "productTypeOverridesHint": "Spécifier les remplacements de types de produits pour le taux de taxe.", + "addProductTypeOverridesAction": "Ajouter des remplacements de types de produits", + "shippingOptionOverridesLabel": "Remplacements d'options de livraison", + "shippingOptionOverridesHint": "Spécifier les remplacements d'options de livraison pour le taux de taxe.", + "addShippingOptionOverridesAction": "Ajouter des remplacements d'options de livraison", + "productOverridesHeader": "Produits", + "productTypeOverridesHeader": "Types de produits", + "shippingOptionOverridesHeader": "Options de livraison" + } + }, + "locations": { + "domain": "Localisations", + "editLocation": "Modifier la localisation", + "addSalesChannels": "Ajouter des canaux de vente", + "noLocationsFound": "Aucune localisation trouvée", + "selectLocations": "Sélectionner les localisations qui stockent l'article.", + "deleteLocationWarning": "Vous êtes sur le point de supprimer la localisation {{name}}. Cette action ne peut être annulée.", + "removeSalesChannelsWarning_one": "Vous êtes sur le point de supprimer {{count}} canal de vente de la localisation.", + "removeSalesChannelsWarning_other": "Vous êtes sur le point de supprimer {{count}} canaux de vente de la localisation.", + "toast": { + "create": "Localisation créée avec succès", + "update": "Localisation mise à jour avec succès", + "removeChannel": "Canal de vente supprimé avec succès" + } + }, + "reservations": { + "domain": "Réservations", + "subtitle": "Gérer la quantité réservée des articles en stock.", + "deleteWarning": "Vous êtes sur le point de supprimer une réservation. Cette action ne peut être annulée." + }, + "salesChannels": { + "domain": "Canaux de vente", + "subtitle": "Gérer les canaux de vente en ligne et hors ligne.", + "createSalesChannel": "Créer un canal de vente", + "createSalesChannelHint": "Créer un nouveau canal de vente pour vendre vos produits.", + "enabledHint": "Spécifier si le canal de vente est activé.", + "removeProductsWarning_one": "Vous êtes sur le point de supprimer {{count}} produit de {{sales_channel}}.", + "removeProductsWarning_other": "Vous êtes sur le point de supprimer {{count}} produits de {{sales_channel}}.", + "addProducts": "Ajouter des produits", + "editSalesChannel": "Modifier le canal de vente", + "productAlreadyAdded": "Le produit a déjà été ajouté au canal de vente.", + "deleteSalesChannelWarning": "Vous êtes sur le point de supprimer le canal de vente {{name}}. Cette action ne peut être annulée.", + "toast": { + "create": "Canal de vente créé avec succès", + "update": "Canal de vente mis à jour avec succès", + "delete": "Canal de vente supprimé avec succès" + }, + "products": { + "list": { + "noRecordsMessage": "Il n'y a aucun produit dans le canal de vente." + }, + "add": { + "list": { + "noRecordsMessage": "Créer un produit d'abord." + } + } + } + }, + "apiKeyManagement": { + "domain": { + "publishable": "Publishable API Keys", + "secret": "Secret API Keys" + }, + "subtitle": { + "publishable": "Gérer les clés API utilisées dans le storefront pour limiter la portée des requêtes aux canaux de vente spécifiques.", + "secret": "Gérer les clés API utilisées pour authentifier les utilisateurs administratifs dans les applications administratives." + }, + "status": { + "active": "Active", + "revoked": "Revoqué" + }, + "type": { + "publishable": "Publique", + "secret": "Secrète" + }, + "create": { + "createPublishableHeader": "Créer une clé API publique", + "createPublishableHint": "Créer une nouvelle clé API publique pour limiter la portée des requêtes aux canaux de vente spécifiques.", + "createSecretHeader": "Créer une clé API secrète", + "createSecretHint": "Créer une nouvelle clé API secrète pour accéder à l'API Medusa en tant qu'utilisateur administratif authentifié.", + "secretKeyCreatedHeader": "Clé secrète créée", + "secretKeyCreatedHint": "Votre nouvelle clé secrète a été générée. Copiez et stockez-la maintenant. C'est la seule fois où elle sera affichée.", + "copySecretTokenSuccess": "Clé secrète copiée dans le presse-papiers.", + "copySecretTokenFailure": "Erreur lors de la copie de la clé secrète dans le presse-papiers.", + "successToast": "Clé API créée avec succès." + }, + "edit": { + "header": "Modifier la clé API", + "description": "Modifier le titre de la clé API.", + "successToast": "Clé API {{title}} mise à jour avec succès." + }, + "salesChannels": { + "title": "Ajouter des canaux de vente", + "description": "Ajouter les canaux de vente auxquels la clé API doit être limitée.", + "successToast_one": "{{count}} canal de vente a été ajouté à la clé API.", + "successToast_other": "{{count}} canaux de vente ont été ajoutés à la clé API.", + "alreadyAddedTooltip": "Le canal de vente a déjà été ajouté à la clé API.", + "list": { + "noRecordsMessage": "Il n'y a aucun canal de vente dans la portée de la clé API publique." + } + }, + "delete": { + "warning": "Vous êtes sur le point de supprimer la clé API {{title}}. Cette action ne peut être annulée.", + "successToast": "Clé API {{title}} supprimée avec succès." + }, + "revoke": { + "warning": "Vous êtes sur le point de révoquer la clé API {{title}}. Cette action ne peut être annulée.", + "successToast": "Clé API {{title}} révoquée avec succès." + }, + "addSalesChannels": { + "list": { + "noRecordsMessage": "Créer un canal de vente d'abord." + } + }, + "removeSalesChannel": { + "warning": "Vous êtes sur le point de supprimer le canal de vente {{name}} de la clé API. Cette action ne peut être annulée.", + "warningBatch_one": "Vous êtes sur le point de supprimer {{count}} canal de vente de la clé API. Cette action ne peut être annulée.", + "warningBatch_other": "Vous êtes sur le point de supprimer {{count}} canaux de vente de la clé API. Cette action ne peut être annulée.", + "successToast": "Canal de vente supprimé avec succès de la clé API.", + "successToastBatch_one": "{{count}} canal de vente a été supprimé de la clé API.", + "successToastBatch_other": "{{count}} canaux de vente ont été supprimés de la clé API." + }, + "actions": { + "revoke": "Révoquer la clé API", + "copy": "Copier la clé API", + "copySuccessToast": "Clé API copiée dans le presse-papiers." + }, + "table": { + "lastUsedAtHeader": "Dernière utilisation", + "createdAtHeader": "Annuler à" + }, + "fields": { + "lastUsedAtLabel": "Dernière utilisation", + "revokedByLabel": "Annuler par", + "revokedAtLabel": "Annuler à", + "createdByLabel": "Créée par" + } + }, + "returnReasons": { + "domain": "Raison de retour", + "subtitle": "Gérer les raisons des retours.", + "calloutHint": "Gérer les raisons pour classer les retours.", + "editReason": "Modifier la raison de retour", + "create": { + "header": "Ajouter une raison de retour", + "subtitle": "Spécifier les raisons les plus courantes des retours.", + "hint": "Créer une nouvelle raison de retour pour classer les retours.", + "successToast": "Raison de retour {{label}} créée avec succès." + }, + "edit": { + "header": "Modifier la raison de retour", + "subtitle": "Modifier la valeur de la raison de retour.", + "successToast": "Raison de retour {{label}} mise à jour avec succès." + }, + "delete": { + "confirmation": "Vous êtes sur le point de supprimer la raison de retour {{label}}. Cette action ne peut être annulée.", + "successToast": "Raison de retour {{label}} supprimée avec succès." + }, + "fields": { + "value": { + "label": "Valeur", + "placeholder": "mauvaise_taille", + "tooltip": "La valeur doit être un identifiant unique pour la raison de retour." + }, + "label": { "label": "Label", "placeholder": "Mauvaise taille" }, + "description": { + "label": "Description", + "placeholder": "Le client a reçu la mauvaise taille" + } + } + }, + "login": { + "forgotPassword": "Mot de passe oublié? - <0>Réinitialiser", + "title": "Bienvenue sur Medusa", + "hint": "Connectez-vous pour accéder à l'espace administratif" + }, + "invite": { + "title": "Bienvenue sur Medusa", + "hint": "Créez votre compte ci-dessous", + "backToLogin": "Retour à la connexion", + "createAccount": "Créer un compte", + "alreadyHaveAccount": "Vous avez déjà un compte? - <0>Se connecter", + "emailTooltip": "Votre email ne peut être modifié. Si vous souhaitez utiliser un autre email, un nouveau lien d'invitation doit être envoyé.", + "invalidInvite": "Le lien d'invitation est invalide ou a expiré.", + "successTitle": "Votre compte a été enregistré", + "successHint": "Commencez avec Medusa Admin dès maintenant.", + "successAction": "Commencer Medusa Admin", + "invalidTokenTitle": "Votre lien d'invitation est invalide", + "invalidTokenHint": "Essayez de demander un nouveau lien d'invitation.", + "passwordMismatch": "Les mots de passe ne correspondent pas", + "toast": { + "accepted": "Invitation acceptée avec succès" + } + }, + "resetPassword": { + "title": "Réinitialiser le mot de passe", + "hint": "Entrez votre email ci-dessous, et nous vous enverrons des instructions sur la façon de réinitialiser votre mot de passe.", + "email": "Email", + "sendResetInstructions": "Envoyer les instructions de réinitialisation", + "backToLogin": "<0>Retour à la connexion", + "newPasswordHint": "Choisissez un nouveau mot de passe ci-dessous.", + "invalidTokenTitle": "Votre lien de réinitialisation est invalide", + "invalidTokenHint": "Essayez de demander un nouveau lien de réinitialisation.", + "expiredTokenTitle": "Votre lien de réinitialisation a expiré", + "goToResetPassword": "Aller à la réinitialisation du mot de passe", + "resetPassword": "Réinitialiser le mot de passe", + "newPassword": "Nouveau mot de passe", + "repeatNewPassword": "Répéter le nouveau mot de passe", + "tokenExpiresIn": "Le lien expire dans <0>{{time}} minutes", + "successfulRequestTitle": "Un email vous a été envoyé", + "successfulRequest": "Nous vous avons envoyé un email que vous pouvez utiliser pour réinitialiser votre mot de passe. Vérifiez votre dossier de spam si vous n'avez pas reçu l'email après quelques minutes.", + "successfulResetTitle": "Réinitialisation du mot de passe réussie", + "successfulReset": "Veuillez vous connecter sur la page de connexion.", + "passwordMismatch": "Les mots de passe ne correspondent pas", + "invalidLinkTitle": "Votre lien de réinitialisation est invalide", + "invalidLinkHint": "Essayez de réinitialiser votre mot de passe à nouveau." + }, + "workflowExecutions": { + "domain": "Workflows", + "subtitle": "Voir et suivre les exécutions des workflows dans votre application Medusa.", + "transactionIdLabel": "ID de la transaction", + "workflowIdLabel": "ID du workflow", + "progressLabel": "Progression", + "stepsCompletedLabel_one": "{{completed}} de {{count}} étape", + "stepsCompletedLabel_other": "{{completed}} de {{count}} étapes", + "list": { + "noRecordsMessage": "Aucun workflow n'a été exécuté, pour le moment." + }, + "history": { + "sectionTitle": "Historique", + "runningState": "En cours...", + "awaitingState": "En attente", + "failedState": "Échec", + "skippedState": "Ignoré", + "skippedFailureState": "Ignoré (Échec)", + "definitionLabel": "Définition", + "outputLabel": "Sortie", + "compensateInputLabel": "Entrée de compensation", + "revertedLabel": "Inversé", + "errorLabel": "Erreur" + }, + "state": { + "done": "Terminé", + "failed": "Échec", + "reverted": "Inversé", + "invoking": "Déclencher", + "compensating": "Compenser", + "notStarted": "Non démarré" + }, + "transaction": { + "state": { + "waitingToCompensate": "En attente de compensation" + } + }, + "step": { + "state": { + "skipped": "Ignoré", + "skippedFailure": "Ignoré (Échec)", + "dormant": "Dormant", + "timeout": "Timeout" + } + } + }, + "productTypes": { + "domain": "Types de produits", + "subtitle": "Organiser vos produits en types.", + "create": { + "header": "Créer un type de produit", + "hint": "Créer un nouveau type de produit pour classer vos produits.", + "successToast": "Type de produit {{value}} créé avec succès." + }, + "edit": { + "header": "Modifier le type de produit", + "successToast": "Type de produit {{value}} mis à jour avec succès." + }, + "delete": { + "confirmation": "Vous êtes sur le point de supprimer le type de produit {{value}}. Cette action ne peut être annulée.", + "successToast": "Type de produit {{value}} supprimé avec succès." + }, + "fields": { + "value": "Valeur" + } + }, + "productTags": { + "domain": "Tags de produits", + "create": { + "header": "Créer un tag de produit", + "subtitle": "Créer un nouveau tag de produit pour classer vos produits.", + "successToast": "Tag de produit {{value}} créé avec succès." + }, + "edit": { + "header": "Modifier le tag de produit", + "subtitle": "Modifier la valeur du tag de produit.", + "successToast": "Tag de produit {{value}} mis à jour avec succès." + }, + "delete": { + "confirmation": "Vous êtes sur le point de supprimer le tag de produit {{value}}. Cette action ne peut être annulée.", + "successToast": "Tag de produit {{value}} supprimé avec succès." + }, + "fields": { + "value": "Valeur" + } + }, + "notifications": { + "domain": "Notifications", + "emptyState": { + "title": "Aucune notification", + "description": "Vous n'avez aucune notification pour le moment, mais elles apparaîtront ici une fois que vous les aurez." + }, + "accessibility": { + "description": "Les notifications concernant les activités Medusa seront listées ici." + } + }, + "errors": { + "serverError": "Erreur serveur - Réessayez plus tard.", + "invalidCredentials": "Mauvais email ou mot de passe" + }, + "statuses": { + "scheduled": "Programmé", + "expired": "Expiré", + "active": "Actif", + "enabled": "Activé", + "disabled": "Désactivé" + }, + "labels": { + "productVariant": "Variante de produit", + "prices": "Prix", + "available": "Disponible", + "inStock": "En stock", + "added": "Ajouté", + "removed": "Supprimé" + }, + "fields": { + "amount": "Montant", + "refundAmount": "Montant de remboursement", + "name": "Nom", + "default": "Par défaut", + "lastName": "Nom de famille", + "firstName": "Prénom", + "title": "Titre", + "customTitle": "Titre personnalisé", + "manageInventory": "Gérer l'inventaire", + "inventoryKit": "Kit d'inventaire", + "inventoryItems": "Articles en inventaire", + "inventoryItem": "Article en inventaire", + "requiredQuantity": "Quantité requise", + "description": "Description", + "email": "Email", + "password": "Mot de passe", + "repeatPassword": "Répéter le mot de passe", + "confirmPassword": "Confirmer le mot de passe", + "newPassword": "Nouveau mot de passe", + "repeatNewPassword": "Répéter le nouveau mot de passe", + "categories": "Catégories", + "shippingMethod": "Méthode de livraison", + "configurations": "Configurations", + "conditions": "Conditions", + "category": "Catégorie", + "collection": "Collection", + "discountable": "Remise", + "handle": "Identifiant", + "subtitle": "Sous-titre", + "item": "Article", + "qty": "Qté", + "limit": "Limite", + "tags": "Tags", + "type": "Type", + "reason": "Raison", + "none": "Aucun", + "all": "Tous", + "search": "Rechercher", + "percentage": "Pourcentage", + "sales_channels": "Canaux de vente", + "customer_groups": "Groupes de clients", + "product_tags": "Tags de produits", + "product_types": "Types de produits", + "product_collections": "Collections de produits", + "status": "Statut", + "code": "Code", + "value": "Valeur", + "disabled": "Désactivé", + "dynamic": "Dynamique", + "normal": "Normal", + "years": "Années", + "months": "Mois", + "days": "Jours", + "hours": "Heures", + "minutes": "Minutes", + "totalRedemptions": "Total d'utilisations", + "countries": "Pays", + "paymentProviders": "Fournisseurs de paiement", + "refundReason": "Raison de remboursement", + "fulfillmentProviders": "Fournisseurs de livraison", + "fulfillmentProvider": "Fournisseur de livraison", + "providers": "Fournisseurs", + "availability": "Disponibilité", + "inventory": "Inventaire", + "optional": "Optionnel", + "note": "Note", + "automaticTaxes": "Taxes automatiques", + "taxInclusivePricing": "Prix incluant les taxes", + "currency": "Monnaie", + "address": "Adresse", + "address2": "Appartement, suite, etc.", + "city": "Ville", + "postalCode": "Code postal", + "country": "Pays", + "state": "État", + "province": "Province", + "company": "Entreprise", + "phone": "Téléphone", + "metadata": "Métadonnées", + "selectCountry": "Sélectionner un pays", + "products": "Produits", + "variants": "Variantes", + "orders": "Commandes", + "account": "Compte", + "total": "Total de la commande", + "paidTotal": "Total capturé", + "totalExclTax": "Total hors taxes", + "subtotal": "Sous-total", + "shipping": "Livraison", + "outboundShipping": "Outbound Shipping", + "returnShipping": "Return Shipping", + "tax": "Taxe", + "created": "Créé", + "key": "Clé", + "customer": "Client", + "date": "Date", + "order": "Commande", + "fulfillment": "Livraison", + "provider": "Fournisseur", + "payment": "Paiement", + "items": "Articles", + "salesChannel": "Canal de vente", + "region": "Région", + "discount": "Remise", + "role": "Rôle", + "sent": "Envoyé", + "salesChannels": "Canaux de vente", + "product": "Produit", + "createdAt": "Créé", + "updatedAt": "Mis à jour", + "revokedAt": "Annulé à", + "true": "Vrai", + "false": "Faux", + "giftCard": "Carte cadeau", + "tag": "Tag", + "dateIssued": "Date d'émission", + "issuedDate": "Date d'émission", + "expiryDate": "Date d'expiration", + "price": "Prix", + "priceTemplate": "Prix {{regionOrCurrency}}", + "height": "Hauteur", + "width": "Largeur", + "length": "Longueur", + "weight": "Poids", + "midCode": "MID code", + "hsCode": "HS code", + "ean": "EAN", + "upc": "UPC", + "inventoryQuantity": "Quantité en inventaire", + "barcode": "Code barre", + "countryOfOrigin": "Pays d'origine", + "material": "Matière", + "thumbnail": "Miniature", + "sku": "SKU", + "managedInventory": "Inventaire géré", + "allowBackorder": "Autoriser les commandes en rupture de stock", + "inStock": "En stock", + "location": "Emplacement", + "quantity": "Quantité", + "variant": "Variante", + "id": "ID", + "parent": "Parent", + "minSubtotal": "Min. Sous-total", + "maxSubtotal": "Max. Sous-total", + "shippingProfile": "Profil de livraison", + "summary": "Résumé", + "details": "Détails", + "label": "Label", + "rate": "Taux", + "requiresShipping": "Requiert une livraison", + "unitPrice": "Prix unitaire", + "startDate": "Date de début", + "endDate": "Date de fin", + "draft": "Brouillon", + "values": "Valeurs" + }, + "dateTime": { + "years_one": "Année", + "years_other": "Années", + "months_one": "Mois", + "months_other": "Mois", + "weeks_one": "Semaine", + "weeks_other": "Semaines", + "days_one": "Jour", + "days_other": "Jours", + "hours_one": "Heure", + "hours_other": "Heures", + "minutes_one": "Minute", + "minutes_other": "Minutes", + "seconds_one": "Seconde", + "seconds_other": "Secondes" + } } diff --git a/packages/admin/dashboard/src/i18n/translations/it.json b/packages/admin/dashboard/src/i18n/translations/it.json index 14a362cbe6759..24e2dad544992 100644 --- a/packages/admin/dashboard/src/i18n/translations/it.json +++ b/packages/admin/dashboard/src/i18n/translations/it.json @@ -317,6 +317,12 @@ "greaterThanLabel": "maggiore di {{value}}", "andLabel": "e" }, + "radio": { + "yes": "Sì", + "no": "No", + "true": "Vero", + "false": "Falso" + }, "addFilter": "Aggiungi filtro" }, "errorBoundary": { @@ -468,7 +474,17 @@ } }, "deleteWarning": "Stai per eliminare il prodotto {{title}}. Questa azione non può essere annullata.", - "variants": "Varianti", + "variants": { + "header": "Varianti", + "empty": { + "heading": "Nessuna variante", + "description": "Non ci sono varianti da visualizzare." + }, + "filtered": { + "heading": "Nessun risultato", + "description": "Nessuna variante corrisponde ai criteri di filtro correnti." + } + }, "attributes": "Attributi", "editAttributes": "Modifica Attributi", "editOptions": "Modifica Opzioni", @@ -2092,14 +2108,14 @@ "label": "La lista prezzi ha una data di scadenza?", "hint": "Pianifica la lista prezzi per disattivarsi in futuro." }, - "customerAvailability": { - "header": "Scegli gruppi di clienti", - "label": "Disponibilità cliente", - "hint": "Scegli quali gruppi di clienti la lista prezzi dovrebbe essere applicata.", - "placeholder": "Cerca gruppi di clienti", - "attribute": "Gruppi di clienti" - } + "customerAvailability": { + "header": "Scegli gruppi di clienti", + "label": "Disponibilità cliente", + "hint": "Scegli quali gruppi di clienti la lista prezzi dovrebbe essere applicata.", + "placeholder": "Cerca gruppi di clienti", + "attribute": "Gruppi di clienti" } + } }, "profile": { "domain": "Profilo", @@ -2771,4 +2787,4 @@ "seconds_one": "Secondo", "seconds_other": "Secondi" } -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/i18n/translations/ja.json b/packages/admin/dashboard/src/i18n/translations/ja.json index b2c3d13f8de80..5b97b6dc27f99 100644 --- a/packages/admin/dashboard/src/i18n/translations/ja.json +++ b/packages/admin/dashboard/src/i18n/translations/ja.json @@ -317,6 +317,12 @@ "greaterThanLabel": "{{value}}以上", "andLabel": "かつ" }, + "radio": { + "yes": "はい", + "no": "いいえ", + "true": "真", + "false": "偽" + }, "addFilter": "フィルター追加" }, "errorBoundary": { @@ -468,7 +474,17 @@ } }, "deleteWarning": "商品「{{title}}」を削除しようとしています。この操作は元に戻せません。", - "variants": "バリエーション", + "variants": { + "header": "バリエーション", + "empty": { + "heading": "バリエーションはありません", + "description": "表示するバリエーションはありません。" + }, + "filtered": { + "heading": "結果はありません", + "description": "現在のフィルター条件に一致するバリエーションはありません。" + } + }, "attributes": "属性", "editAttributes": "属性を編集", "editOptions": "オプションを編集", diff --git a/packages/admin/dashboard/src/i18n/translations/pl.json b/packages/admin/dashboard/src/i18n/translations/pl.json index fc11e01e23fa7..709771b6da02e 100644 --- a/packages/admin/dashboard/src/i18n/translations/pl.json +++ b/packages/admin/dashboard/src/i18n/translations/pl.json @@ -316,6 +316,12 @@ "greaterThanLabel": "więcej niż {{value}}", "andLabel": "i" }, + "radio": { + "yes": "Tak", + "no": "Nie", + "true": "Prawda", + "false": "Fałsz" + }, "addFilter": "Dodaj filtr" }, "errorBoundary": { @@ -467,7 +473,17 @@ } }, "deleteWarning": "Zamierzasz usunąć produkt {{title}}. Ta akcja nie może zostać cofnięta.", - "variants": "Warianty", + "variants": { + "header": "Warianty", + "empty": { + "heading": "Brak wariantów", + "description": "Nie ma wariantów do wyświetlenia." + }, + "filtered": { + "heading": "Brak wariantów", + "description": "Nie ma wariantów, które pasują do aktualnych kryteriów filtrów." + } + }, "attributes": "Atrybuty", "editAttributes": "Edytuj atrybuty", "editOptions": "Edytuj opcje", diff --git a/packages/admin/dashboard/src/i18n/translations/ptBR.json b/packages/admin/dashboard/src/i18n/translations/ptBR.json index a545b60ba44ea..ad3460fe09f5f 100644 --- a/packages/admin/dashboard/src/i18n/translations/ptBR.json +++ b/packages/admin/dashboard/src/i18n/translations/ptBR.json @@ -316,6 +316,12 @@ "greaterThanLabel": "maior que {{value}}", "andLabel": "e" }, + "radio": { + "yes": "Sim", + "no": "Não", + "true": "Verdadeiro", + "false": "Falso" + }, "addFilter": "Adicionar filtro" }, "errorBoundary": { @@ -467,7 +473,17 @@ } }, "deleteWarning": "Você está prestes a excluir o produto {{title}}. Esta ação não pode ser desfeita.", - "variants": "Variantes", + "variants": { + "header": "Variantes", + "empty": { + "heading": "Nenhuma variante", + "description": "Não há variantes para exibir." + }, + "filtered": { + "heading": "Nenhuma variante", + "description": "Nenhuma variante corresponde aos critérios de filtro atuais." + } + }, "attributes": "Atributos", "editAttributes": "Editar atributos", "editOptions": "Editar opções", diff --git a/packages/admin/dashboard/src/i18n/translations/th.json b/packages/admin/dashboard/src/i18n/translations/th.json index 8aa466ff94827..7fa67aa93f30f 100644 --- a/packages/admin/dashboard/src/i18n/translations/th.json +++ b/packages/admin/dashboard/src/i18n/translations/th.json @@ -316,6 +316,12 @@ "greaterThanLabel": "มากกว่า {{value}}", "andLabel": "และ" }, + "radio": { + "yes": "ใช่", + "no": "ไม่", + "true": "จริง", + "false": "เท็จ" + }, "addFilter": "เพิ่มตัวกรอง" }, "errorBoundary": { @@ -467,7 +473,17 @@ } }, "deleteWarning": "คุณกำลังจะลบสินค้า {{title}} การดำเนินการนี้ไม่สามารถยกเลิกได้", - "variants": "ตัวเลือก", + "variants": { + "header": "ตัวเลือก", + "empty": { + "heading": "ไม่มีตัวเลือก", + "description": "ไม่มีตัวเลือกที่จะแสดง" + }, + "filtered": { + "heading": "ไม่มีผลลัพธ์", + "description": "ไม่มีตัวเลือกที่ตรงกับเกณฑ์การกรองปัจจุบัน" + } + }, "attributes": "แอตทริบิวต์", "editAttributes": "แก้ไขแอตทริบิวต์", "editOptions": "แก้ไขตัวเลือก", diff --git a/packages/admin/dashboard/src/i18n/translations/tr.json b/packages/admin/dashboard/src/i18n/translations/tr.json index 3dea4ecb02c99..5d81efc021037 100644 --- a/packages/admin/dashboard/src/i18n/translations/tr.json +++ b/packages/admin/dashboard/src/i18n/translations/tr.json @@ -316,6 +316,12 @@ "greaterThanLabel": "{{value}}'den büyük", "andLabel": "ve" }, + "radio": { + "yes": "Evet", + "no": "Hayır", + "true": "Doğru", + "false": "Yanlış" + }, "addFilter": "Filtre Ekle" }, "errorBoundary": { @@ -467,7 +473,17 @@ } }, "deleteWarning": "Ürün {{title}}'i silmek üzeresiniz. Bu işlem geri alınamaz.", - "variants": "Varyantlar", + "variants": { + "header": "Varyantlar", + "empty": { + "heading": "Varyant yok", + "description": "Görüntülenecek varyant yok." + }, + "filtered": { + "heading": "Sonuç yok", + "description": "Varyantlar mevcut filtrelerle eşleşmiyor." + } + }, "attributes": "Öznitelikler", "editAttributes": "Öznitelikleri Düzenle", "editOptions": "Seçenekleri Düzenle", diff --git a/packages/admin/dashboard/src/i18n/translations/uk.json b/packages/admin/dashboard/src/i18n/translations/uk.json index f1f3e4e4b3279..3dd4912291492 100644 --- a/packages/admin/dashboard/src/i18n/translations/uk.json +++ b/packages/admin/dashboard/src/i18n/translations/uk.json @@ -317,6 +317,12 @@ "greaterThanLabel": "більше ніж {{value}}", "andLabel": "і" }, + "radio": { + "yes": "Так", + "no": "немає", + "true": "правда", + "false": "помилковий" + }, "addFilter": "Додати фільтр" }, "errorBoundary": { @@ -468,7 +474,17 @@ } }, "deleteWarning": "Ви збираєтеся видалити продукт {{title}}. Цю дію не можна скасувати.", - "variants": "Варіанти", + "variants": { + "header": "Варіанти", + "empty": { + "heading": "Немає варіантів", + "description": "Немає варіантів для відображення." + }, + "filtered": { + "heading": "Немає результатів", + "description": "Немає варіантів, які відповідають поточному критерію фільтрації." + } + }, "attributes": "Атрибути", "editAttributes": "Редагувати атрибути", "editOptions": "Редагувати опції", diff --git a/packages/admin/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx b/packages/admin/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx index d7b958811ad22..cda422b2ba411 100644 --- a/packages/admin/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx +++ b/packages/admin/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx @@ -6,7 +6,7 @@ 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 { _DataTable } from "../../../../../components/table/data-table" import { useBatchRemoveSalesChannelsFromApiKey } from "../../../../../hooks/api/api-keys" import { useSalesChannels } from "../../../../../hooks/api/sales-channels" import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns" @@ -109,7 +109,7 @@ export const ApiKeySalesChannelSection = ({ ]} /> - - - - - { - - { - - - - { - - - { const { t } = useTranslation() + const { getWidgets } = useDashboardExtension() - const { searchParams, raw } = useCustomerGroupTableQuery({ - pageSize: PAGE_SIZE, - }) - const { customer_groups, count, isLoading, isError, error } = - useCustomerGroups({ - ...searchParams, - fields: "id,name,customers.id", - }) + const { q, order, offset, created_at, updated_at } = useQueryParams([ + "q", + "order", + "offset", + "created_at", + "updated_at", + ]) - const filters = useCustomerGroupTableFilters() const columns = useColumns() - - const { table } = useDataTable({ - data: customer_groups ?? [], - columns, - enablePagination: true, - count, - getRowId: (row) => row.id, - pageSize: PAGE_SIZE, - }) + const filters = useFilters() + + const { customer_groups, count, isPending, isError, error } = + useCustomerGroups( + { + q, + order, + offset: offset ? parseInt(offset) : undefined, + limit: PAGE_SIZE, + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + fields: "id,name,created_at,updated_at,customers.id", + }, + { + placeholderData: keepPreviousData, + } + ) if (isError) { throw error } return ( - -
-
- {t("customerGroups.domain")} - - {t("customerGroups.subtitle")} - -
- - - -
- `/customer-groups/${row.original.id}`} - orderBy={[ - { key: "name", label: t("fields.name") }, - { key: "created_at", label: t("fields.createdAt") }, - { key: "updated_at", label: t("fields.updatedAt") }, - ]} - queryObject={raw} - isLoading={isLoading} - /> -
+ + + row.id} + rowHref={(row) => `/customer-groups/${row.id}`} + action={{ + label: t("actions.create"), + to: "/customer-groups/create", + }} + emptyState={{ + empty: { + heading: t("customerGroups.list.empty.heading"), + description: t("customerGroups.list.empty.description"), + }, + filtered: { + heading: t("customerGroups.list.filtered.heading"), + description: t("customerGroups.list.filtered.description"), + }, + }} + pageSize={PAGE_SIZE} + isLoading={isPending} + /> + + ) } -const CustomerGroupRowActions = ({ - group, -}: { - group: HttpTypes.AdminCustomerGroup -}) => { +const columnHelper = createDataTableColumnHelper() + +const useColumns = () => { const { t } = useTranslation() + const { getFullDate } = useDate() + const navigate = useNavigate() const prompt = usePrompt() - const { mutateAsync } = useDeleteCustomerGroup(group.id) + const { mutateAsync: deleteCustomerGroup } = useDeleteCustomerGroupLazy() + + const handleDeleteCustomerGroup = useCallback( + async ({ id, name }: { id: string; name: string }) => { + const res = await prompt({ + title: t("customerGroups.delete.title"), + description: t("customerGroups.delete.description", { + name, + }), + verificationText: name, + verificationInstruction: t("general.typeToConfirm"), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await deleteCustomerGroup( + { id }, + { + onSuccess: () => { + toast.success(t("customerGroups.delete.successToast", { name })) + }, + onError: (e) => { + toast.error(e.message) + }, + } + ) + }, + [t, prompt, deleteCustomerGroup] + ) - const handleDelete = async () => { - const res = await prompt({ - title: t("customerGroups.delete.title"), - description: t("customerGroups.delete.description", { - name: group.name, + return useMemo(() => { + return [ + columnHelper.accessor("name", { + header: t("fields.name"), + enableSorting: true, + sortAscLabel: t("filters.sorting.alphabeticallyAsc"), + sortDescLabel: t("filters.sorting.alphabeticallyDesc"), }), - confirmText: t("actions.delete"), - cancelText: t("actions.cancel"), - }) - - if (!res) { - return - } - - await mutateAsync(undefined, { - onSuccess: () => { - toast.success( - t("customerGroups.delete.successToast", { - name: group.name, - }) - ) - }, - onError: (error) => { - toast.error(error.message) - }, - }) - } - - return ( - { + return {row.original.customers?.length ?? 0} + }, + }), + columnHelper.accessor("created_at", { + header: t("fields.createdAt"), + cell: ({ row }) => { + return ( + + {getFullDate({ + date: row.original.created_at, + includeTime: true, + })} + + ) + }, + enableSorting: true, + sortAscLabel: t("filters.sorting.dateAsc"), + sortDescLabel: t("filters.sorting.dateDesc"), + }), + columnHelper.accessor("updated_at", { + header: t("fields.updatedAt"), + cell: ({ row }) => { + return ( + + {getFullDate({ + date: row.original.updated_at, + includeTime: true, + })} + + ) + }, + enableSorting: true, + sortAscLabel: t("filters.sorting.dateAsc"), + sortDescLabel: t("filters.sorting.dateDesc"), + }), + columnHelper.action({ + actions: [ + [ { - label: t("actions.edit"), - to: `/customer-groups/${group.id}/edit`, icon: , + label: t("actions.edit"), + onClick: (row) => { + navigate(`/customer-groups/${row.row.original.id}/edit`) + }, }, ], - }, - { - actions: [ + [ { - label: t("actions.delete"), - onClick: handleDelete, icon: , + label: t("actions.delete"), + onClick: (row) => { + handleDeleteCustomerGroup({ + id: row.row.original.id, + name: row.row.original.name ?? "", + }) + }, }, ], - }, - ]} - /> - ) + ], + }), + ] + }, [t, navigate, getFullDate, handleDeleteCustomerGroup]) } -const columnHelper = createColumnHelper() +const filterHelper = createDataTableFilterHelper() -const useColumns = () => { - const columns = useCustomerGroupTableColumns() - - return useMemo( - () => [ - ...columns, - columnHelper.display({ - id: "actions", - cell: ({ row }) => , +const useFilters = () => { + const { t } = useTranslation() + const { getFullDate } = useDate() + const dateFilterOptions = useDateFilterOptions() + + return useMemo(() => { + return [ + filterHelper.accessor("created_at", { + type: "date", + label: t("fields.createdAt"), + format: "date", + formatDateValue: (date) => getFullDate({ date }), + rangeOptionStartLabel: t("filters.date.starting"), + rangeOptionEndLabel: t("filters.date.ending"), + rangeOptionLabel: t("filters.date.custom"), + options: dateFilterOptions, }), - ], - [columns] - ) + filterHelper.accessor("updated_at", { + type: "date", + label: t("fields.updatedAt"), + format: "date", + rangeOptionStartLabel: t("filters.date.starting"), + rangeOptionEndLabel: t("filters.date.ending"), + rangeOptionLabel: t("filters.date.custom"), + formatDateValue: (date) => getFullDate({ date }), + options: dateFilterOptions, + }), + ] + }, [t, dateFilterOptions, getFullDate]) } diff --git a/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx index 8d391a76480ce..543507e8c883e 100644 --- a/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx +++ b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx @@ -15,17 +15,18 @@ import { PencilSquare, Trash } from "@medusajs/icons" import { keepPreviousData } from "@tanstack/react-query" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" -import { ActionMenu } from "../../../../../components/common/action-menu/index.ts" -import { DataTable } from "../../../../../components/table/data-table/index.ts" + +import { ActionMenu } from "../../../../../components/common/action-menu" +import { _DataTable } from "../../../../../components/table/data-table" +import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api" import { useCustomerGroups, useRemoveCustomersFromGroup, -} from "../../../../../hooks/api/customer-groups.tsx" -import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns.tsx" -import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters.tsx" -import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query.tsx" -import { useDataTable } from "../../../../../hooks/use-data-table.tsx" -import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api" +} from "../../../../../hooks/api/customer-groups" +import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns" +import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters" +import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" type CustomerGroupSectionProps = { customer: HttpTypes.AdminCustomer @@ -97,19 +98,23 @@ export const CustomerGroupSection = ({ return } - try { - await batchCustomerCustomerGroups({ remove: customerGroupIds }) - - toast.success( - t("customers.groups.removed.success", { - groups: customer_groups! - .filter((cg) => customerGroupIds.includes(cg.id)) - .map((cg) => cg?.name), - }) - ) - } catch (e) { - toast.error(e.message) - } + await batchCustomerCustomerGroups( + { remove: customerGroupIds }, + { + onSuccess: () => { + toast.success( + t("customers.groups.removed.success", { + groups: customer_groups! + .filter((cg) => customerGroupIds.includes(cg.id)) + .map((cg) => cg?.name), + }) + ) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) } if (isError) { @@ -126,7 +131,7 @@ export const CustomerGroupSection = ({ - { ), }), ], - [columns] + [columns, customerId] ) } diff --git a/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx index 9dc1ff7725bb2..35a57fa774035 100644 --- a/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx +++ b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx @@ -6,7 +6,7 @@ import { createColumnHelper } from "@tanstack/react-table" import { useMemo } from "react" import { useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" -import { DataTable } from "../../../../../components/table/data-table" +import { _DataTable } from "../../../../../components/table/data-table" import { useOrders } from "../../../../../hooks/api/orders" import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns" import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters" @@ -70,7 +70,7 @@ export const CustomerOrderSection = ({ {/* */} {/**/} - { - - { {t("actions.create")} - >({ - - - - - - - - - {
{t("orders.domain")}
- - { return (
-
- { {t("actions.create")} - - {t("products.domain")} - { {t("actions.create")} - {t("products.domain")} - { {t("actions.create")} - - - { const { t } = useTranslation() - const navigate = useNavigate() - const { searchParams, raw } = useProductVariantTableQuery({ - pageSize: PAGE_SIZE, - }) - const { variants, count, isLoading, isError, error } = useProductVariants( + const { + q, + order, + offset, + allow_backorder, + manage_inventory, + created_at, + updated_at, + } = useQueryParams([ + "q", + "order", + "offset", + "manage_inventory", + "allow_backorder", + "created_at", + "updated_at", + ]) + + const columns = useColumns(product) + const filters = useFilters() + const commands = useCommands() + + const { variants, count, isPending, isError, error } = useProductVariants( product.id, { - ...searchParams, + q, + order, + offset: offset ? parseInt(offset) : undefined, + limit: PAGE_SIZE, + allow_backorder: allow_backorder + ? JSON.parse(allow_backorder) + : undefined, + manage_inventory: manage_inventory + ? JSON.parse(manage_inventory) + : undefined, + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, fields: "*inventory_items.inventory.location_levels,+inventory_quantity", }, { @@ -42,45 +82,40 @@ export const ProductVariantSection = ({ } ) - const [selection, setSelection] = useState({}) - - const filters = useProductVariantTableFilters() - const columns = useProductVariantTableColumns(product) - - const { table } = useDataTable({ - data: variants ?? [], - columns, - count, - enablePagination: true, - getRowId: (row) => row.id, - pageSize: PAGE_SIZE, - enableRowSelection: true, - rowSelection: { - state: selection, - updater: setSelection, - }, - meta: { - product, - }, - }) - if (isError) { throw error } return ( -
- {t("products.variants")} - row.id} + rowHref={(row) => `/products/${product.id}/variants/${row.id}`} + pageSize={PAGE_SIZE} + isLoading={isPending} + heading={t("products.variants.header")} + emptyState={{ + empty: { + heading: t("products.variants.empty.heading"), + description: t("products.variants.empty.description"), + }, + filtered: { + heading: t("products.variants.filtered.heading"), + description: t("products.variants.filtered.description"), + }, + }} + action={{ + label: t("actions.create"), + to: `variants/create`, + }} + actionMenu={{ + groups: [ { actions: [ - { - label: t("actions.create"), - to: `variants/create`, - icon: , - }, { label: t("products.editPrices"), to: `prices`, @@ -93,41 +128,289 @@ export const ProductVariantSection = ({ }, ], }, - ]} - /> -
- - `/products/${row.original.product_id}/variants/${row.id}` - } - pagination - search - queryObject={raw} - commands={[ - { - action: async (selection) => { - navigate( - `stock?${PRODUCT_VARIANT_IDS_KEY}=${Object.keys(selection).join( - "," - )}` - ) - }, - label: t("inventory.stock.action"), - shortcut: "i", - }, - ]} + ], + }} + commands={commands} />
) } + +const columnHelper = + createDataTableColumnHelper() + +const useColumns = (product: HttpTypes.AdminProduct) => { + const { t } = useTranslation() + const navigate = useNavigate() + const { mutateAsync } = useDeleteVariantLazy(product.id) + const prompt = usePrompt() + + const handleDelete = useCallback( + async (id: string, title: string) => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("products.deleteVariantWarning", { + title, + }), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync({ variantId: id }) + }, + [mutateAsync, prompt, t] + ) + + const optionColumns = useMemo(() => { + if (!product?.options) { + return [] + } + + return product.options.map((option) => { + return columnHelper.display({ + id: option.id, + header: option.title, + cell: ({ row }) => { + const variantOpt = row.original.options?.find( + (opt) => opt.option_id === option.id + ) + + if (!variantOpt) { + return - + } + + return ( +
+ + + {variantOpt.value} + + +
+ ) + }, + }) + }) + }, [product]) + + const getActions = useCallback( + (ctx: CellContext) => { + const variant = ctx.row.original as HttpTypes.AdminProductVariant & { + inventory_items: { inventory: HttpTypes.AdminInventoryItem }[] + } + + const mainActions: DataTableAction[] = [ + { + icon: , + label: t("actions.edit"), + onClick: (row) => { + navigate(`edit-variant?variant_id=${row.row.original.id}`) + }, + }, + ] + + const secondaryActions: DataTableAction[] = + [ + { + icon: , + label: t("actions.delete"), + onClick: () => handleDelete(variant.id, variant.title!), + }, + ] + + const inventoryItemsCount = variant.inventory_items?.length || 0 + + switch (inventoryItemsCount) { + case 0: + break + case 1: { + const inventoryItemLink = `/inventory/${ + variant.inventory_items![0].inventory.id + }` + + mainActions.push({ + label: t("products.variant.inventory.actions.inventoryItems"), + onClick: () => { + navigate(inventoryItemLink) + }, + icon: , + }) + break + } + default: { + const inventoryKitLink = `/inventory?${new URLSearchParams({ + id: variant.inventory_items!.map((i) => i.inventory.id).join(","), + }).toString()}` + + mainActions.push({ + label: t("products.variant.inventory.actions.inventoryKit"), + onClick: () => { + navigate(inventoryKitLink) + }, + icon: , + }) + } + } + + return [mainActions, secondaryActions] + }, + [handleDelete, navigate, t] + ) + + const getInventory = useCallback( + (variant: HttpTypes.AdminProductVariant) => { + const castVariant = variant as HttpTypes.AdminProductVariant & { + inventory_items: { inventory: HttpTypes.AdminInventoryItem }[] + } + const quantity = variant.inventory_quantity + + const inventoryItems = castVariant.inventory_items?.map( + (i) => i.inventory + ) + + const hasInventoryKit = inventoryItems.length > 1 + + const locations: Record = {} + + inventoryItems.forEach((i) => { + i.location_levels?.forEach((l) => { + locations[l.id] = true + }) + }) + + const locationCount = Object.keys(locations).length + + const text = hasInventoryKit + ? t("products.variant.tableItemAvailable", { + availableCount: quantity, + }) + : t("products.variant.tableItem", { + availableCount: quantity, + locationCount, + count: locationCount, + }) + + return { text, hasInventoryKit, quantity } + }, + [t] + ) + + return useMemo(() => { + return [ + columnHelper.accessor("title", { + header: t("fields.title"), + enableSorting: true, + sortAscLabel: t("filters.sorting.alphabeticallyAsc"), + sortDescLabel: t("filters.sorting.alphabeticallyDesc"), + }), + columnHelper.accessor("sku", { + header: t("fields.sku"), + enableSorting: true, + sortAscLabel: t("filters.sorting.alphabeticallyAsc"), + sortDescLabel: t("filters.sorting.alphabeticallyDesc"), + }), + ...optionColumns, + columnHelper.display({ + id: "inventory", + header: t("fields.inventory"), + cell: ({ row }) => { + const { text, hasInventoryKit, quantity } = getInventory(row.original) + + return ( +
+ {hasInventoryKit && } + + {text} + +
+ ) + }, + }), + columnHelper.action({ + actions: getActions, + }), + ] + }, [t, optionColumns, getActions, getInventory]) +} + +const filterHelper = + createDataTableFilterHelper() + +const useFilters = () => { + const { t } = useTranslation() + const { getFullDate } = useDate() + const dateFilterOptions = useDateFilterOptions() + + return useMemo(() => { + return [ + filterHelper.accessor("allow_backorder", { + type: "radio", + label: t("fields.allowBackorder"), + options: [ + { label: t("filters.radio.yes"), value: "true" }, + { label: t("filters.radio.no"), value: "false" }, + ], + }), + filterHelper.accessor("manage_inventory", { + type: "radio", + label: t("fields.manageInventory"), + options: [ + { label: t("filters.radio.yes"), value: "true" }, + { label: t("filters.radio.no"), value: "false" }, + ], + }), + filterHelper.accessor("created_at", { + type: "date", + label: t("fields.createdAt"), + format: "date", + formatDateValue: (date) => getFullDate({ date }), + rangeOptionStartLabel: t("filters.date.starting"), + rangeOptionEndLabel: t("filters.date.ending"), + rangeOptionLabel: t("filters.date.custom"), + options: dateFilterOptions, + }), + filterHelper.accessor("updated_at", { + type: "date", + label: t("fields.updatedAt"), + format: "date", + rangeOptionStartLabel: t("filters.date.starting"), + rangeOptionEndLabel: t("filters.date.ending"), + rangeOptionLabel: t("filters.date.custom"), + formatDateValue: (date) => getFullDate({ date }), + options: dateFilterOptions, + }), + ] + }, [t, dateFilterOptions, getFullDate]) +} + +const commandHelper = createDataTableCommandHelper() + +const useCommands = () => { + const { t } = useTranslation() + const navigate = useNavigate() + + return [ + commandHelper.command({ + label: t("inventory.stock.action"), + shortcut: "i", + action: async (selection) => { + navigate( + `stock?${PRODUCT_VARIANT_IDS_KEY}=${Object.keys(selection).join(",")}` + ) + }, + }), + ] +} diff --git a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/use-variant-table-columns.tsx b/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/use-variant-table-columns.tsx deleted file mode 100644 index 3c6994645e796..0000000000000 --- a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/use-variant-table-columns.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { Buildings, Component, PencilSquare, Trash } from "@medusajs/icons" -import { HttpTypes, InventoryItemDTO } from "@medusajs/types" -import { Badge, Checkbox, clx, usePrompt } from "@medusajs/ui" -import { createColumnHelper } from "@tanstack/react-table" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" - -import { - Action, - ActionMenu, -} from "../../../../../components/common/action-menu" -import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell" -import { useDeleteVariant } from "../../../../../hooks/api/products" - -const VariantActions = ({ - variant, - product, -}: { - variant: HttpTypes.AdminProductVariant & { - inventory_items: { inventory: InventoryItemDTO }[] - } - product: HttpTypes.AdminProduct -}) => { - const { mutateAsync } = useDeleteVariant(product.id, variant.id) - const { t } = useTranslation() - const prompt = usePrompt() - - const inventoryItemsCount = variant.inventory_items?.length || 0 - const hasInventoryItem = inventoryItemsCount === 1 - const hasInventoryKit = inventoryItemsCount > 1 - - const handleDelete = async () => { - const res = await prompt({ - title: t("general.areYouSure"), - description: t("products.deleteVariantWarning", { - title: variant.title, - }), - confirmText: t("actions.delete"), - cancelText: t("actions.cancel"), - }) - - if (!res) { - return - } - - await mutateAsync() - } - - const [inventoryItemLink, inventoryKitLink] = useMemo(() => { - if (!variant.inventory_items?.length) { - return ["", ""] - } - - const itemId = variant.inventory_items![0].inventory.id - const itemLink = `/inventory/${itemId}` - - const itemIds = variant.inventory_items!.map((i) => i.inventory.id) - const params = { id: itemIds } - const query = new URLSearchParams(params).toString() - - const kitLink = `/inventory?${query}` - - return [itemLink, kitLink] - }, [variant.inventory_items]) - - return ( - , - }, - hasInventoryItem - ? { - label: t("products.variant.inventory.actions.inventoryItems"), - to: inventoryItemLink, - icon: , - } - : false, - hasInventoryKit - ? { - label: t("products.variant.inventory.actions.inventoryKit"), - to: inventoryKitLink, - icon: , - } - : false, - ].filter(Boolean) as Action[], - }, - { - actions: [ - { - label: t("actions.delete"), - onClick: handleDelete, - icon: , - }, - ], - }, - ]} - /> - ) -} - -const columnHelper = createColumnHelper() - -export const useProductVariantTableColumns = ( - product?: HttpTypes.AdminProduct -) => { - const { t } = useTranslation() - - const optionColumns = useMemo(() => { - if (!product?.options) { - return [] - } - - return product.options.map((option) => { - return columnHelper.display({ - id: option.id, - header: () => ( -
- {option.title} -
- ), - cell: ({ row }) => { - const variantOpt = row.original.options?.find( - (opt) => opt.option_id === option.id - ) - if (!variantOpt) { - return - } - - return ( -
- - {variantOpt.value} - -
- ) - }, - }) - }) - }, [product]) - - return useMemo( - () => [ - columnHelper.display({ - id: "select", - header: ({ table }) => { - return ( - - table.toggleAllPageRowsSelected(!!value) - } - /> - ) - }, - cell: ({ row }) => { - return ( - row.toggleSelected(!!value)} - onClick={(e) => { - e.stopPropagation() - }} - /> - ) - }, - }), - columnHelper.accessor("title", { - header: () => ( -
- {t("fields.title")} -
- ), - cell: ({ getValue }) => ( -
- {getValue()} -
- ), - }), - columnHelper.accessor("sku", { - header: () => ( -
- {t("fields.sku")} -
- ), - cell: ({ getValue }) => { - const value = getValue() - - if (!value) { - return - } - - return ( -
- {value} -
- ) - }, - }), - ...optionColumns, - columnHelper.accessor("inventory_items", { - header: () => ( -
- {t("fields.inventory")} -
- ), - cell: ({ getValue, row }) => { - const variant = row.original - - if (!variant.manage_inventory) { - return t("products.variant.inventory.notManaged") - } - - const inventory: InventoryItemDTO[] = getValue().map( - (i) => i.inventory - ) - - const hasInventoryKit = inventory.length > 1 - - const locations = {} - - inventory.forEach((i) => { - i.location_levels.forEach((l) => { - locations[l.id] = true - }) - }) - - const locationCount = Object.keys(locations).length - - const text = hasInventoryKit - ? t("products.variant.tableItemAvailable", { - availableCount: variant.inventory_quantity, - }) - : t("products.variant.tableItem", { - availableCount: variant.inventory_quantity, - locationCount, - count: locationCount, - }) - - return ( -
- {hasInventoryKit && } - - {text} - -
- ) - }, - }), - columnHelper.display({ - id: "actions", - cell: ({ row, table }) => { - const { product } = table.options.meta as { - product: HttpTypes.AdminProduct - } - - return - }, - }), - ], - [t, optionColumns] - ) -} diff --git a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/use-variant-table-filters.tsx b/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/use-variant-table-filters.tsx deleted file mode 100644 index 7b5faa884c750..0000000000000 --- a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/use-variant-table-filters.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useTranslation } from "react-i18next" -import { Filter } from "../../../../../components/table/data-table" - -export const useProductVariantTableFilters = () => { - const { t } = useTranslation() - - let filters: Filter[] = [] - - const manageInventoryFilter: Filter = { - key: "manage_inventory", - label: t("fields.managedInventory"), - type: "select", - options: [ - { - label: t("fields.true"), - value: "true", - }, - { - label: t("fields.false"), - value: "false", - }, - ], - } - - const allowBackorderFilter: Filter = { - key: "allow_backorder", - label: t("fields.allowBackorder"), - type: "select", - options: [ - { - label: t("fields.true"), - value: "true", - }, - { - label: t("fields.false"), - value: "false", - }, - ], - } - - const dateFilters: Filter[] = [ - { label: t("fields.createdAt"), key: "created_at" }, - { label: t("fields.updatedAt"), key: "updated_at" }, - ].map((f) => ({ - key: f.key, - label: f.label, - type: "date", - })) - - filters = [ - ...filters, - manageInventoryFilter, - allowBackorderFilter, - ...dateFilters, - ] - - return filters -} diff --git a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/use-variant-table-query.tsx b/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/use-variant-table-query.tsx deleted file mode 100644 index b21ac3bc989b0..0000000000000 --- a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/use-variant-table-query.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { HttpTypes } from "@medusajs/types" -import { useQueryParams } from "../../../../../hooks/use-query-params" - -export const useProductVariantTableQuery = ({ - pageSize, - prefix, -}: { - pageSize: number - prefix?: string -}) => { - const queryObject = useQueryParams( - [ - "offset", - "q", - "manage_inventory", - "allow_backorder", - "order", - "created_at", - "updated_at", - ], - prefix - ) - - const { - offset, - manage_inventory, - allow_backorder, - created_at, - updated_at, - q, - order, - } = queryObject - - const searchParams: HttpTypes.AdminProductVariantParams = { - limit: pageSize, - offset: offset ? Number(offset) : 0, - manage_inventory: manage_inventory - ? manage_inventory === "true" - : undefined, - allow_backorder: allow_backorder ? allow_backorder === "true" : undefined, - order, - created_at: created_at ? JSON.parse(created_at) : undefined, - updated_at: updated_at ? JSON.parse(updated_at) : undefined, - q, - } - - return { - searchParams, - raw: queryObject, - } -} diff --git a/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx index cb9bc2dc774d6..6cb2ae04bc613 100644 --- a/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx +++ b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx @@ -8,7 +8,7 @@ import { Link, Outlet, useLoaderData, useLocation } from "react-router-dom" import { HttpTypes } from "@medusajs/types" import { ActionMenu } from "../../../../../components/common/action-menu" -import { DataTable } from "../../../../../components/table/data-table" +import { _DataTable } from "../../../../../components/table/data-table" import { useDeleteProduct, useProducts, @@ -72,7 +72,7 @@ export const ProductListTable = () => { - - { - { -
- { ]} /> - { - { {t("actions.create")} - { {t("actions.create")} - - - { - { - - { ]} /> - {
{t("users.pendingInvites")} - { {t("users.invite")}
- { - { + ctx: CellContext +} + +const DataTableActionCell = ({ + ctx, +}: DataTableActionCellProps) => { + const meta = ctx.column.columnDef.meta as + | ActionColumnDefMeta + | undefined + const actions = meta?.___actions + + if (!actions) { + return null + } + + const resolvedActions = typeof actions === "function" ? actions(ctx) : actions + + if (!Array.isArray(resolvedActions)) { + return null + } + + return ( + + + + + + + + {resolvedActions.map((actionOrGroup, idx) => { + const isArray = Array.isArray(actionOrGroup) + const isLast = idx === resolvedActions.length - 1 + + return isArray ? ( + + {actionOrGroup.map((action) => ( + { + e.stopPropagation() + action.onClick(ctx) + }} + className="[&>svg]:text-ui-fg-subtle flex items-center gap-2" + > + {action.icon} + {action.label} + + ))} + {!isLast && } + + ) : ( + { + e.stopPropagation() + actionOrGroup.onClick(ctx) + }} + className="[&>svg]:text-ui-fg-subtle flex items-center gap-2" + > + {actionOrGroup.icon} + {actionOrGroup.label} + + ) + })} + + + ) +} + +export { DataTableActionCell } +export type { DataTableActionCellProps } + diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-command-bar.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-command-bar.tsx new file mode 100644 index 0000000000000..06be9f26baccc --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-command-bar.tsx @@ -0,0 +1,60 @@ +"use client" + +import * as React from "react" + +import { CommandBar } from "@/components/command-bar" +import { useDataTableContext } from "../context/use-data-table-context" + +interface DataTableCommandBarProps { + selectedLabel?: ((count: number) => string) | string +} + +const DataTableCommandBar = ({ selectedLabel }: DataTableCommandBarProps) => { + const { instance } = useDataTableContext() + + const commands = instance.getCommands() + const rowSelection = instance.getRowSelection() + + const count = Object.keys(rowSelection || []).length + + const open = commands && commands.length > 0 && count > 0 + + function getSelectedLabel(count: number) { + if (typeof selectedLabel === "function") { + return selectedLabel(count) + } + + return selectedLabel + } + + if (!commands || commands.length === 0) { + return null + } + + return ( + + + {selectedLabel && ( + + {getSelectedLabel(count)} + + + )} + {commands.map((command, idx) => ( + + command.action(rowSelection)} + label={command.label} + shortcut={command.shortcut} + /> + {idx < commands.length - 1 && } + + ))} + + + ) +} + +export { DataTableCommandBar } +export type { DataTableCommandBarProps } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-filter-bar.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-filter-bar.tsx new file mode 100644 index 0000000000000..89dd957d241e5 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-filter-bar.tsx @@ -0,0 +1,71 @@ +"use client" + +import * as React from "react" + +import { Button } from "@/components/button" +import { Skeleton } from "@/components/skeleton" +import { useDataTableContext } from "../context/use-data-table-context" +import { DataTableFilter } from "./data-table-filter" + +interface DataTableFilterBarProps { + clearAllFiltersLabel?: string +} + +const DataTableFilterBar = ({ + clearAllFiltersLabel = "Clear all", +}: DataTableFilterBarProps) => { + const { instance } = useDataTableContext() + + const filterState = instance.getFiltering() + + const clearFilters = React.useCallback(() => { + instance.clearFilters() + }, [instance]) + + const filterCount = Object.keys(filterState).length + + if (filterCount === 0) { + return null + } + + if (instance.showSkeleton) { + return + } + + return ( +
+ {Object.entries(filterState).map(([id, filter]) => ( + + ))} + {filterCount > 0 ? ( + + ) : null} +
+ ) +} + +const DataTableFilterBarSkeleton = ({ + filterCount, +}: { + filterCount: number +}) => { + return ( +
+ {Array.from({ length: filterCount }).map((_, index) => ( + + ))} + {filterCount > 0 ? : null} +
+ ) +} + +export { DataTableFilterBar } +export type { DataTableFilterBarProps } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-filter-menu.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-filter-menu.tsx new file mode 100644 index 0000000000000..acdc3cfc908de --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-filter-menu.tsx @@ -0,0 +1,65 @@ +import * as React from "react" + +import { Funnel } from "@medusajs/icons" +import { DropdownMenu } from "../../../components/dropdown-menu" +import { IconButton } from "../../../components/icon-button" +import { Skeleton } from "../../../components/skeleton" +import { Tooltip } from "../../../components/tooltip" +import { useDataTableContext } from "../context/use-data-table-context" + +interface DataTableFilterMenuProps { + tooltip?: string +} + +const DataTableFilterMenu = ({ tooltip }: DataTableFilterMenuProps) => { + const { instance } = useDataTableContext() + + const enabledFilters = Object.keys(instance.getFiltering()) + + const filterOptions = instance + .getFilters() + .filter((filter) => !enabledFilters.includes(filter.id)) + + if (!enabledFilters.length && !filterOptions.length) { + throw new Error( + "DataTable.FilterMenu was rendered but there are no filters to apply. Make sure to pass filters to 'useDataTable'" + ) + } + + const Wrapper = tooltip ? Tooltip : React.Fragment + + if (instance.showSkeleton) { + return + } + + return ( + + + + {filterOptions.map((filter) => ( + { + instance.addFilter({ id: filter.id, value: undefined }) + }} + > + {filter.label} + + ))} + + + ) +} + +const DataTableFilterMenuSkeleton = () => { + return +} + +export { DataTableFilterMenu } +export type { DataTableFilterMenuProps } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-filter.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-filter.tsx new file mode 100644 index 0000000000000..d86c6bfe531da --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-filter.tsx @@ -0,0 +1,616 @@ +"use client" + +import { CheckMini, EllipseMiniSolid, XMark } from "@medusajs/icons" +import * as React from "react" + +import { Popover } from "@/components/popover" +import { clx } from "@/utils/clx" + +import { DatePicker } from "../../../components/date-picker" +import { Label } from "../../../components/label" +import { useDataTableContext } from "../context/use-data-table-context" +import { + DataTableDateComparisonOperator, + DateFilterProps, + FilterOption, +} from "../types" +import { isDateComparisonOperator } from "../utils/is-date-comparison-operator" + +interface DataTableFilterProps { + id: string + filter: unknown +} + +const DEFAULT_FORMAT_DATE_VALUE = (d: Date) => + d.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }) +const DEFAULT_RANGE_OPTION_LABEL = "Custom" +const DEFAULT_RANGE_OPTION_START_LABEL = "Starting" +const DEFAULT_RANGE_OPTION_END_LABEL = "Ending" + +const DataTableFilter = ({ id, filter }: DataTableFilterProps) => { + const { instance } = useDataTableContext() + const [open, setOpen] = React.useState(filter === undefined) + const [isCustom, setIsCustom] = React.useState(false) + + const onOpenChange = React.useCallback( + (open: boolean) => { + if ( + !open && + (!filter || (Array.isArray(filter) && filter.length === 0)) + ) { + instance.removeFilter(id) + } + + setOpen(open) + }, + [instance, id, filter] + ) + + const removeFilter = React.useCallback(() => { + instance.removeFilter(id) + }, [instance, id]) + + const meta = instance.getFilterMeta(id) + const { type, options, label, ...rest } = meta ?? {} + + const { displayValue, isCustomRange } = React.useMemo(() => { + let displayValue: string | null = null + let isCustomRange = false + + if (typeof filter === "string") { + displayValue = options?.find((o) => o.value === filter)?.label ?? null + } + + if (Array.isArray(filter)) { + displayValue = + filter + .map((v) => options?.find((o) => o.value === v)?.label) + .join(", ") ?? null + } + + if (isDateComparisonOperator(filter)) { + displayValue = + options?.find((o) => { + if (!isDateComparisonOperator(o.value)) { + return false + } + + return ( + !isCustom && + (filter.$gte === o.value.$gte || (!filter.$gte && !o.value.$gte)) && + (filter.$lte === o.value.$lte || (!filter.$lte && !o.value.$lte)) && + (filter.$gt === o.value.$gt || (!filter.$gt && !o.value.$gt)) && + (filter.$lt === o.value.$lt || (!filter.$lt && !o.value.$lt)) + ) + })?.label ?? null + + if (!displayValue && isDateFilterProps(meta)) { + const formatDateValue = meta.formatDateValue + ? meta.formatDateValue + : DEFAULT_FORMAT_DATE_VALUE + + if (filter.$gte && !filter.$lte) { + isCustomRange = true + displayValue = `${ + meta.rangeOptionStartLabel || DEFAULT_RANGE_OPTION_START_LABEL + } ${formatDateValue(new Date(filter.$gte))}` + } + + if (filter.$lte && !filter.$gte) { + isCustomRange = true + displayValue = `${ + meta.rangeOptionEndLabel || DEFAULT_RANGE_OPTION_END_LABEL + } ${formatDateValue(new Date(filter.$lte))}` + } + + if (filter.$gte && filter.$lte) { + isCustomRange = true + displayValue = `${formatDateValue( + new Date(filter.$gte) + )} - ${formatDateValue(new Date(filter.$lte))}` + } + } + } + + return { displayValue, isCustomRange } + }, [filter, options]) + + React.useEffect(() => { + if (isCustomRange && !isCustom) { + setIsCustom(true) + } + }, [isCustomRange, isCustom]) + + if (!meta) { + return null + } + + return ( + + +
*]:txt-compact-small-plus [&>*]:flex [&>*]:items-center [&>*]:justify-center", + { + "shadow-borders-base divide-x": displayValue, + "border border-dashed": !displayValue, + } + )} + > + {displayValue && ( +
+ {label || id} +
+ )} + + {displayValue || label || id} + + + {displayValue && ( + + )} +
+
+ + {(() => { + switch (type) { + case "select": + return ( + []} + /> + ) + case "radio": + return ( + []} + /> + ) + case "date": + return ( + [] + } + isCustom={isCustom} + setIsCustom={setIsCustom} + {...rest} + /> + ) + default: + return null + } + })()} + +
+ ) +} + +type DataTableFilterDateContentProps = { + id: string + filter: unknown + options: FilterOption[] + isCustom: boolean + setIsCustom: (isCustom: boolean) => void +} & Pick< + DateFilterProps, + | "format" + | "rangeOptionLabel" + | "disableRangeOption" + | "rangeOptionStartLabel" + | "rangeOptionEndLabel" +> + +const DataTableFilterDateContent = ({ + id, + filter, + options, + format = "date", + rangeOptionLabel = DEFAULT_RANGE_OPTION_LABEL, + rangeOptionStartLabel = DEFAULT_RANGE_OPTION_START_LABEL, + rangeOptionEndLabel = DEFAULT_RANGE_OPTION_END_LABEL, + disableRangeOption = false, + isCustom, + setIsCustom, +}: DataTableFilterDateContentProps) => { + const currentValue = filter as DataTableDateComparisonOperator | undefined + const { instance } = useDataTableContext() + + const selectedValue = React.useMemo(() => { + if (!currentValue || isCustom) { + return undefined + } + + return JSON.stringify(currentValue) + }, [currentValue, isCustom]) + + const onValueChange = React.useCallback( + (valueStr: string) => { + setIsCustom(false) + + const value = JSON.parse(valueStr) as DataTableDateComparisonOperator + instance.updateFilter({ id, value }) + }, + [instance, id] + ) + + const onSelectCustom = React.useCallback(() => { + setIsCustom(true) + instance.updateFilter({ id, value: undefined }) + }, [instance, id]) + + const onCustomValueChange = React.useCallback( + (input: "$gte" | "$lte", value: Date | null) => { + const newCurrentValue = { ...currentValue } + newCurrentValue[input] = value ? value.toISOString() : undefined + instance.updateFilter({ id, value: newCurrentValue }) + }, + [instance, id] + ) + + const { focusedIndex, setFocusedIndex } = useKeyboardNavigation( + options, + (index) => { + if (index === options.length && !disableRangeOption) { + onSelectCustom() + } else { + onValueChange(JSON.stringify(options[index].value)) + } + }, + disableRangeOption ? 0 : 1 + ) + + const granularity = format === "date-time" ? "minute" : "day" + + const maxDate = currentValue?.$lte + ? granularity === "minute" + ? new Date(currentValue.$lte) + : new Date(new Date(currentValue.$lte).setHours(23, 59, 59, 999)) + : undefined + + const minDate = currentValue?.$gte + ? granularity === "minute" + ? new Date(currentValue.$gte) + : new Date(new Date(currentValue.$gte).setHours(0, 0, 0, 0)) + : undefined + + const initialFocusedIndex = isCustom ? options.length : 0 + + const onListFocus = React.useCallback(() => { + if (focusedIndex === -1) { + setFocusedIndex(initialFocusedIndex) + } + }, [focusedIndex, initialFocusedIndex]) + + return ( + +
+ {options.map((option, idx) => { + const value = JSON.stringify(option.value) + const isSelected = selectedValue === value + + return ( + onValueChange(value)} + onMouseEvent={setFocusedIndex} + icon={EllipseMiniSolid} + /> + ) + })} + {!disableRangeOption && ( + + )} +
+ {!disableRangeOption && isCustom && ( + +
+
+
+
+
+
+ + onCustomValueChange("$gte", value)} + /> +
+
+ + onCustomValueChange("$lte", value)} + /> +
+
+ + )} + + ) +} + +type DataTableFilterSelectContentProps = { + id: string + filter?: string[] + options: FilterOption[] +} + +const DataTableFilterSelectContent = ({ + id, + filter = [], + options, +}: DataTableFilterSelectContentProps) => { + const { instance } = useDataTableContext() + + const onValueChange = React.useCallback( + (value: string) => { + if (filter?.includes(value)) { + const newValues = filter?.filter((v) => v !== value) + instance.updateFilter({ + id, + value: newValues, + }) + } else { + instance.updateFilter({ + id, + value: [...(filter ?? []), value], + }) + } + }, + [instance, id, filter] + ) + + const { focusedIndex, setFocusedIndex } = useKeyboardNavigation( + options, + (index) => onValueChange(options[index].value) + ) + + const onListFocus = React.useCallback(() => { + if (focusedIndex === -1) { + setFocusedIndex(0) + } + }, [focusedIndex]) + + return ( +
+ {options.map((option, idx) => { + const isSelected = !!filter?.includes(option.value) + + return ( + onValueChange(option.value)} + onMouseEvent={setFocusedIndex} + icon={CheckMini} + /> + ) + })} +
+ ) +} + +type DataTableFilterRadioContentProps = { + id: string + filter: unknown + options: FilterOption[] +} + +const DataTableFilterRadioContent = ({ + id, + filter, + options, +}: DataTableFilterRadioContentProps) => { + const { instance } = useDataTableContext() + + const onValueChange = React.useCallback( + (value: string) => { + instance.updateFilter({ id, value }) + }, + [instance, id] + ) + + const { focusedIndex, setFocusedIndex } = useKeyboardNavigation( + options, + (index) => onValueChange(options[index].value) + ) + + const onListFocus = React.useCallback(() => { + if (focusedIndex === -1) { + setFocusedIndex(0) + } + }, [focusedIndex]) + + return ( +
+ {options.map((option, idx) => { + const isSelected = filter === option.value + + return ( + onValueChange(option.value)} + onMouseEvent={setFocusedIndex} + icon={EllipseMiniSolid} + /> + ) + })} +
+ ) +} + +function isDateFilterProps(props?: unknown | null): props is DateFilterProps { + if (!props) { + return false + } + + return (props as DateFilterProps).type === "date" +} + +type OptionButtonProps = { + index: number + option: FilterOption + isSelected: boolean + isFocused: boolean + onClick: () => void + onMouseEvent: (idx: number) => void + icon: React.ElementType +} + +const OptionButton = React.memo( + ({ + index, + option, + isSelected, + isFocused, + onClick, + onMouseEvent, + icon: Icon, + }: OptionButtonProps) => ( + + ) +) + +function useKeyboardNavigation( + options: unknown[], + onSelect: (index: number) => void, + extraItems: number = 0 +) { + const [focusedIndex, setFocusedIndex] = React.useState(-1) + + const onKeyDown = React.useCallback( + (e: KeyboardEvent) => { + const totalLength = options.length + extraItems + + if ((document.activeElement as HTMLElement).contentEditable === "true") { + return + } + + switch (e.key) { + case "ArrowDown": + e.preventDefault() + setFocusedIndex((prev) => (prev < totalLength - 1 ? prev + 1 : prev)) + break + case "ArrowUp": + e.preventDefault() + setFocusedIndex((prev) => (prev > 0 ? prev - 1 : prev)) + break + case " ": + case "Enter": + e.preventDefault() + if (focusedIndex >= 0) { + onSelect(focusedIndex) + } + break + } + }, + [options.length, extraItems, focusedIndex, onSelect] + ) + + React.useEffect(() => { + window.addEventListener("keydown", onKeyDown) + + return () => { + window.removeEventListener("keydown", onKeyDown) + } + }, [onKeyDown]) + + return { focusedIndex, setFocusedIndex } +} + +export { DataTableFilter } +export type { DataTableFilterProps } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-pagination.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-pagination.tsx new file mode 100644 index 0000000000000..fac8e70733367 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-pagination.tsx @@ -0,0 +1,59 @@ +"use client" + +import * as React from "react" + +import { Table } from "@/components/table" + +import { Skeleton } from "../../../components/skeleton" +import { useDataTableContext } from "../context/use-data-table-context" + +interface DataTablePaginationProps { + translations?: React.ComponentProps["translations"] +} + +const DataTablePagination = ({ translations }: DataTablePaginationProps) => { + const { instance } = useDataTableContext() + + if (!instance.enablePagination) { + throw new Error( + "DataTable.Pagination was rendered but pagination is not enabled. Make sure to pass pagination to 'useDataTable'" + ) + } + + if (instance.showSkeleton) { + return + } + + return ( + + ) +} + +const DataTablePaginationSkeleton = () => { + return ( +
+
+ +
+ + + +
+
+
+ ) +} + +export { DataTablePagination } +export type { DataTablePaginationProps } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-search.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-search.tsx new file mode 100644 index 0000000000000..e35a5012cd733 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-search.tsx @@ -0,0 +1,50 @@ +"use client" + +import { Input } from "@/components/input" +import * as React from "react" +import { Skeleton } from "../../../components/skeleton" +import { clx } from "../../../utils/clx" +import { useDataTableContext } from "../context/use-data-table-context" + +interface DataTableSearchProps { + autoFocus?: boolean + className?: string + placeholder?: string +} + +const DataTableSearch = ({ className, ...props }: DataTableSearchProps) => { + const { instance } = useDataTableContext() + + if (!instance.enableSearch) { + throw new Error( + "DataTable.Search was rendered but search is not enabled. Make sure to pass search to 'useDataTable'" + ) + } + + if (instance.showSkeleton) { + return + } + + return ( + instance.onSearchChange(e.target.value)} + className={clx( + { + "pr-[calc(15px+2px+8px)]": instance.isLoading, + }, + className + )} + {...props} + /> + ) +} + +const DataTableSearchSkeleton = () => { + return +} + +export { DataTableSearch } +export type { DataTableSearchProps } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-select-cell.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-select-cell.tsx new file mode 100644 index 0000000000000..ac58d46a7581b --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-select-cell.tsx @@ -0,0 +1,52 @@ +"use client" + +import { Checkbox } from "@/components/checkbox" +import { CheckedState } from "@radix-ui/react-checkbox" +import { CellContext, HeaderContext } from "@tanstack/react-table" +import * as React from "react" + +interface DataTableSelectCellProps { + ctx: CellContext +} + +const DataTableSelectCell = ({ + ctx, +}: DataTableSelectCellProps) => { + const checked = ctx.row.getIsSelected() + const onChange = ctx.row.getToggleSelectedHandler() + + return ( + e.stopPropagation()} + checked={checked} + onCheckedChange={onChange} + /> + ) +} + +interface DataTableSelectHeaderProps { + ctx: HeaderContext +} + +const DataTableSelectHeader = ({ + ctx, +}: DataTableSelectHeaderProps) => { + const checked = ctx.table.getIsSomePageRowsSelected() + ? "indeterminate" + : ctx.table.getIsAllPageRowsSelected() + + const onChange = (checked: CheckedState) => { + ctx.table.toggleAllPageRowsSelected(!!checked) + } + + return ( + e.stopPropagation()} + checked={checked} + onCheckedChange={onChange} + /> + ) +} + +export { DataTableSelectCell, DataTableSelectHeader } +export type { DataTableSelectCellProps, DataTableSelectHeaderProps } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-sorting-icon.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-sorting-icon.tsx new file mode 100644 index 0000000000000..0e4a4f259ad87 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-sorting-icon.tsx @@ -0,0 +1,45 @@ +"use client" + +import { clx } from "@/utils/clx" +import type { SortDirection } from "@tanstack/react-table" +import * as React from "react" + +interface SortingIconProps { + direction: SortDirection | false +} + +const DataTableSortingIcon = ({ direction }: SortingIconProps) => { + const isAscending = direction === "asc" + const isDescending = direction === "desc" + + const isSorted = isAscending || isDescending + + return ( + + + + + ) +} + +export { DataTableSortingIcon } +export type { SortingIconProps } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-sorting-menu.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-sorting-menu.tsx new file mode 100644 index 0000000000000..1515ad68f66c7 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-sorting-menu.tsx @@ -0,0 +1,155 @@ +"use client" + +import { ArrowDownMini, ArrowUpMini, DescendingSorting } from "@medusajs/icons" +import type { Column } from "@tanstack/react-table" +import * as React from "react" + +import { DropdownMenu } from "@/components/dropdown-menu" + +import { IconButton } from "../../../components/icon-button" +import { Skeleton } from "../../../components/skeleton" +import { Tooltip } from "../../../components/tooltip" +import { useDataTableContext } from "../context/use-data-table-context" +import { SortableColumnDefMeta } from "../types" + +interface DataTableSortingMenuProps { + tooltip?: string +} + +const DataTableSortingMenu = ({ tooltip }: DataTableSortingMenuProps) => { + const { instance } = useDataTableContext() + + const sortableColumns = instance + .getAllColumns() + .filter((column) => column.getCanSort()) + + const sorting = instance.getSorting() + + const selectedColumn = React.useMemo(() => { + return sortableColumns.find((column) => column.id === sorting?.id) + }, [sortableColumns, sorting]) + + const setKey = React.useCallback( + (key: string) => { + instance.setSorting((prev) => ({ id: key, desc: prev?.desc ?? false })) + }, + [instance] + ) + + const setDesc = React.useCallback( + (desc: string) => { + instance.setSorting((prev) => ({ + id: prev?.id ?? "", + desc: desc === "true", + })) + }, + [instance] + ) + + if (!instance.enableSorting) { + throw new Error( + "DataTable.SortingMenu was rendered but sorting is not enabled. Make sure to pass sorting to 'useDataTable'" + ) + } + + if (!sortableColumns.length) { + throw new Error( + "DataTable.SortingMenu was rendered but there are no sortable columns. Make sure to set `enableSorting` to true on at least one column." + ) + } + + if (instance.showSkeleton) { + return + } + + const Wrapper = tooltip ? Tooltip : React.Fragment + + return ( + + + + + + + + + + + {sortableColumns.map((column) => { + return ( + e.preventDefault()} + value={column.id} + key={column.id} + > + {getSortLabel(column)} + + ) + })} + + {sorting && ( + + + + e.preventDefault()} + value="false" + className="flex items-center gap-2" + > + + {getSortDescriptor("asc", selectedColumn)} + + e.preventDefault()} + value="true" + className="flex items-center gap-2" + > + + {getSortDescriptor("desc", selectedColumn)} + + + + )} + + + ) +} + +function getSortLabel(column: Column) { + const meta = column.columnDef.meta as SortableColumnDefMeta | undefined + let headerValue: string | undefined = undefined + + if (typeof column.columnDef.header === "string") { + headerValue = column.columnDef.header + } + + return meta?.___sortMetaData?.sortLabel ?? headerValue ?? column.id +} + +function getSortDescriptor( + direction: "asc" | "desc", + column?: Column +) { + if (!column) { + return null + } + + const meta = column.columnDef.meta as SortableColumnDefMeta | undefined + + switch (direction) { + case "asc": + return meta?.___sortMetaData?.sortAscLabel ?? "A-Z" + case "desc": + return meta?.___sortMetaData?.sortDescLabel ?? "Z-A" + } +} + +const DataTableSortingMenuSkeleton = () => { + return +} + +export { DataTableSortingMenu } +export type { DataTableSortingMenuProps } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-table.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-table.tsx new file mode 100644 index 0000000000000..de24791afc94b --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-table.tsx @@ -0,0 +1,328 @@ +"use client" + +import * as React from "react" + +import { Table } from "@/components/table" +import { flexRender } from "@tanstack/react-table" + +import { Skeleton } from "../../../components/skeleton" +import { Text } from "../../../components/text" +import { clx } from "../../../utils/clx" +import { useDataTableContext } from "../context/use-data-table-context" +import { + DataTableEmptyState, + DataTableEmptyStateContent, + DataTableEmptyStateProps, +} from "../types" +import { DataTableSortingIcon } from "./data-table-sorting-icon" + +interface DataTableTableProps { + emptyState?: DataTableEmptyStateProps +} + +const DataTableTable = ({ emptyState }: DataTableTableProps) => { + const [hoveredRowId, setHoveredRowId] = React.useState(null) + const isKeyDown = React.useRef(false) + + const [showStickyBorder, setShowStickyBorder] = React.useState(false) + const scrollableRef = React.useRef(null) + + const { instance } = useDataTableContext() + + const pageIndex = instance.pageIndex + + const columns = instance.getAllColumns() + + const hasSelect = columns.find((c) => c.id === "select") + const hasActions = columns.find((c) => c.id === "action") + + React.useEffect(() => { + const onKeyDownHandler = (event: KeyboardEvent) => { + // If an editable element is focused, we don't want to select a row + const isEditableElementFocused = getIsEditableElementFocused() + + if ( + event.key.toLowerCase() === "x" && + hoveredRowId && + !isKeyDown.current && + !isEditableElementFocused + ) { + isKeyDown.current = true + + const row = instance + .getRowModel() + .rows.find((r) => r.id === hoveredRowId) + + if (row && row.getCanSelect()) { + row.toggleSelected() + } + } + } + + const onKeyUpHandler = (event: KeyboardEvent) => { + if (event.key.toLowerCase() === "x") { + isKeyDown.current = false + } + } + + document.addEventListener("keydown", onKeyDownHandler) + document.addEventListener("keyup", onKeyUpHandler) + return () => { + document.removeEventListener("keydown", onKeyDownHandler) + document.removeEventListener("keyup", onKeyUpHandler) + } + }, [hoveredRowId, instance]) + + const handleHorizontalScroll = (e: React.UIEvent) => { + const scrollLeft = e.currentTarget.scrollLeft + + if (scrollLeft > 0) { + setShowStickyBorder(true) + } else { + setShowStickyBorder(false) + } + } + + React.useEffect(() => { + scrollableRef.current?.scroll({ top: 0, left: 0 }) + }, [pageIndex]) + + if (instance.showSkeleton) { + return + } + + return ( +
+ {instance.emptyState === DataTableEmptyState.POPULATED && ( +
+ + + {instance.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header, idx) => { + const canSort = header.column.getCanSort() + const sortDirection = header.column.getIsSorted() + const sortHandler = header.column.getToggleSortingHandler() + + const isActionHeader = header.id === "action" + const isSelectHeader = header.id === "select" + const isSpecialHeader = isActionHeader || isSelectHeader + + const Wrapper = canSort ? "button" : "div" + const isFirstColumn = hasSelect ? idx === 1 : idx === 0 + + return ( + + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {canSort && ( + + )} + + + ) + })} + + ))} + + + {instance.getRowModel().rows.map((row) => { + return ( + setHoveredRowId(row.id)} + onMouseLeave={() => setHoveredRowId(null)} + onClick={(e) => instance.onRowClick?.(e, row)} + className={clx("group/row last:border-b-0", { + "cursor-pointer": !!instance.onRowClick, + })} + > + {row.getVisibleCells().map((cell, idx) => { + const isSelectCell = cell.column.id === "select" + const isActionCell = cell.column.id === "action" + const isSpecialCell = isSelectCell || isActionCell + + const isFirstColumn = hasSelect ? idx === 1 : idx === 0 + + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ) + })} + + ) + })} + +
+
+ )} + +
+ ) +} + +interface DataTableEmptyStateDisplayProps { + state: DataTableEmptyState + props?: DataTableEmptyStateProps +} + +const DefaultEmptyStateContent = ({ + heading, + description, +}: DataTableEmptyStateContent) => ( +
+ + {heading} + + {description} +
+) + +const DataTableEmptyStateDisplay = ({ + state, + props, +}: DataTableEmptyStateDisplayProps) => { + if (state === DataTableEmptyState.POPULATED) { + return null + } + + const content = + state === DataTableEmptyState.EMPTY ? props?.empty : props?.filtered + + return ( +
+ {content?.custom ?? ( + + )} +
+ ) +} + +interface DataTableTableSkeletonProps { + pageSize?: number +} + +const DataTableTableSkeleton = ({ + pageSize = 10, +}: DataTableTableSkeletonProps) => { + return ( +
+
+
+ + {Array.from({ length: pageSize }, (_, i) => i).map((row) => ( + + ))} +
+
+
+ ) +} + +function getIsEditableElementFocused() { + const activeElement = !!document ? document.activeElement : null + const isEditableElementFocused = + activeElement instanceof HTMLInputElement || + activeElement instanceof HTMLTextAreaElement || + activeElement?.getAttribute("contenteditable") === "true" + + return isEditableElementFocused +} + +export { DataTableTable } +export type { DataTableEmptyStateProps, DataTableTableProps } + diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-toolbar.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-toolbar.tsx new file mode 100644 index 0000000000000..34efc4f9af82b --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-toolbar.tsx @@ -0,0 +1,36 @@ +import { clx } from "@/utils/clx" +import * as React from "react" + +import { DataTableFilterBar } from "./data-table-filter-bar" + +interface DataTableToolbarTranslations { + /** + * The label for the clear all filters button + * @default "Clear all" + */ + clearAll?: string +} + +interface DataTableToolbarProps { + className?: string + children?: React.ReactNode + translations?: DataTableToolbarTranslations +} + +const DataTableToolbar = ({ + children, + className, + translations, +}: DataTableToolbarProps) => { + return ( +
+
+ {children} +
+ +
+ ) +} + +export { DataTableToolbar } +export type { DataTableToolbarProps, DataTableToolbarTranslations } diff --git a/packages/design-system/ui/src/blocks/data-table/context/data-table-context-provider.tsx b/packages/design-system/ui/src/blocks/data-table/context/data-table-context-provider.tsx new file mode 100644 index 0000000000000..bce96b20c6f2c --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/context/data-table-context-provider.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" + +import { UseDataTableReturn } from "../use-data-table" +import { DataTableContext } from "./data-table-context" + +type DataTableContextProviderProps = { + instance: UseDataTableReturn + children: React.ReactNode +} + +const DataTableContextProvider = ({ + instance, + children, +}: DataTableContextProviderProps) => { + return ( + + {children} + + ) +} + +export { DataTableContextProvider } diff --git a/packages/design-system/ui/src/blocks/data-table/context/data-table-context.tsx b/packages/design-system/ui/src/blocks/data-table/context/data-table-context.tsx new file mode 100644 index 0000000000000..51a6b3aabdebd --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/context/data-table-context.tsx @@ -0,0 +1,9 @@ +import { createContext } from "react" +import { UseDataTableReturn } from "../use-data-table" + +export interface DataTableContextValue { + instance: UseDataTableReturn +} + +export const DataTableContext = + createContext | null>(null) diff --git a/packages/design-system/ui/src/blocks/data-table/context/use-data-table-context.tsx b/packages/design-system/ui/src/blocks/data-table/context/use-data-table-context.tsx new file mode 100644 index 0000000000000..86bb403a30fc2 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/context/use-data-table-context.tsx @@ -0,0 +1,17 @@ +import * as React from "react" + +import { DataTableContext, DataTableContextValue } from "./data-table-context" + +const useDataTableContext = (): DataTableContextValue => { + const context = React.useContext(DataTableContext) + + if (!context) { + throw new Error( + "useDataTableContext must be used within a DataTableContextProvider" + ) + } + + return context +} + +export { useDataTableContext } diff --git a/packages/design-system/ui/src/blocks/data-table/data-table.stories.tsx b/packages/design-system/ui/src/blocks/data-table/data-table.stories.tsx new file mode 100644 index 0000000000000..f721d88db3943 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/data-table.stories.tsx @@ -0,0 +1,473 @@ +import { faker } from "@faker-js/faker" +import type { Meta, StoryObj } from "@storybook/react" +import * as React from "react" + +import { Container } from "@/components/container" +import { PencilSquare, Trash } from "@medusajs/icons" +import { Button } from "../../components/button" +import { Heading } from "../../components/heading" +import { TooltipProvider } from "../../components/tooltip" +import { DataTable } from "./data-table" +import { + DataTableFilteringState, + DataTablePaginationState, + DataTableRowSelectionState, + DataTableSortingState, +} from "./types" +import { useDataTable } from "./use-data-table" +import { createDataTableColumnHelper } from "./utils/create-data-table-column-helper" +import { createDataTableCommandHelper } from "./utils/create-data-table-command-helper" +import { createDataTableFilterHelper } from "./utils/create-data-table-filter-helper" +import { isDateComparisonOperator } from "./utils/is-date-comparison-operator" + +const meta: Meta = { + title: "Blocks/DataTable", + component: DataTable, +} + +export default meta + +type Story = StoryObj + +type Employee = { + id: string + name: string + email: string + position: string + age: number + birthday: Date + relationshipStatus: "single" | "married" | "divorced" | "widowed" +} + +const generateEmployees = (count: number): Employee[] => { + return Array.from({ length: count }, (_, i) => { + const age = faker.number.int({ min: 18, max: 65 }) + const birthday = faker.date.birthdate({ + mode: "age", + min: age, + max: age, + }) + + return { + id: i.toString(), + name: faker.person.fullName(), + email: faker.internet.email(), + position: faker.person.jobTitle(), + age, + birthday, + relationshipStatus: faker.helpers.arrayElement([ + "single", + "married", + "divorced", + "widowed", + ]), + } + }) +} + +const data: Employee[] = generateEmployees(100) + +const usePeople = ({ + q, + order, + filters, + offset, + limit, +}: { + q?: string + order?: DataTableSortingState | null + filters?: DataTableFilteringState + offset?: number + limit?: number +}) => { + return React.useMemo(() => { + let results = [...data] + + if (q) { + results = results.filter((person) => + person.name.toLowerCase().includes(q.toLowerCase()) + ) + } + + if (filters && Object.keys(filters).length > 0) { + results = results.filter((person) => { + return Object.entries(filters).every(([key, filter]) => { + if (!filter) return true + + const value = person[key as keyof Employee] + + if (key === "birthday") { + if (isDateComparisonOperator(filter)) { + if (!(value instanceof Date)) { + return false + } + + if (filter.$gte) { + const compareDate = new Date(filter.$gte) + if (value < compareDate) { + return false + } + } + + if (filter.$lte) { + const compareDate = new Date(filter.$lte) + if (value > compareDate) { + return false + } + } + + if (filter.$gt) { + const compareDate = new Date(filter.$gt) + if (value <= compareDate) { + return false + } + } + + if (filter.$lt) { + const compareDate = new Date(filter.$lt) + if (value >= compareDate) { + return false + } + } + + return true + } + } + + if (Array.isArray(filter)) { + if (filter.length === 0) return true + + return filter.includes(value) + } + + return filter === value + }) + }) + } + + // Apply sorting + if (order) { + const key = order.id as keyof Employee + const desc = order.desc + + results.sort((a, b) => { + const aVal = a[key] + const bVal = b[key] + + if (aVal instanceof Date && bVal instanceof Date) { + return desc + ? bVal.getTime() - aVal.getTime() + : aVal.getTime() - bVal.getTime() + } + + if (aVal < bVal) return desc ? 1 : -1 + if (aVal > bVal) return desc ? -1 : 1 + return 0 + }) + } + + if (offset) { + results = results.slice(offset) + } + + if (limit) { + results = results.slice(0, limit) + } + + return { + data: results, + count: data.length, + } + }, [q, order, filters, offset, limit]) // Add filters to dependencies +} + +const columnHelper = createDataTableColumnHelper() + +const columns = [ + columnHelper.select(), + columnHelper.accessor("name", { + header: "Name", + enableSorting: true, + sortAscLabel: "A-Z", + sortDescLabel: "Z-A", + }), + columnHelper.accessor("email", { + header: "Email", + enableSorting: true, + sortAscLabel: "A-Z", + sortDescLabel: "Z-A", + maxSize: 200, + }), + columnHelper.accessor("position", { + header: "Position", + enableSorting: true, + sortAscLabel: "A-Z", + sortDescLabel: "Z-A", + }), + columnHelper.accessor("age", { + header: "Age", + enableSorting: true, + sortAscLabel: "Low to High", + sortDescLabel: "High to Low", + sortLabel: "Age", + }), + columnHelper.accessor("birthday", { + header: "Birthday", + cell: ({ row }) => { + return ( +
+ {row.original.birthday.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + })} +
+ ) + }, + enableSorting: true, + sortAscLabel: "Oldest to Youngest", + sortDescLabel: "Youngest to Oldest", + }), + columnHelper.accessor("relationshipStatus", { + header: "Relationship Status", + cell: ({ row }) => { + return ( +
+ {row.original.relationshipStatus.charAt(0).toUpperCase() + + row.original.relationshipStatus.slice(1)} +
+ ) + }, + enableSorting: true, + sortAscLabel: "A-Z", + sortDescLabel: "Z-A", + }), + columnHelper.action({ + actions: (ctx) => { + const actions = [ + [ + { + label: "Edit", + onClick: () => {}, + icon: , + }, + { + label: "Edit", + onClick: () => {}, + icon: , + }, + { + label: "Edit", + onClick: () => {}, + icon: , + }, + ], + [ + { + label: "Delete", + onClick: () => {}, + icon: , + }, + ], + ] + + return actions + }, + }), +] + +const filterHelper = createDataTableFilterHelper() + +const filters = [ + filterHelper.accessor("birthday", { + label: "Birthday", + type: "date", + format: "date", + options: [ + { + label: "18 - 25 years old", + value: { + $lte: new Date( + new Date().setFullYear(new Date().getFullYear() - 18) + ).toISOString(), + $gte: new Date( + new Date().setFullYear(new Date().getFullYear() - 25) + ).toISOString(), + }, + }, + { + label: "26 - 35 years old", + value: { + $lte: new Date( + new Date().setFullYear(new Date().getFullYear() - 26) + ).toISOString(), + $gte: new Date( + new Date().setFullYear(new Date().getFullYear() - 35) + ).toISOString(), + }, + }, + { + label: "36 - 45 years old", + value: { + $lte: new Date( + new Date().setFullYear(new Date().getFullYear() - 36) + ).toISOString(), + $gte: new Date( + new Date().setFullYear(new Date().getFullYear() - 45) + ).toISOString(), + }, + }, + { + label: "46 - 55 years old", + value: { + $lte: new Date( + new Date().setFullYear(new Date().getFullYear() - 46) + ).toISOString(), + $gte: new Date( + new Date().setFullYear(new Date().getFullYear() - 55) + ).toISOString(), + }, + }, + { + label: "Over 55 years old", + value: { + $lt: new Date( + new Date().setFullYear(new Date().getFullYear() - 55) + ).toISOString(), + }, + }, + ], + }), + filterHelper.accessor("relationshipStatus", { + label: "Relationship Status", + type: "select", + options: [ + { label: "Single", value: "single" }, + { label: "Married", value: "married" }, + { label: "Divorced", value: "divorced" }, + { label: "Widowed", value: "widowed" }, + ], + }), +] + +const commandHelper = createDataTableCommandHelper() + +const commands = [ + commandHelper.command({ + label: "Archive", + action: (selection) => { + alert(`Archive ${Object.keys(selection).length} items`) + }, + shortcut: "A", + }), + commandHelper.command({ + label: "Delete", + action: (selection) => { + alert(`Delete ${Object.keys(selection).length} items`) + }, + shortcut: "D", + }), +] + +const KitchenSinkDemo = () => { + const [search, setSearch] = React.useState("") + + const [rowSelection, setRowSelection] = + React.useState({}) + const [sorting, setSorting] = React.useState( + null + ) + const [filtering, setFiltering] = React.useState({ + birthday: { + $gte: new Date( + new Date().setFullYear(new Date().getFullYear() - 18) + ).toISOString(), + }, + }) + + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }) + + const { data, count } = usePeople({ + q: search, + order: sorting, + filters: filtering, + offset: pagination.pageIndex * pagination.pageSize, + limit: pagination.pageSize, + }) + + const table = useDataTable({ + data, + columns, + filters, + commands, + rowCount: count, + getRowId: (row) => row.id, + onRowClick: (_event, row) => { + alert(`Navigate to ${row.id}`) + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + rowSelection: { + state: rowSelection, + onRowSelectionChange: setRowSelection, + }, + sorting: { + state: sorting, + onSortingChange: setSorting, + }, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + }) + + return ( + + + + + Employees +
+ + + + +
+
+ + + `${count} selected`} + /> +
+
+
+ ) +} + +export const KitchenSink: Story = { + render: () => , +} diff --git a/packages/design-system/ui/src/blocks/data-table/data-table.tsx b/packages/design-system/ui/src/blocks/data-table/data-table.tsx new file mode 100644 index 0000000000000..489d3eac526cd --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/data-table.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" + +import { clx } from "@/utils/clx" + +import { DataTableCommandBar } from "./components/data-table-command-bar" +import { DataTableFilterMenu } from "./components/data-table-filter-menu" +import { DataTablePagination } from "./components/data-table-pagination" +import { DataTableSearch } from "./components/data-table-search" +import { DataTableSortingMenu } from "./components/data-table-sorting-menu" +import { DataTableTable } from "./components/data-table-table" +import { DataTableToolbar } from "./components/data-table-toolbar" +import { DataTableContextProvider } from "./context/data-table-context-provider" +import { UseDataTableReturn } from "./use-data-table" + +interface DataTableProps { + instance: UseDataTableReturn + children?: React.ReactNode + className?: string +} + +const Root = ({ + instance, + children, + className, +}: DataTableProps) => { + return ( + +
+ {children} +
+
+ ) +} + +const DataTable = Object.assign(Root, { + Table: DataTableTable, + Toolbar: DataTableToolbar, + Search: DataTableSearch, + SortingMenu: DataTableSortingMenu, + FilterMenu: DataTableFilterMenu, + Pagination: DataTablePagination, + CommandBar: DataTableCommandBar, +}) + +export { DataTable } +export type { DataTableProps } diff --git a/packages/design-system/ui/src/blocks/data-table/index.ts b/packages/design-system/ui/src/blocks/data-table/index.ts new file mode 100644 index 0000000000000..54eeda8f12569 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/index.ts @@ -0,0 +1,19 @@ +export * from "./data-table" +export * from "./use-data-table" +export * from "./utils/create-data-table-column-helper" +export * from "./utils/create-data-table-command-helper" +export * from "./utils/create-data-table-filter-helper" + +export type { + DataTableAction, + DataTableCommand, + DataTableDateComparisonOperator, + DataTableEmptyState, + DataTableEmptyStateContent, + DataTableEmptyStateProps, + DataTableFilter, + DataTableFilteringState, + DataTablePaginationState, + DataTableRowSelectionState, + DataTableSortingState, +} from "./types" diff --git a/packages/design-system/ui/src/blocks/data-table/types.ts b/packages/design-system/ui/src/blocks/data-table/types.ts new file mode 100644 index 0000000000000..12d86ef36df3f --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/types.ts @@ -0,0 +1,212 @@ +import type { + AccessorFn, + AccessorFnColumnDef, + AccessorKeyColumnDef, + CellContext, + ColumnSort, + DeepKeys, + DeepValue, + DisplayColumnDef, + IdentifiedColumnDef, + PaginationState, + RowSelectionState, +} from "@tanstack/react-table" + +export type DataTableAction = { + label: string + onClick: (ctx: CellContext) => void + icon?: React.ReactNode +} + +export interface DataTableActionColumnDef + extends Pick, "meta"> { + actions: + | DataTableAction[] + | DataTableAction[][] + | (( + ctx: CellContext + ) => DataTableAction[] | DataTableAction[][]) +} + +export interface DataTableSelectColumnDef + extends Pick, "cell" | "header"> {} + +export type SortableColumnDef = { + sortLabel?: string + sortAscLabel?: string + sortDescLabel?: string +} + +export type SortableColumnDefMeta = { + ___sortMetaData?: SortableColumnDef +} + +export type ActionColumnDefMeta = { + ___actions?: + | DataTableAction[] + | DataTableAction[][] + | ((ctx: CellContext) => DataTableAction[]) +} + +export type DataTableColumnSizing = { + /** + * The maximum size of the column. + */ + maxSize?: number + /** + * The minimum size of the column. + */ + minSize?: number + /** + * The size of the column. + */ +} + +export interface DataTableColumnDef extends DataTableColumnSizing { + /** + * Whether the column is sortable. + * @default false + */ + enableSorting?: boolean +} + +export interface DataTableDisplayColumnDef extends DataTableColumnSizing { + id: string +} + +export interface DataTableColumnHelper { + accessor: < + TAccessor extends AccessorFn | DeepKeys, + TValue extends TAccessor extends AccessorFn + ? TReturn + : TAccessor extends DeepKeys + ? DeepValue + : never + >( + accessor: TAccessor, + column: TAccessor extends AccessorFn + ? Pick, "meta" | "header" | "cell"> & + DataTableColumnDef & + SortableColumnDef + : Pick< + IdentifiedColumnDef, + "id" | "meta" | "header" | "cell" + > & + DataTableColumnDef & + SortableColumnDef + ) => TAccessor extends AccessorFn + ? AccessorFnColumnDef + : AccessorKeyColumnDef + display: ( + column: Pick, "meta" | "header" | "cell"> & + DataTableDisplayColumnDef + ) => DisplayColumnDef + action: ( + props: DataTableActionColumnDef + ) => DisplayColumnDef + select: ( + props?: DataTableSelectColumnDef + ) => DisplayColumnDef +} + +export interface DataTableSortingState extends ColumnSort {} +export interface DataTableRowSelectionState extends RowSelectionState {} +export interface DataTablePaginationState extends PaginationState {} +export type DataTableFilteringState< + T extends Record = Record +> = { + [K in keyof T]: T[K] +} + +export type FilterType = "radio" | "select" | "date" +export type FilterOption = { + label: string + value: T +} + +interface BaseFilterProps { + type: FilterType + label: string +} + +export interface RadioFilterProps extends BaseFilterProps { + type: "radio" + options: FilterOption[] +} + +export interface SelectFilterProps extends BaseFilterProps { + type: "select" + options: FilterOption[] +} + +export interface DateFilterProps extends BaseFilterProps { + type: "date" + /** + * The format of the date. + * @default "date" + */ + format?: "date" | "date-time" + rangeOptionLabel?: string + rangeOptionStartLabel?: string + rangeOptionEndLabel?: string + disableRangeOption?: boolean + formatDateValue?: (value: Date) => string + options: FilterOption[] +} + +export type DataTableFilterProps = + | RadioFilterProps + | SelectFilterProps + | DateFilterProps + +export type DataTableFilter< + T extends DataTableFilterProps = DataTableFilterProps +> = T & { + id: string +} + +export enum DataTableEmptyState { + EMPTY = "EMPTY", + FILTERED_EMPTY = "FILTERED_EMPTY", + POPULATED = "POPULATED", +} + +export type DataTableDateComparisonOperator = { + /** + * The filtered date must be greater than or equal to this value. + */ + $gte?: string + /** + * The filtered date must be less than or equal to this value. + */ + $lte?: string + /** + * The filtered date must be less than this value. + */ + $lt?: string + /** + * The filtered date must be greater than this value. + */ + $gt?: string +} + +type CommandAction = ( + selection: DataTableRowSelectionState +) => void | Promise + +export interface DataTableCommand { + label: string + action: CommandAction + shortcut: string +} + +export type DataTableEmptyStateContent = { + heading?: string + description?: string + custom?: React.ReactNode +} + +export type DataTableEmptyStateProps = { + filtered?: DataTableEmptyStateContent + empty?: DataTableEmptyStateContent +} diff --git a/packages/design-system/ui/src/blocks/data-table/use-data-table.tsx b/packages/design-system/ui/src/blocks/data-table/use-data-table.tsx new file mode 100644 index 0000000000000..4f2c03bad2a0a --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/use-data-table.tsx @@ -0,0 +1,526 @@ +import { + ColumnFilter, + ColumnFiltersState, + type ColumnSort, + getCoreRowModel, + PaginationState, + type RowSelectionState, + type SortingState, + type TableOptions, + type Updater, + useReactTable, +} from "@tanstack/react-table" +import * as React from "react" +import { + DataTableCommand, + DataTableDateComparisonOperator, + DataTableEmptyState, + DataTableFilter, + DataTableFilteringState, + DataTablePaginationState, + DataTableRowSelectionState, + DataTableSortingState, + FilterOption, +} from "./types" + +interface DataTableOptions + extends Pick, "data" | "columns" | "getRowId"> { + /** + * The filters which the user can apply to the table. + */ + filters?: DataTableFilter[] + /** + * The commands which the user can apply to selected rows. + */ + commands?: DataTableCommand[] + /** + * Whether the data for the table is currently being loaded. + */ + isLoading?: boolean + /** + * The state and callback for the filtering. + */ + filtering?: { + state: DataTableFilteringState + onFilteringChange: (state: DataTableFilteringState) => void + } + /** + * The state and callback for the row selection. + */ + rowSelection?: { + state: DataTableRowSelectionState + onRowSelectionChange: (state: DataTableRowSelectionState) => void + } + /** + * The state and callback for the sorting. + */ + sorting?: { + state: DataTableSortingState | null + onSortingChange: (state: DataTableSortingState) => void + } + /** + * The state and callback for the search, with optional debounce. + */ + search?: { + state: string + onSearchChange: (state: string) => void + /** + * Debounce time in milliseconds for the search callback. + * @default 300 + */ + debounce?: number + } + /** + * The state and callback for the pagination. + */ + pagination?: { + state: DataTablePaginationState + onPaginationChange: (state: DataTablePaginationState) => void + } + /** + * The function to execute when a row is clicked. + */ + onRowClick?: ( + event: React.MouseEvent, + row: TData + ) => void + /** + * The total count of rows. When working with pagination, this will be the total + * number of rows available, not the number of rows currently being displayed. + */ + rowCount?: number + /** + * Whether the page index should be reset the filtering, sorting, or pagination changes. + * + * @default true + */ + autoResetPageIndex?: boolean +} + +interface UseDataTableReturn + extends Pick< + ReturnType>, + | "getHeaderGroups" + | "getRowModel" + | "getCanNextPage" + | "getCanPreviousPage" + | "nextPage" + | "previousPage" + | "getPageCount" + | "getAllColumns" + > { + getSorting: () => DataTableSortingState | null + setSorting: ( + sortingOrUpdater: + | DataTableSortingState + | ((prev: DataTableSortingState | null) => DataTableSortingState) + ) => void + getFilters: () => DataTableFilter[] + getFilterOptions: < + T extends string | string[] | DataTableDateComparisonOperator + >( + id: string + ) => FilterOption[] | null + getFilterMeta: (id: string) => DataTableFilter | null + getFiltering: () => DataTableFilteringState + addFilter: (filter: ColumnFilter) => void + removeFilter: (id: string) => void + clearFilters: () => void + updateFilter: (filter: ColumnFilter) => void + getSearch: () => string + onSearchChange: (search: string) => void + getCommands: () => DataTableCommand[] + getRowSelection: () => DataTableRowSelectionState + onRowClick?: ( + event: React.MouseEvent, + row: TData + ) => void + emptyState: DataTableEmptyState + isLoading: boolean + showSkeleton: boolean + pageIndex: number + pageSize: number + rowCount: number + enablePagination: boolean + enableFiltering: boolean + enableSorting: boolean + enableSearch: boolean +} + +const useDataTable = ({ + rowCount = 0, + filters, + commands, + rowSelection, + sorting, + filtering, + pagination, + search, + onRowClick, + autoResetPageIndex = true, + isLoading = false, + ...options +}: DataTableOptions): UseDataTableReturn => { + const { state: sortingState, onSortingChange } = sorting ?? {} + const { state: filteringState, onFilteringChange } = filtering ?? {} + const { state: paginationState, onPaginationChange } = pagination ?? {} + const { state: rowSelectionState, onRowSelectionChange } = rowSelection ?? {} + + const autoResetPageIndexHandler = React.useCallback(() => { + return autoResetPageIndex + ? () => + paginationState && + onPaginationChange?.({ ...paginationState, pageIndex: 0 }) + : undefined + }, [autoResetPageIndex, paginationState, onPaginationChange]) + + const sortingStateHandler = React.useCallback(() => { + return onSortingChange + ? (updaterOrValue: Updater) => { + autoResetPageIndexHandler()?.() + onSortingChangeTransformer( + onSortingChange, + sortingState + )(updaterOrValue) + } + : undefined + }, [onSortingChange, sortingState, autoResetPageIndexHandler]) + + const rowSelectionStateHandler = React.useCallback(() => { + return onRowSelectionChange + ? (updaterOrValue: Updater) => { + autoResetPageIndexHandler()?.() + onRowSelectionChangeTransformer( + onRowSelectionChange, + rowSelectionState + )(updaterOrValue) + } + : undefined + }, [onRowSelectionChange, rowSelectionState, autoResetPageIndexHandler]) + + const filteringStateHandler = React.useCallback(() => { + return onFilteringChange + ? (updaterOrValue: Updater) => { + autoResetPageIndexHandler()?.() + onFilteringChangeTransformer( + onFilteringChange, + filteringState + )(updaterOrValue) + } + : undefined + }, [onFilteringChange, filteringState, autoResetPageIndexHandler]) + + const paginationStateHandler = React.useCallback(() => { + return onPaginationChange + ? onPaginationChangeTransformer(onPaginationChange, paginationState) + : undefined + }, [onPaginationChange, paginationState]) + + const instance = useReactTable({ + ...options, + getCoreRowModel: getCoreRowModel(), + state: { + rowSelection: rowSelectionState ?? {}, + sorting: sortingState ? [sortingState] : undefined, + columnFilters: Object.entries(filteringState ?? {}).map( + ([id, filter]) => ({ + id, + value: filter, + }) + ), + pagination: paginationState, + }, + rowCount, + onColumnFiltersChange: filteringStateHandler(), + onRowSelectionChange: rowSelectionStateHandler(), + onSortingChange: sortingStateHandler(), + onPaginationChange: paginationStateHandler(), + manualSorting: true, + manualPagination: true, + manualFiltering: true, + }) + + const getSorting = React.useCallback(() => { + return instance.getState().sorting?.[0] ?? null + }, [instance]) + + const setSorting = React.useCallback( + ( + sortingOrUpdater: ColumnSort | ((prev: ColumnSort | null) => ColumnSort) + ) => { + const currentSort = instance.getState().sorting?.[0] ?? null + const newSorting = + typeof sortingOrUpdater === "function" + ? sortingOrUpdater(currentSort) + : sortingOrUpdater + + autoResetPageIndexHandler()?.() + instance.setSorting([newSorting]) + }, + [instance, autoResetPageIndexHandler] + ) + + const getFilters = React.useCallback(() => { + return filters ?? [] + }, [filters]) + + const getFilterOptions = React.useCallback( + ( + id: string + ) => { + const filter = getFilters().find((filter) => filter.id === id) + + if (!filter) { + return null + } + + return filter.options as FilterOption[] + }, + [getFilters] + ) + + const getFilterMeta = React.useCallback( + (id: string) => { + return getFilters().find((filter) => filter.id === id) || null + }, + [getFilters] + ) + + const getFiltering = React.useCallback(() => { + const state = instance.getState().columnFilters ?? [] + return Object.fromEntries(state.map((filter) => [filter.id, filter.value])) + }, [instance]) + + const addFilter = React.useCallback( + (filter: ColumnFilter) => { + if (filter.value) { + autoResetPageIndexHandler()?.() + } + onFilteringChange?.({ ...getFiltering(), [filter.id]: filter.value }) + }, + [onFilteringChange, getFiltering, autoResetPageIndexHandler] + ) + + const removeFilter = React.useCallback( + (id: string) => { + const currentFilters = getFiltering() + delete currentFilters[id] + autoResetPageIndexHandler()?.() + onFilteringChange?.(currentFilters) + }, + [onFilteringChange, getFiltering, autoResetPageIndexHandler] + ) + + const clearFilters = React.useCallback(() => { + autoResetPageIndexHandler()?.() + onFilteringChange?.({}) + }, [onFilteringChange, autoResetPageIndexHandler]) + + const updateFilter = React.useCallback( + (filter: ColumnFilter) => { + addFilter(filter) + }, + [addFilter] + ) + + const { state: searchState, onSearchChange, debounce = 300 } = search ?? {} + + const [localSearch, setLocalSearch] = React.useState(searchState ?? "") + const timeoutRef = React.useRef>() + + React.useEffect(() => { + setLocalSearch(searchState ?? "") + }, [searchState]) + + const getSearch = React.useCallback(() => { + return localSearch + }, [localSearch]) + + const debouncedSearchChange = React.useMemo(() => { + if (!onSearchChange) { + return undefined + } + + return (value: string) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + + if (debounce <= 0) { + autoResetPageIndexHandler()?.() + onSearchChange(value) + return + } + + timeoutRef.current = setTimeout(() => { + autoResetPageIndexHandler()?.() + onSearchChange(value) + }, debounce) + } + }, [onSearchChange, debounce, autoResetPageIndexHandler]) + + React.useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } + }, []) + + const onSearchChangeHandler = React.useCallback( + (search: string) => { + setLocalSearch(search) + debouncedSearchChange?.(search) + }, + [debouncedSearchChange] + ) + + const getCommands = React.useCallback(() => { + return commands ?? [] + }, [commands]) + + const getRowSelection = React.useCallback(() => { + return instance.getState().rowSelection + }, [instance]) + + const rows = instance.getRowModel().rows + + const emptyState = React.useMemo(() => { + const hasRows = rows.length > 0 + const hasSearch = Boolean(searchState) + const hasFilters = Object.keys(filteringState ?? {}).length > 0 + + if (hasRows) { + return DataTableEmptyState.POPULATED + } + + if (hasSearch || hasFilters) { + return DataTableEmptyState.FILTERED_EMPTY + } + + return DataTableEmptyState.EMPTY + }, [rows, searchState, filteringState]) + + const showSkeleton = React.useMemo(() => { + return isLoading === true && rows.length === 0 + }, [isLoading, rows]) + + const enablePagination: boolean = !!pagination + const enableFiltering: boolean = !!filtering + const enableSorting: boolean = !!sorting + const enableSearch: boolean = !!search + + return { + // Table + getHeaderGroups: instance.getHeaderGroups, + getRowModel: instance.getRowModel, + getAllColumns: instance.getAllColumns, + // Pagination + enablePagination, + getCanNextPage: instance.getCanNextPage, + getCanPreviousPage: instance.getCanPreviousPage, + nextPage: instance.nextPage, + previousPage: instance.previousPage, + getPageCount: instance.getPageCount, + pageIndex: instance.getState()?.pagination?.pageIndex ?? 0, + pageSize: instance.getState()?.pagination?.pageSize ?? 10, + rowCount, + // Search + enableSearch, + getSearch, + onSearchChange: onSearchChangeHandler, + // Sorting + enableSorting, + getSorting, + setSorting, + // Filtering + enableFiltering, + getFilters, + getFilterOptions, + getFilterMeta, + getFiltering, + addFilter, + removeFilter, + clearFilters, + updateFilter, + // Commands + getCommands, + getRowSelection, + // Handlers + onRowClick, + // Empty State + emptyState, + // Loading + isLoading, + showSkeleton, + } +} + +function onSortingChangeTransformer( + onSortingChange: (state: ColumnSort) => void, + state?: ColumnSort | null +) { + return (updaterOrValue: Updater) => { + const value = + typeof updaterOrValue === "function" + ? updaterOrValue(state ? [state] : []) + : updaterOrValue + const columnSort = value[0] + + onSortingChange(columnSort) + } +} + +function onRowSelectionChangeTransformer( + onRowSelectionChange: (state: RowSelectionState) => void, + state?: RowSelectionState +) { + return (updaterOrValue: Updater) => { + const value = + typeof updaterOrValue === "function" + ? updaterOrValue(state ?? {}) + : updaterOrValue + + onRowSelectionChange(value) + } +} + +function onFilteringChangeTransformer( + onFilteringChange: (state: DataTableFilteringState) => void, + state?: DataTableFilteringState +) { + return (updaterOrValue: Updater) => { + const value = + typeof updaterOrValue === "function" + ? updaterOrValue( + Object.entries(state ?? {}).map(([id, filter]) => ({ + id, + value: filter, + })) + ) + : updaterOrValue + + const transformedValue = Object.fromEntries( + value.map((filter) => [filter.id, filter]) + ) + + onFilteringChange(transformedValue) + } +} + +function onPaginationChangeTransformer( + onPaginationChange: (state: PaginationState) => void, + state?: PaginationState +) { + return (updaterOrValue: Updater) => { + const value = + typeof updaterOrValue === "function" + ? updaterOrValue(state ?? { pageIndex: 0, pageSize: 10 }) + : updaterOrValue + + onPaginationChange(value) + } +} + +export { useDataTable } +export type { DataTableOptions, UseDataTableReturn } diff --git a/packages/design-system/ui/src/blocks/data-table/utils/create-data-table-column-helper.tsx b/packages/design-system/ui/src/blocks/data-table/utils/create-data-table-column-helper.tsx new file mode 100644 index 0000000000000..771cce4172fcb --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/utils/create-data-table-column-helper.tsx @@ -0,0 +1,76 @@ +"use client" + +import { createColumnHelper as createColumnHelperTanstack } from "@tanstack/react-table" +import * as React from "react" +import { DataTableActionCell } from "../components/data-table-action-cell" +import { + DataTableSelectCell, + DataTableSelectHeader, +} from "../components/data-table-select-cell" +import { + DataTableActionColumnDef, + DataTableColumnHelper, + DataTableSelectColumnDef, + SortableColumnDef, + SortableColumnDefMeta, +} from "../types" + +const createDataTableColumnHelper = < + TData, +>(): DataTableColumnHelper => { + const { accessor: accessorTanstack, display } = + createColumnHelperTanstack() + + return { + accessor: (accessor, column) => { + const { + sortLabel, + sortAscLabel, + sortDescLabel, + meta, + enableSorting, + ...rest + } = column as any & SortableColumnDef + + const extendedMeta: SortableColumnDefMeta = { + ___sortMetaData: { sortLabel, sortAscLabel, sortDescLabel }, + ...(meta || {}), + } + + return accessorTanstack(accessor, { + ...rest, + enableSorting: enableSorting ?? false, + meta: extendedMeta, + }) + }, + display, + action: ({ actions, ...props }: DataTableActionColumnDef) => + display({ + id: "action", + cell: (ctx) => , + meta: { + ___actions: actions, + ...(props.meta || {}), + }, + ...props, + }), + select: (props?: DataTableSelectColumnDef) => + display({ + id: "select", + header: props?.header + ? props.header + : (ctx) => , + cell: props?.cell + ? props.cell + : (ctx) => , + }), + } +} + +const helper = createColumnHelperTanstack() + +helper.accessor("name", { + meta: {}, +}) + +export { createDataTableColumnHelper } diff --git a/packages/design-system/ui/src/blocks/data-table/utils/create-data-table-command-helper.ts b/packages/design-system/ui/src/blocks/data-table/utils/create-data-table-command-helper.ts new file mode 100644 index 0000000000000..38667b4285627 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/utils/create-data-table-command-helper.ts @@ -0,0 +1,8 @@ +import { DataTableCommand } from "../types" + +const createDataTableCommandHelper = () => ({ + command: (command: DataTableCommand) => command, +}) + +export { createDataTableCommandHelper } + diff --git a/packages/design-system/ui/src/blocks/data-table/utils/create-data-table-filter-helper.ts b/packages/design-system/ui/src/blocks/data-table/utils/create-data-table-filter-helper.ts new file mode 100644 index 0000000000000..633e3e702d8d7 --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/utils/create-data-table-filter-helper.ts @@ -0,0 +1,14 @@ +import { DeepKeys } from "@tanstack/react-table" +import { DataTableFilter, DataTableFilterProps } from "../types" + + +const createDataTableFilterHelper = () => ({ + accessor: (accessor: DeepKeys, props: DataTableFilterProps) => ({ + id: accessor, + ...props, + }), + custom: (props: DataTableFilter) => props, +}) + +export { createDataTableFilterHelper } + diff --git a/packages/design-system/ui/src/blocks/data-table/utils/is-date-comparison-operator.ts b/packages/design-system/ui/src/blocks/data-table/utils/is-date-comparison-operator.ts new file mode 100644 index 0000000000000..08d4fb178e61a --- /dev/null +++ b/packages/design-system/ui/src/blocks/data-table/utils/is-date-comparison-operator.ts @@ -0,0 +1,19 @@ +import { DataTableDateComparisonOperator } from "../types"; + +export function isDateComparisonOperator( + value: unknown +): value is DataTableDateComparisonOperator { + if (typeof value !== "object" || value === null) { + return false + } + + const validOperators = ["$gte", "$lte", "$gt", "$lt"] + const hasAtLeastOneOperator = validOperators.some((op) => op in value) + + const allPropertiesValid = Object.entries(value as Record) + .every(([key, val]) => + validOperators.includes(key) && (typeof val === "string" || val === undefined) + ) + + return hasAtLeastOneOperator && allPropertiesValid +} diff --git a/packages/design-system/ui/src/components/checkbox/checkbox.tsx b/packages/design-system/ui/src/components/checkbox/checkbox.tsx index 2136906986112..94eb8b0cb9ad8 100644 --- a/packages/design-system/ui/src/components/checkbox/checkbox.tsx +++ b/packages/design-system/ui/src/components/checkbox/checkbox.tsx @@ -19,13 +19,13 @@ const Checkbox = React.forwardRef< ref={ref} checked={checked} className={clx( - "group relative inline-flex h-5 w-5 items-center justify-center outline-none ", + "group inline-flex h-5 w-5 items-center justify-center outline-none ", className )} >
( ) => { React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === shortcut) { + if (event.key.toLowerCase() === shortcut.toLowerCase()) { event.preventDefault() event.stopPropagation() action() diff --git a/packages/design-system/ui/src/components/date-picker/date-picker-field.tsx b/packages/design-system/ui/src/components/date-picker/date-picker-field.tsx index 1e0bbfc5fda2c..f3df0ef4ae2df 100644 --- a/packages/design-system/ui/src/components/date-picker/date-picker-field.tsx +++ b/packages/design-system/ui/src/components/date-picker/date-picker-field.tsx @@ -33,7 +33,6 @@ const datePickerFieldStyles = cva({ const DatePickerField = ({ size = "base", ...props }: DatePickerFieldProps) => { const { locale } = useLocale() - const state = useDateFieldState({ ...props, locale, @@ -44,7 +43,12 @@ const DatePickerField = ({ size = "base", ...props }: DatePickerFieldProps) => { const { fieldProps } = useDateField(props, state, ref) return ( -
+
{state.segments.map((segment, index) => { return })} diff --git a/packages/design-system/ui/src/components/dropdown-menu/dropdown-menu.tsx b/packages/design-system/ui/src/components/dropdown-menu/dropdown-menu.tsx index e0388f76d4f1c..1396c22f22e4d 100644 --- a/packages/design-system/ui/src/components/dropdown-menu/dropdown-menu.tsx +++ b/packages/design-system/ui/src/components/dropdown-menu/dropdown-menu.tsx @@ -48,7 +48,7 @@ const SubMenuTrigger = React.forwardRef< className={clx( "bg-ui-bg-component text-ui-fg-base txt-compact-small relative flex cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors", "focus-visible:bg-ui-bg-component-hover focus:bg-ui-bg-component-hover", - "active:bg-ui-bg-component-pressed", + "active:bg-ui-bg-component-hover", "data-[disabled]:text-ui-fg-disabled data-[disabled]:pointer-events-none", "data-[state=open]:!bg-ui-bg-component-hover", className @@ -56,7 +56,7 @@ const SubMenuTrigger = React.forwardRef< {...props} > {children} - + )) SubMenuTrigger.displayName = "DropdownMenu.SubMenuTrigger" @@ -130,7 +130,7 @@ const Item = React.forwardRef< className={clx( "bg-ui-bg-component text-ui-fg-base txt-compact-small relative flex cursor-pointer select-none items-center rounded-md px-2 py-1.5 outline-none transition-colors", "focus-visible:bg-ui-bg-component-hover focus:bg-ui-bg-component-hover", - "active:bg-ui-bg-component-pressed", + "active:bg-ui-bg-component-hover", "data-[disabled]:text-ui-fg-disabled data-[disabled]:pointer-events-none", className )} @@ -149,9 +149,9 @@ const CheckboxItem = React.forwardRef< (({ className, ...props }, ref) => ( )) diff --git a/packages/design-system/ui/src/components/icon-button/icon-button.tsx b/packages/design-system/ui/src/components/icon-button/icon-button.tsx index 06911d5f46de7..d6783f67e0b76 100644 --- a/packages/design-system/ui/src/components/icon-button/icon-button.tsx +++ b/packages/design-system/ui/src/components/icon-button/icon-button.tsx @@ -7,17 +7,16 @@ import { clx } from "@/utils/clx" const iconButtonVariants = cva({ base: clx( - "transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none", - "disabled:bg-ui-bg-disabled disabled:shadow-buttons-neutral disabled:text-ui-fg-disabled disabled:after:hidden" + "transition-fg inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none", + "disabled:bg-ui-bg-disabled disabled:shadow-buttons-neutral disabled:text-ui-fg-disabled " ), variants: { variant: { primary: clx( - "shadow-buttons-neutral text-ui-fg-subtle bg-ui-button-neutral after:button-neutral-gradient", - "hover:bg-ui-button-neutral-hover hover:after:button-neutral-hover-gradient", - "active:bg-ui-button-neutral-pressed active:after:button-neutral-pressed-gradient", - "focus-visible:shadow-buttons-neutral-focus", - "after:absolute after:inset-0 after:content-['']" + "shadow-buttons-neutral text-ui-fg-subtle bg-ui-button-neutral", + "hover:bg-ui-button-neutral-hover", + "active:bg-ui-button-neutral-pressed", + "focus-visible:shadow-buttons-neutral-focus" ), transparent: clx( "text-ui-fg-subtle bg-ui-button-transparent", diff --git a/packages/design-system/ui/src/components/skeleton/index.ts b/packages/design-system/ui/src/components/skeleton/index.ts new file mode 100644 index 0000000000000..8e713ba5d5858 --- /dev/null +++ b/packages/design-system/ui/src/components/skeleton/index.ts @@ -0,0 +1 @@ +export * from "./skeleton"; diff --git a/packages/design-system/ui/src/components/skeleton/skeleton.tsx b/packages/design-system/ui/src/components/skeleton/skeleton.tsx new file mode 100644 index 0000000000000..148572a4a84b7 --- /dev/null +++ b/packages/design-system/ui/src/components/skeleton/skeleton.tsx @@ -0,0 +1,16 @@ +import { clx } from "@/utils/clx" +import * as React from "react" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/packages/design-system/ui/src/components/table/table.tsx b/packages/design-system/ui/src/components/table/table.tsx index 40f58e18d9028..38c8bb66c75a3 100644 --- a/packages/design-system/ui/src/components/table/table.tsx +++ b/packages/design-system/ui/src/components/table/table.tsx @@ -49,7 +49,7 @@ const Cell = React.forwardRef< HTMLTableCellElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( - + )) Cell.displayName = "Table.Cell" @@ -60,7 +60,7 @@ const Header = React.forwardRef< (({ className, ...props }, ref) => ( )) diff --git a/packages/design-system/ui/src/index.ts b/packages/design-system/ui/src/index.ts index 526663311428d..707494139d805 100644 --- a/packages/design-system/ui/src/index.ts +++ b/packages/design-system/ui/src/index.ts @@ -1,3 +1,4 @@ +// Components export { Alert } from "./components/alert" export { Avatar } from "./components/avatar" export { Badge } from "./components/badge" @@ -29,6 +30,7 @@ export { ProgressTabs } from "./components/progress-tabs" export { Prompt } from "./components/prompt" export { RadioGroup } from "./components/radio-group" export { Select } from "./components/select" +export { Skeleton } from "./components/skeleton" export { StatusBadge } from "./components/status-badge" export { Switch } from "./components/switch" export { Table } from "./components/table" @@ -39,6 +41,9 @@ export { Toast } from "./components/toast" export { Toaster } from "./components/toaster" export { Tooltip, TooltipProvider } from "./components/tooltip" +// Blocks +export * from "./blocks/data-table" + // Hooks export { usePrompt } from "./hooks/use-prompt" export { useToggleState } from "./hooks/use-toggle-state" diff --git a/packages/design-system/ui/src/main.css b/packages/design-system/ui/src/main.css index 6c1817e9939d3..b0412db5677eb 100644 --- a/packages/design-system/ui/src/main.css +++ b/packages/design-system/ui/src/main.css @@ -3,8 +3,8 @@ @tailwind utilities; @layer base { - body { - @apply !bg-ui-bg-base; + :root { + @apply bg-ui-bg-subtle text-ui-fg-base antialiased; text-rendering: optimizeLegibility; } } diff --git a/yarn.lock b/yarn.lock index 6ce8ed89ad825..6d8f6997633fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4209,6 +4209,13 @@ __metadata: languageName: node linkType: hard +"@faker-js/faker@npm:^9.2.0": + version: 9.2.0 + resolution: "@faker-js/faker@npm:9.2.0" + checksum: d711a5d206558f90e3ce9ecafe366e236fbe190b4df9d3968b512ccb87ec625843c919d16050beade88b790ed3df6332f6a837e41fba6de33e7a2f8daa67f08d + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.0.0": version: 1.6.1 resolution: "@floating-ui/core@npm:1.6.1" @@ -6655,6 +6662,7 @@ __metadata: version: 0.0.0-use.local resolution: "@medusajs/ui@workspace:packages/design-system/ui" dependencies: + "@faker-js/faker": ^9.2.0 "@medusajs/icons": ^2.2.0 "@medusajs/ui-preset": ^2.2.0 "@radix-ui/react-accordion": 1.2.0 @@ -6680,6 +6688,7 @@ __metadata: "@storybook/react": ^8.3.5 "@storybook/react-vite": ^8.3.5 "@storybook/testing-library": ^0.2.2 + "@tanstack/react-table": 8.20.5 "@testing-library/dom": ^9.3.1 "@testing-library/jest-dom": ^5.16.5 "@testing-library/react": ^14.0.0