@spree/dashboard-core 1.0.0-rc.4 → 1.0.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +10 -7
  2. package/src/api-client.ts +76 -7
  3. package/src/components/address-form-dialog.tsx +2 -2
  4. package/src/components/country-combobox.tsx +1 -2
  5. package/src/components/currency-amounts-field.tsx +93 -0
  6. package/src/components/currency-select.tsx +6 -7
  7. package/src/components/file-upload-field.tsx +1 -0
  8. package/src/components/locale-select.tsx +22 -23
  9. package/src/components/nav-main.tsx +7 -2
  10. package/src/components/preferences-form.tsx +2 -8
  11. package/src/components/settings-sidebar.tsx +8 -1
  12. package/src/components/slot.tsx +21 -1
  13. package/src/components/table-toolbar.tsx +102 -39
  14. package/src/hooks/use-display-name.ts +1 -18
  15. package/src/hooks/use-file-upload.ts +2 -1
  16. package/src/hooks/use-import.ts +3 -2
  17. package/src/index.ts +17 -3
  18. package/src/lib/currencies.ts +2 -34
  19. package/src/lib/currency-amounts.test.ts +147 -0
  20. package/src/lib/currency-amounts.ts +172 -0
  21. package/src/lib/currency-parts.ts +2 -21
  22. package/src/lib/filters-to-ransack.ts +4 -1
  23. package/src/lib/form-fields-registry.ts +23 -1
  24. package/src/lib/form-mappers.ts +1 -1
  25. package/src/lib/formatters.ts +6 -10
  26. package/src/lib/fulfillment-items.ts +11 -0
  27. package/src/lib/i18n.ts +1 -12
  28. package/src/lib/iso-codes.ts +9 -26
  29. package/src/lib/money.ts +9 -99
  30. package/src/lib/normalize-money.ts +2 -85
  31. package/src/lib/return-refunds.ts +1 -2
  32. package/src/lib/route-registry.ts +29 -0
  33. package/src/lib/settings-nav-registry.ts +6 -1
  34. package/src/lib/store-config.test.ts +93 -0
  35. package/src/lib/store-config.ts +76 -0
  36. package/src/lib/table-registry.ts +16 -5
  37. package/src/locales/ar.json +51 -0
  38. package/src/locales/de.json +51 -0
  39. package/src/locales/en.json +51 -0
  40. package/src/locales/es.json +51 -0
  41. package/src/locales/fr.json +51 -0
  42. package/src/locales/pl.json +51 -0
  43. package/src/locales/zh-CN.json +51 -0
  44. package/src/pages/package-types.tsx +50 -11
  45. package/src/pages/settings-index.tsx +4 -1
  46. package/src/pages/stock-locations.tsx +31 -6
  47. package/src/products/assignable-variants.ts +35 -0
  48. package/src/products/bulk-variants-dialog.tsx +27 -28
  49. package/src/products/digital-asset-edit-sheet.tsx +192 -0
  50. package/src/products/digital-asset-provider-sheet.tsx +88 -0
  51. package/src/products/digital-assets-card.tsx +451 -0
  52. package/src/products/media-edit-sheet.tsx +68 -20
  53. package/src/products/normalize-quantity.ts +2 -3
  54. package/src/products/product-bulk-price-editor.tsx +1 -3
  55. package/src/products/product-form-cards.tsx +67 -37
  56. package/src/products/product-form-mapping.ts +44 -5
  57. package/src/products/product-schema.test.ts +8 -0
  58. package/src/products/product-schema.ts +14 -7
  59. package/src/products/product-types.ts +1 -1
  60. package/src/products/provider-settings-fields.tsx +110 -0
  61. package/src/products/sheet-save-handlers.ts +37 -0
  62. package/src/products/use-digital-assets.ts +66 -0
  63. package/src/products/variant-edit-sheet.tsx +21 -11
  64. package/src/products/variants-section.tsx +4 -1
  65. package/src/providers/store-provider.tsx +31 -7
  66. package/src/schemas/package-type.ts +14 -12
  67. package/src/schemas/stock-location.ts +2 -3
  68. package/src/tables/stock-locations.tsx +5 -8
  69. package/src/lib/money.test.ts +0 -63
  70. package/src/lib/normalize-money.test.ts +0 -67
@@ -0,0 +1,93 @@
1
+ import type { Market } from '@spree/admin-sdk'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { adminClient } from '../client'
4
+ import { listAllMarkets, marketLocalization } from './store-config'
5
+
6
+ vi.mock('../client', () => ({ adminClient: { markets: { list: vi.fn() } } }))
7
+
8
+ function market(attributes: Partial<Market>): Market {
9
+ return {
10
+ id: 'mkt_1',
11
+ name: 'Market',
12
+ currency: 'USD',
13
+ default_locale: 'en',
14
+ tax_inclusive: false,
15
+ default: false,
16
+ country_codes: [],
17
+ supported_locales: [],
18
+ created_at: '',
19
+ updated_at: '',
20
+ tax_provider: null,
21
+ ...attributes,
22
+ } as Market
23
+ }
24
+
25
+ describe('marketLocalization', () => {
26
+ const markets = [
27
+ market({
28
+ id: 'mkt_eu',
29
+ currency: 'EUR',
30
+ default_locale: 'de',
31
+ supported_locales: ['de', 'fr'],
32
+ }),
33
+ market({ id: 'mkt_us', currency: 'USD', default_locale: 'en', default: true }),
34
+ market({ id: 'mkt_ie', currency: 'EUR', default_locale: 'en' }),
35
+ ]
36
+
37
+ it('takes the defaults from the default market', () => {
38
+ const { defaultCurrency, defaultLocale } = marketLocalization(markets)
39
+
40
+ expect(defaultCurrency).toBe('USD')
41
+ expect(defaultLocale).toBe('en')
42
+ })
43
+
44
+ it('lists each market currency once, the default market first', () => {
45
+ expect(marketLocalization(markets).currencies).toEqual(['USD', 'EUR'])
46
+ })
47
+
48
+ it('lists every language any market offers', () => {
49
+ expect(marketLocalization(markets).locales).toEqual(['de', 'en', 'fr'])
50
+ })
51
+
52
+ it('falls back to the first market when none is flagged default', () => {
53
+ const { defaultCurrency } = marketLocalization([market({ currency: 'GBP' })])
54
+
55
+ expect(defaultCurrency).toBe('GBP')
56
+ })
57
+
58
+ it('returns empty lists before markets load', () => {
59
+ expect(marketLocalization([])).toEqual({
60
+ currencies: [],
61
+ locales: [],
62
+ defaultCurrency: undefined,
63
+ defaultLocale: undefined,
64
+ })
65
+ })
66
+ })
67
+
68
+ describe('listAllMarkets', () => {
69
+ it('joins every page so markets past the first page are not lost', async () => {
70
+ const page = (number: number, pages: number) => ({
71
+ data: [market({ id: `mkt_${number}` })],
72
+ meta: {
73
+ page: number,
74
+ pages,
75
+ limit: 100,
76
+ count: pages,
77
+ from: 1,
78
+ to: 1,
79
+ in: 1,
80
+ previous: null,
81
+ next: null,
82
+ },
83
+ })
84
+ vi.mocked(adminClient.markets.list).mockImplementation(async (params) =>
85
+ page(params?.page ?? 1, 3),
86
+ )
87
+
88
+ const result = await listAllMarkets()
89
+
90
+ expect(result.data.map((record) => record.id)).toEqual(['mkt_1', 'mkt_2', 'mkt_3'])
91
+ expect(adminClient.markets.list).toHaveBeenCalledTimes(3)
92
+ })
93
+ })
@@ -0,0 +1,76 @@
1
+ import type { Market, PaginatedResponse } from '@spree/admin-sdk'
2
+ import type { CurrencyCode, LocaleCode } from '@spree/core'
3
+ import { adminClient } from '../client'
4
+
5
+ /**
6
+ * Markets and channels are store configuration that rarely changes, so the
7
+ * dashboard fetches each list once and keeps it for an hour. Their create,
8
+ * update and delete mutations invalidate `[resource, storeId]`, which refreshes
9
+ * the cached list immediately.
10
+ */
11
+ export const STORE_CONFIG_STALE_TIME = 60 * 60_000
12
+
13
+ const PAGE_SIZE = 100
14
+
15
+ /**
16
+ * Fetches every page of a list and joins them, so a store with more records
17
+ * than one page holds still gets all of them.
18
+ */
19
+ async function listEveryPage<T>(
20
+ fetchPage: (page: number) => Promise<PaginatedResponse<T>>,
21
+ ): Promise<PaginatedResponse<T>> {
22
+ const first = await fetchPage(1)
23
+ if (first.meta.pages <= 1) return first
24
+
25
+ const rest = await Promise.all(
26
+ Array.from({ length: first.meta.pages - 1 }, (_, index) => fetchPage(index + 2)),
27
+ )
28
+ return { ...first, data: [first, ...rest].flatMap((response) => response.data) }
29
+ }
30
+
31
+ /** Every market of the current store, under `['markets', storeId, 'all']`. */
32
+ export function listAllMarkets() {
33
+ return listEveryPage((page) =>
34
+ adminClient.markets.list({ page, limit: PAGE_SIZE, sort: 'position' }),
35
+ )
36
+ }
37
+
38
+ /** Every channel of the current store, under `['channels', storeId, 'all']`. */
39
+ export function listAllChannels() {
40
+ return listEveryPage((page) => adminClient.channels.list({ page, limit: PAGE_SIZE }))
41
+ }
42
+
43
+ export interface MarketLocalization {
44
+ /** The default market's currency first, then every other market's. */
45
+ currencies: CurrencyCode[]
46
+ /** Every language any market offers, sorted. */
47
+ locales: LocaleCode[]
48
+ defaultCurrency: CurrencyCode | undefined
49
+ defaultLocale: LocaleCode | undefined
50
+ }
51
+
52
+ /**
53
+ * The currencies and languages a store sells in, read from its markets. The
54
+ * default market's currency and language are the store's defaults.
55
+ */
56
+ export function marketLocalization(markets: readonly Market[]): MarketLocalization {
57
+ const defaultMarket = markets.find((market) => market.default) ?? markets[0]
58
+ const currencies = [
59
+ ...new Set([
60
+ ...(defaultMarket ? [defaultMarket.currency] : []),
61
+ ...markets.map((market) => market.currency),
62
+ ]),
63
+ ]
64
+ const locales = [
65
+ ...new Set(
66
+ markets.flatMap((market) => [market.default_locale, ...(market.supported_locales ?? [])]),
67
+ ),
68
+ ].sort()
69
+
70
+ return {
71
+ currencies,
72
+ locales,
73
+ defaultCurrency: defaultMarket?.currency,
74
+ defaultLocale: defaultMarket?.default_locale,
75
+ }
76
+ }
@@ -67,6 +67,11 @@ interface ColumnDefBase<T = any> {
67
67
  * can show human-readable names instead of raw prefixed IDs.
68
68
  */
69
69
  export interface ResourceFilterConfig<R extends { id: string } = { id: string }> {
70
+ /**
71
+ * Leads every cache entry the filter creates, ahead of the tenant. Naming
72
+ * it after the resource (`'markets'`) lets that resource's writes, which
73
+ * invalidate `[resource, tenant]`, refresh the filter's lists too.
74
+ */
70
75
  queryKey: string
71
76
  search: (query: string) => Promise<{ data: R[] }>
72
77
  hydrate: (ids: string[]) => Promise<{ data: R[] }>
@@ -75,12 +80,18 @@ export interface ResourceFilterConfig<R extends { id: string } = { id: string }>
75
80
  emptyText?: string
76
81
  /**
77
82
  * Fetches the whole list, for a resource small enough to show in full —
78
- * a store's warehouses, its channels. The control then opens as a plain
79
- * list of options instead of an empty box waiting to be typed into, and
80
- * still searches once the operator types. Leave it off for anything that
81
- * could run to hundreds of rows.
83
+ * a store's warehouses, its channels. It is fetched once and cached under
84
+ * `[queryKey, tenant, 'all']`; the filter narrows it as the operator types
85
+ * and labels its chips from it, without further requests. Use the same
86
+ * request as the resource's own full-list hook, which shares that key.
87
+ * Leave it off for anything that could run to hundreds of rows.
82
88
  */
83
89
  listAll?: () => Promise<{ data: R[] }>
90
+ /**
91
+ * How long fetched options stay fresh, in milliseconds. Raise it for
92
+ * rarely-changing resources whose writes invalidate `queryKey`.
93
+ */
94
+ staleTime?: number
84
95
  }
85
96
 
86
97
  /**
@@ -100,7 +111,7 @@ export type TaggableType = 'product' | 'customer' | 'order' | (string & {})
100
111
  */
101
112
  export type ColumnDef<T = any> =
102
113
  | (ColumnDefBase<T> & {
103
- filterType?: 'string' | 'boolean' | 'number' | 'date' | 'currency'
114
+ filterType?: 'string' | 'boolean' | 'number' | 'date' | 'currency' | 'locale'
104
115
  filterOptions?: never
105
116
  filterResource?: never
106
117
  taggableType?: never
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "admin": {
3
+ "address_select": {
4
+ "none": "لم يتم اختيار عنوان"
5
+ },
3
6
  "app_name": "Spree Admin",
4
7
  "a11y": {
5
8
  "breadcrumb": "مسار التنقل",
@@ -97,6 +100,7 @@
97
100
  "address_book": {
98
101
  "default_billing": "الفوترة الافتراضية",
99
102
  "default_shipping": "الشحن الافتراضي",
103
+ "inherited_from": "موروث من {{name}}",
100
104
  "set_default_billing": "تعيين كعنوان فوترة افتراضي",
101
105
  "set_default_shipping": "تعيين كعنوان شحن افتراضي"
102
106
  },
@@ -113,6 +117,7 @@
113
117
  "title": "إعادة تعيين كلمة المرور"
114
118
  },
115
119
  "login": {
120
+ "account_deactivated": "تم إلغاء تفعيل هذا الحساب. تواصل مع المسؤول لاستعادة الوصول.",
116
121
  "account_not_provisioned": "هذا الحساب غير مصرح له بالوصول إلى هذا المتجر. اطلب دعوة من المسؤول.",
117
122
  "divider": "أو",
118
123
  "invalid_oauth_state": "انتهت صلاحية جلسة تسجيل الدخول أو أنها غير صالحة. يرجى بدء تسجيل الدخول من جديد.",
@@ -483,6 +488,12 @@
483
488
  "discard_title": "تجاهل هذه المسودة؟"
484
489
  }
485
490
  },
491
+ "currency_amounts": {
492
+ "amount_in_currency": "المبلغ بعملة {{currency}}",
493
+ "help": "العملات المتروكة فارغة لا تُعرض لها طريقة التوصيل هذه.",
494
+ "label": "المبلغ",
495
+ "required": "أدخل مبلغًا بعملة واحدة على الأقل."
496
+ },
486
497
  "custom_field_definitions": {
487
498
  "errors": {
488
499
  "failed_to_create": "فشل إنشاء تعريف الحقل المخصص",
@@ -937,6 +948,14 @@
937
948
  "order_multiple": {
938
949
  "label": "مضاعف الطلب",
939
950
  "help": "كل زيادة فوق الحد الأدنى. اتركه فارغًا للسماح بأي كمية."
951
+ },
952
+ "store": {
953
+ "digital_asset_authorized_clicks": {
954
+ "label": "التنزيلات المسموح بها"
955
+ },
956
+ "digital_asset_authorized_days": {
957
+ "label": "الأيام المتاحة"
958
+ }
940
959
  }
941
960
  },
942
961
  "image_upload": {
@@ -2039,6 +2058,38 @@
2039
2058
  "description": "يجب على البائع تقديم عنوان للمرتجعات"
2040
2059
  }
2041
2060
  }
2061
+ },
2062
+ "digital_assets": {
2063
+ "add": "إضافة",
2064
+ "all_variants": "كل المتغيرات",
2065
+ "columns": {
2066
+ "file": "الملف",
2067
+ "variant": "المتغير",
2068
+ "size": "الحجم",
2069
+ "downloads": "التنزيلات",
2070
+ "days": "الأيام"
2071
+ },
2072
+ "delete_description": "لن يتمكن من اشترى «{{name}}» من تنزيله بعد الآن. يسري هذا فورًا.",
2073
+ "delete_title": "حذف هذا الملف؟",
2074
+ "description": "ملفات يمكن للمشترين تنزيلها بعد الشراء. أرفقها بمنتج رقمي، أو بمنتج مادي لتسليم دليل أو ضمان مع البضاعة.",
2075
+ "download": "تنزيل",
2076
+ "drop_hint": "اسحب الملفات وأفلتها هنا، أو انقر للتصفح",
2077
+ "empty": "لا توجد ملفات بعد",
2078
+ "limit_invalid": "يجب أن تكون الحدود أعدادًا صحيحة من 1 فأكثر، أو فارغة.",
2079
+ "limits_help": "اترك حدود التنزيل والأيام فارغة لاستخدام إعدادات المتجر.",
2080
+ "source": {
2081
+ "upload": "رفع ملف"
2082
+ },
2083
+ "store_default": "{{count}} (افتراضي المتجر)",
2084
+ "title": "الملفات الرقمية",
2085
+ "untitled": "ملف بدون اسم",
2086
+ "upload": "إضافة ملف",
2087
+ "upload_failed": "فشل رفع {{name}}: {{message}}",
2088
+ "uploading": "جارٍ رفع {{name}}…",
2089
+ "uploading_short": "جارٍ الرفع…",
2090
+ "replace": "استبدال الملف",
2091
+ "replace_description": "سيُحذف \"{{name}}\" وسيحمّل كل من اشتراه الملف الجديد من روابطه الحالية. يسري فورًا.",
2092
+ "replace_title": "استبدال هذا الملف؟"
2042
2093
  }
2043
2094
  }
2044
2095
  }
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "admin": {
3
+ "address_select": {
4
+ "none": "Keine Adresse ausgewählt"
5
+ },
3
6
  "app_name": "Spree Admin",
4
7
  "a11y": {
5
8
  "breadcrumb": "Breadcrumb",
@@ -97,6 +100,7 @@
97
100
  "address_book": {
98
101
  "default_billing": "Standard-Rechnung",
99
102
  "default_shipping": "Standard-Versand",
103
+ "inherited_from": "Geerbt von {{name}}",
100
104
  "set_default_billing": "Als Standard-Rechnungsadresse festlegen",
101
105
  "set_default_shipping": "Als Standard-Versandadresse festlegen"
102
106
  },
@@ -113,6 +117,7 @@
113
117
  "title": "Passwort zurücksetzen"
114
118
  },
115
119
  "login": {
120
+ "account_deactivated": "Dieses Konto wurde deaktiviert. Wenden Sie sich an einen Administrator, um den Zugang wiederherzustellen.",
116
121
  "account_not_provisioned": "Dieses Konto ist für diesen Shop nicht freigegeben. Bitten Sie einen Administrator um eine Einladung.",
117
122
  "divider": "oder",
118
123
  "invalid_oauth_state": "Die Anmeldesitzung ist abgelaufen oder ungültig. Bitte melden Sie sich erneut an.",
@@ -483,6 +488,12 @@
483
488
  "discard_title": "Entwurf verwerfen?"
484
489
  }
485
490
  },
491
+ "currency_amounts": {
492
+ "amount_in_currency": "Betrag in {{currency}}",
493
+ "help": "Währungen ohne Betrag wird diese Versandart nicht angeboten.",
494
+ "label": "Betrag",
495
+ "required": "Gib einen Betrag in mindestens einer Währung ein."
496
+ },
486
497
  "custom_field_definitions": {
487
498
  "errors": {
488
499
  "failed_to_create": "Benutzerdefiniertes Feld konnte nicht erstellt werden",
@@ -937,6 +948,14 @@
937
948
  "order_multiple": {
938
949
  "label": "Bestellvielfaches",
939
950
  "help": "Jeder Schritt über der Mindestmenge. Leer lassen für beliebige Mengen."
951
+ },
952
+ "store": {
953
+ "digital_asset_authorized_clicks": {
954
+ "label": "Erlaubte Downloads"
955
+ },
956
+ "digital_asset_authorized_days": {
957
+ "label": "Verfügbare Tage"
958
+ }
940
959
  }
941
960
  },
942
961
  "image_upload": {
@@ -2034,6 +2053,38 @@
2034
2053
  "description": "Der Verkäufer muss eine Rücksendeadresse angeben"
2035
2054
  }
2036
2055
  }
2056
+ },
2057
+ "digital_assets": {
2058
+ "add": "Hinzufügen",
2059
+ "all_variants": "Alle Varianten",
2060
+ "columns": {
2061
+ "file": "Datei",
2062
+ "variant": "Variante",
2063
+ "size": "Größe",
2064
+ "downloads": "Downloads",
2065
+ "days": "Tage"
2066
+ },
2067
+ "delete_description": "„{{name}}“ kann von niemandem mehr heruntergeladen werden, der sie gekauft hat. Dies wirkt sofort.",
2068
+ "delete_title": "Diese Datei löschen?",
2069
+ "description": "Dateien, die Käufer nach dem Kauf herunterladen können. Fügen Sie sie einem digitalen Produkt hinzu oder einem physischen Produkt, um eine Anleitung oder Garantie mitzuliefern.",
2070
+ "download": "Herunterladen",
2071
+ "drop_hint": "Dateien hierher ziehen und ablegen oder zum Durchsuchen klicken",
2072
+ "empty": "Noch keine Dateien",
2073
+ "limit_invalid": "Limits müssen ganze Zahlen ab 1 sein oder leer bleiben.",
2074
+ "limits_help": "Lassen Sie die Download- und Tageslimits leer, um die Einstellungen des Shops zu verwenden.",
2075
+ "source": {
2076
+ "upload": "Datei hochladen"
2077
+ },
2078
+ "store_default": "{{count}} (Shop-Standard)",
2079
+ "title": "Digitale Dateien",
2080
+ "untitled": "Unbenannte Datei",
2081
+ "upload": "Datei hinzufügen",
2082
+ "upload_failed": "Hochladen von {{name}} fehlgeschlagen: {{message}}",
2083
+ "uploading": "{{name}} wird hochgeladen…",
2084
+ "uploading_short": "Wird hochgeladen…",
2085
+ "replace": "Datei ersetzen",
2086
+ "replace_description": "„{{name}}“ wird entfernt, und alle Käufer laden über ihre bestehenden Links die neue Datei herunter. Dies gilt sofort.",
2087
+ "replace_title": "Diese Datei ersetzen?"
2037
2088
  }
2038
2089
  }
2039
2090
  }
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "admin": {
3
+ "address_select": {
4
+ "none": "No address selected"
5
+ },
3
6
  "app_name": "Spree Admin",
4
7
  "a11y": {
5
8
  "breadcrumb": "Breadcrumb",
@@ -97,6 +100,7 @@
97
100
  "address_book": {
98
101
  "default_billing": "Default billing",
99
102
  "default_shipping": "Default shipping",
103
+ "inherited_from": "Inherited from {{name}}",
100
104
  "set_default_billing": "Set as default billing",
101
105
  "set_default_shipping": "Set as default shipping"
102
106
  },
@@ -113,6 +117,7 @@
113
117
  "title": "Reset your password"
114
118
  },
115
119
  "login": {
120
+ "account_deactivated": "This account has been deactivated. Contact an administrator to restore access.",
116
121
  "account_not_provisioned": "This account is not authorized for this store. Ask an administrator for an invitation.",
117
122
  "divider": "or",
118
123
  "invalid_oauth_state": "That sign-in session expired or was invalid. Please start signing in again.",
@@ -485,6 +490,12 @@
485
490
  "discard_title": "Discard this draft?"
486
491
  }
487
492
  },
493
+ "currency_amounts": {
494
+ "amount_in_currency": "Amount in {{currency}}",
495
+ "help": "Currencies left blank are not offered this delivery method.",
496
+ "label": "Amount",
497
+ "required": "Enter an amount in at least one currency."
498
+ },
488
499
  "custom_field_definitions": {
489
500
  "errors": {
490
501
  "failed_to_create": "Failed to create custom field definition",
@@ -939,6 +950,14 @@
939
950
  "order_multiple": {
940
951
  "label": "Order multiple",
941
952
  "help": "Each step up from the minimum. Leave blank for any quantity."
953
+ },
954
+ "store": {
955
+ "digital_asset_authorized_clicks": {
956
+ "label": "Downloads allowed"
957
+ },
958
+ "digital_asset_authorized_days": {
959
+ "label": "Days available"
960
+ }
942
961
  }
943
962
  },
944
963
  "image_upload": {
@@ -2036,6 +2055,38 @@
2036
2055
  "description": "The seller must provide an address for returns"
2037
2056
  }
2038
2057
  }
2058
+ },
2059
+ "digital_assets": {
2060
+ "add": "Add",
2061
+ "all_variants": "All variants",
2062
+ "columns": {
2063
+ "file": "File",
2064
+ "variant": "Variant",
2065
+ "size": "Size",
2066
+ "downloads": "Downloads",
2067
+ "days": "Days"
2068
+ },
2069
+ "delete_description": "\"{{name}}\" will no longer be downloadable by anyone who bought it. This takes effect immediately.",
2070
+ "delete_title": "Delete this file?",
2071
+ "description": "Files buyers can download after purchase. Attach them to a digital product, or to a physical one to deliver a manual or warranty alongside the goods.",
2072
+ "download": "Download",
2073
+ "drop_hint": "Drag & drop files here, or click to browse",
2074
+ "empty": "No files yet",
2075
+ "limit_invalid": "Limits must be whole numbers of 1 or more, or blank.",
2076
+ "limits_help": "Leave the download and day limits blank to use the store's settings.",
2077
+ "source": {
2078
+ "upload": "Upload a file"
2079
+ },
2080
+ "store_default": "{{count}} (store default)",
2081
+ "title": "Digital files",
2082
+ "untitled": "Untitled file",
2083
+ "upload": "Add file",
2084
+ "upload_failed": "Failed to upload {{name}}: {{message}}",
2085
+ "uploading": "Uploading {{name}}…",
2086
+ "uploading_short": "Uploading…",
2087
+ "replace": "Replace file",
2088
+ "replace_description": "\"{{name}}\" is removed and everyone who bought it downloads the new file from their existing links. This takes effect immediately.",
2089
+ "replace_title": "Replace this file?"
2039
2090
  }
2040
2091
  }
2041
2092
  }
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "admin": {
3
+ "address_select": {
4
+ "none": "Ninguna dirección seleccionada"
5
+ },
3
6
  "components": {
4
7
  "address_block": {
5
8
  "not_provided": "No proporcionado"
@@ -298,6 +301,12 @@
298
301
  "discard_title": "¿Descartar este borrador?"
299
302
  }
300
303
  },
304
+ "currency_amounts": {
305
+ "amount_in_currency": "Monto en {{currency}}",
306
+ "help": "A las monedas dejadas en blanco no se les ofrece este método de entrega.",
307
+ "label": "Monto",
308
+ "required": "Introduce un monto en al menos una moneda."
309
+ },
301
310
  "pages": {
302
311
  "settings": {
303
312
  "stock_locations": {
@@ -967,6 +976,14 @@
967
976
  "order_multiple": {
968
977
  "label": "Múltiplo de pedido",
969
978
  "help": "Cada escalón por encima del mínimo. Dejalo en blanco para cualquier cantidad."
979
+ },
980
+ "store": {
981
+ "digital_asset_authorized_clicks": {
982
+ "label": "Descargas permitidas"
983
+ },
984
+ "digital_asset_authorized_days": {
985
+ "label": "Días disponibles"
986
+ }
970
987
  }
971
988
  },
972
989
  "products": {
@@ -1720,6 +1737,7 @@
1720
1737
  "title": "Restablecer tu contraseña"
1721
1738
  },
1722
1739
  "login": {
1740
+ "account_deactivated": "Esta cuenta fue desactivada. Contactá a un administrador para recuperar el acceso.",
1723
1741
  "account_not_provisioned": "Esta cuenta no está autorizada para esta tienda. Pedile una invitación a un administrador.",
1724
1742
  "divider": "o",
1725
1743
  "invalid_oauth_state": "Esa sesión de inicio caducó o fue inválida. Empezá a iniciar sesión de nuevo.",
@@ -2003,6 +2021,7 @@
2003
2021
  "address_book": {
2004
2022
  "default_billing": "Facturación predeterminada",
2005
2023
  "default_shipping": "Envío predeterminado",
2024
+ "inherited_from": "Heredada de {{name}}",
2006
2025
  "set_default_billing": "Establecer como facturación predeterminada",
2007
2026
  "set_default_shipping": "Establecer como envío predeterminado"
2008
2027
  },
@@ -2036,6 +2055,38 @@
2036
2055
  },
2037
2056
  "media_library": {
2038
2057
  "download": "Descargar"
2058
+ },
2059
+ "digital_assets": {
2060
+ "add": "Agregar",
2061
+ "all_variants": "Todas las variantes",
2062
+ "columns": {
2063
+ "file": "Archivo",
2064
+ "variant": "Variante",
2065
+ "size": "Tamaño",
2066
+ "downloads": "Descargas",
2067
+ "days": "Días"
2068
+ },
2069
+ "delete_description": "«{{name}}» ya no será descargable por nadie que lo haya comprado. Se aplica de inmediato.",
2070
+ "delete_title": "¿Eliminar este archivo?",
2071
+ "description": "Archivos que los compradores pueden descargar después de comprar. Adjuntalos a un producto digital, o a uno físico para entregar un manual o una garantía junto con la mercadería.",
2072
+ "download": "Descargar",
2073
+ "drop_hint": "Arrastrá y soltá los archivos acá, o hacé clic para explorar",
2074
+ "empty": "Todavía no hay archivos",
2075
+ "limit_invalid": "Los límites deben ser números enteros de 1 o más, o estar en blanco.",
2076
+ "limits_help": "Dejá los límites de descarga y de días en blanco para usar la config de la tienda.",
2077
+ "source": {
2078
+ "upload": "Subir un archivo"
2079
+ },
2080
+ "store_default": "{{count}} (predeterminado de la tienda)",
2081
+ "title": "Archivos digitales",
2082
+ "untitled": "Archivo sin título",
2083
+ "upload": "Agregar archivo",
2084
+ "upload_failed": "No se pudo subir {{name}}: {{message}}",
2085
+ "uploading": "Subiendo {{name}}…",
2086
+ "uploading_short": "Subiendo…",
2087
+ "replace": "Reemplazar archivo",
2088
+ "replace_description": "\"{{name}}\" se elimina y todos los compradores descargan el archivo nuevo desde sus enlaces existentes. Tiene efecto inmediato.",
2089
+ "replace_title": "¿Reemplazar este archivo?"
2039
2090
  }
2040
2091
  }
2041
2092
  }
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "admin": {
3
+ "address_select": {
4
+ "none": "Aucune adresse sélectionnée"
5
+ },
3
6
  "app_name": "Spree Admin",
4
7
  "a11y": {
5
8
  "breadcrumb": "Fil d'Ariane",
@@ -97,6 +100,7 @@
97
100
  "address_book": {
98
101
  "default_billing": "Facturation par défaut",
99
102
  "default_shipping": "Livraison par défaut",
103
+ "inherited_from": "Héritée de {{name}}",
100
104
  "set_default_billing": "Définir comme adresse de facturation par défaut",
101
105
  "set_default_shipping": "Définir comme adresse de livraison par défaut"
102
106
  },
@@ -113,6 +117,7 @@
113
117
  "title": "Réinitialiser votre mot de passe"
114
118
  },
115
119
  "login": {
120
+ "account_deactivated": "Ce compte a été désactivé. Contactez un administrateur pour rétablir l'accès.",
116
121
  "account_not_provisioned": "Ce compte n'est pas autorisé pour cette boutique. Demandez une invitation à un administrateur.",
117
122
  "divider": "ou",
118
123
  "invalid_oauth_state": "Cette session de connexion a expiré ou n'était pas valide. Veuillez recommencer.",
@@ -483,6 +488,12 @@
483
488
  "discard_title": "Abandonner ce brouillon ?"
484
489
  }
485
490
  },
491
+ "currency_amounts": {
492
+ "amount_in_currency": "Montant en {{currency}}",
493
+ "help": "Les devises laissées vides ne se voient pas proposer cette méthode de livraison.",
494
+ "label": "Montant",
495
+ "required": "Saisissez un montant dans au moins une devise."
496
+ },
486
497
  "custom_field_definitions": {
487
498
  "errors": {
488
499
  "failed_to_create": "Impossible de créer le champ personnalisé",
@@ -937,6 +948,14 @@
937
948
  "order_multiple": {
938
949
  "label": "Multiple de commande",
939
950
  "help": "Chaque palier au-dessus du minimum. Laisser vide pour toute quantité."
951
+ },
952
+ "store": {
953
+ "digital_asset_authorized_clicks": {
954
+ "label": "Téléchargements autorisés"
955
+ },
956
+ "digital_asset_authorized_days": {
957
+ "label": "Jours de disponibilité"
958
+ }
940
959
  }
941
960
  },
942
961
  "image_upload": {
@@ -2034,6 +2053,38 @@
2034
2053
  "description": "Le vendeur doit fournir une adresse pour les retours"
2035
2054
  }
2036
2055
  }
2056
+ },
2057
+ "digital_assets": {
2058
+ "add": "Ajouter",
2059
+ "all_variants": "Toutes les variantes",
2060
+ "columns": {
2061
+ "file": "Fichier",
2062
+ "variant": "Variante",
2063
+ "size": "Taille",
2064
+ "downloads": "Téléchargements",
2065
+ "days": "Jours"
2066
+ },
2067
+ "delete_description": "« {{name}} » ne sera plus téléchargeable par les personnes qui l'ont acheté. Effet immédiat.",
2068
+ "delete_title": "Supprimer ce fichier ?",
2069
+ "description": "Fichiers que les acheteurs peuvent télécharger après l'achat. Associez-les à un produit numérique, ou à un produit physique pour livrer un manuel ou une garantie.",
2070
+ "download": "Télécharger",
2071
+ "drop_hint": "Glissez-déposez des fichiers ici, ou cliquez pour parcourir",
2072
+ "empty": "Aucun fichier pour le moment",
2073
+ "limit_invalid": "Les limites doivent être des entiers supérieurs ou égaux à 1, ou vides.",
2074
+ "limits_help": "Laissez les limites de téléchargements et de jours vides pour utiliser les paramètres de la boutique.",
2075
+ "source": {
2076
+ "upload": "Téléverser un fichier"
2077
+ },
2078
+ "store_default": "{{count}} (par défaut)",
2079
+ "title": "Fichiers numériques",
2080
+ "untitled": "Fichier sans nom",
2081
+ "upload": "Ajouter un fichier",
2082
+ "upload_failed": "Échec du téléversement de {{name}} : {{message}}",
2083
+ "uploading": "Téléversement de {{name}}…",
2084
+ "uploading_short": "Téléversement…",
2085
+ "replace": "Remplacer le fichier",
2086
+ "replace_description": "« {{name}} » est supprimé et tous les acheteurs téléchargent le nouveau fichier depuis leurs liens existants. Effet immédiat.",
2087
+ "replace_title": "Remplacer ce fichier ?"
2037
2088
  }
2038
2089
  }
2039
2090
  }