payment-kit 1.13.45 → 1.13.46

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.
@@ -287,7 +287,7 @@ export function getFastCheckoutAmount(
287
287
  mode: string,
288
288
  currency: TPaymentCurrency,
289
289
  includeFreeTrial = false,
290
- minimumCycle = 2
290
+ minimumCycle = 1
291
291
  ) {
292
292
  if (minimumCycle < 1) {
293
293
  // eslint-disable-next-line no-param-reassign
@@ -1,4 +1,4 @@
1
- import { fromTokenToUnit } from '@ocap/util';
1
+ import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
@@ -114,20 +114,20 @@ router.get('/:id/upsell', auth, async (req, res) => {
114
114
  // update price
115
115
  // FIXME: upsell validate https://stripe.com/docs/payments/checkout/upsells
116
116
  router.put('/:id', auth, async (req, res) => {
117
- const price = await Price.findByPkOrLookupKey(req.params.id as string);
117
+ const doc = await Price.findByPkOrLookupKey(req.params.id as string);
118
118
 
119
- if (!price) {
119
+ if (!doc) {
120
120
  return res.status(404).json({ error: 'price not found' });
121
121
  }
122
122
 
123
- if (price.active === false) {
123
+ if (doc.active === false) {
124
124
  return res.status(403).json({ error: 'price archived' });
125
125
  }
126
126
 
127
127
  const updates: Partial<Price> = Price.formatBeforeSave(
128
128
  pick(
129
129
  req.body,
130
- price.locked
130
+ doc.locked
131
131
  ? ['nickname', 'description', 'metadata', 'currency_options', 'upsell']
132
132
  : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell'] // prettier-ignore
133
133
  )
@@ -135,38 +135,41 @@ router.put('/:id', auth, async (req, res) => {
135
135
 
136
136
  if (updates.lookup_key) {
137
137
  const exist = await Price.findOne({ where: { lookup_key: updates.lookup_key } });
138
- if (exist && exist.id !== price.id) {
138
+ if (exist && exist.id !== doc.id) {
139
139
  return res.status(400).json({ error: `lookup_key ${updates.lookup_key} already used by ${exist.id}` });
140
140
  }
141
141
  }
142
142
 
143
143
  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
144
- const currency = currencies.find((x) => x.id === price.currency_id);
144
+ const currency = currencies.find((x) => x.id === doc.currency_id);
145
145
  if (!currency) {
146
- return res.status(400).json({ error: `currency used in price not found or not active: ${price.currency_id}` });
146
+ return res.status(400).json({ error: `currency used in price not found or not active: ${doc.currency_id}` });
147
147
  }
148
148
  if (updates.unit_amount) {
149
149
  updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
150
+ if (updates.currency_options) {
151
+ const exist = updates.currency_options.find((x) => x.currency_id === doc.currency_id);
152
+ if (exist) {
153
+ exist.unit_amount = fromUnitToToken(updates.unit_amount as string, currency.decimal);
154
+ }
155
+ }
150
156
  }
151
157
  if (updates.currency_options) {
152
158
  updates.currency_options = Price.formatCurrencies(updates.currency_options, currencies);
153
- if (updates.currency_options.some((x) => x.currency_id === price.currency_id) === false) {
159
+ const index = updates.currency_options.findIndex((x) => x.currency_id === doc.currency_id);
160
+ if (index > -1) {
161
+ updates.unit_amount = updates.currency_options[index]?.unit_amount;
162
+ } else {
154
163
  updates.currency_options.unshift({
155
- currency_id: price.currency_id,
156
- unit_amount: price.unit_amount,
164
+ currency_id: doc.currency_id,
165
+ unit_amount: doc.unit_amount,
157
166
  tiers: null,
158
167
  custom_unit_amount: null,
159
168
  });
160
169
  }
161
- if (updates.unit_amount) {
162
- const base = price.currency_options.find((x) => x.currency_id === price.currency_id);
163
- if (base) {
164
- base.unit_amount = updates.unit_amount;
165
- }
166
- }
167
170
  }
168
171
 
169
- await price.update(Price.formatBeforeSave(updates));
172
+ await doc.update(Price.formatBeforeSave(updates));
170
173
 
171
174
  return res.json(await getExpandedPrice(req.params.id as string));
172
175
  });
@@ -344,9 +344,11 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
344
344
  },
345
345
  });
346
346
 
347
+ const currency = await PaymentCurrency.findOne({ where: { livemode: doc.livemode, is_base_currency: true } });
348
+
347
349
  raw.livemode = doc.livemode;
348
350
  raw.created_via = 'portal';
349
- raw.currency_id = req.currency.id;
351
+ raw.currency_id = currency?.id;
350
352
 
351
353
  if (req.query.redirect) {
352
354
  raw.success_url = req.query.redirect as string;
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.45
17
+ version: 1.13.46
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.45",
3
+ "version": "1.13.46",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev",
6
6
  "eject": "vite eject",
@@ -103,7 +103,7 @@
103
103
  "@abtnode/types": "1.16.17",
104
104
  "@arcblock/eslint-config": "^0.2.4",
105
105
  "@arcblock/eslint-config-ts": "^0.2.4",
106
- "@did-pay/types": "1.13.45",
106
+ "@did-pay/types": "1.13.46",
107
107
  "@types/cookie-parser": "^1.4.5",
108
108
  "@types/cors": "^2.8.15",
109
109
  "@types/dotenv-flow": "^3.3.2",
@@ -140,5 +140,5 @@
140
140
  "parser": "typescript"
141
141
  }
142
142
  },
143
- "gitHead": "6dacad185a8f180bbb63ec062aa196dc07c56535"
143
+ "gitHead": "7c37d3e6f4814056ddc98033b257707fbefba2d9"
144
144
  }
@@ -23,10 +23,10 @@ ProductItem.defaultProps = {
23
23
  };
24
24
 
25
25
  export default function ProductItem({ item, session, currency, mode, children, onUpsell, onDownsell }: Props) {
26
- const { t } = useLocaleContext();
27
- const pricing = formatLineItemPricing(item, currency, session.subscription_data?.trial_period_days || 0);
26
+ const { t, locale } = useLocaleContext();
27
+ const pricing = formatLineItemPricing(item, currency, session.subscription_data?.trial_period_days || 0, locale);
28
28
  const saving = formatUpsellSaving(session, currency);
29
- const metered = item.price?.recurring?.usage_type === 'metered' ? ' based on usage' : '';
29
+ const metered = item.price?.recurring?.usage_type === 'metered' ? t('common.metered') : '';
30
30
  const canUpsell = mode === 'normal' && session.line_items.length === 1;
31
31
  return (
32
32
  <Stack direction="column" alignItems="flex-start" spacing={1} sx={{ width: '100%' }}>
@@ -38,7 +38,7 @@ export default function ProductItem({ item, session, currency, mode, children, o
38
38
  description={item.price.product?.description}
39
39
  extra={
40
40
  item.price.type === 'recurring' && item.price.recurring
41
- ? [pricing.quantity, `billed ${formatRecurring(item.upsell_price?.recurring || item.price.recurring)} ${metered}`].filter(Boolean).join(', ') // prettier-ignore
41
+ ? [pricing.quantity, t('common.billed', { rule: `${formatRecurring(item.upsell_price?.recurring || item.price.recurring, true, 'per', locale)} ${metered}` })].filter(Boolean).join(', ') // prettier-ignore
42
42
  : pricing.quantity
43
43
  }
44
44
  />
@@ -71,12 +71,12 @@ export default function ProductItem({ item, session, currency, mode, children, o
71
71
  onChange={() => onUpsell(item.price_id, item.price.upsell?.upsells_to_id)}
72
72
  />
73
73
  {t('checkout.upsell.save', {
74
- recurring: t(`common.${formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring)}`),
74
+ recurring: formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring, true, 'per', locale),
75
75
  })}
76
76
  <Status label={t('checkout.upsell.off', { saving })} color="primary" variant="outlined" sx={{ ml: 1 }} />
77
77
  </Typography>
78
78
  <Typography component="span" sx={{ fontSize: 12 }}>
79
- {formatPrice(item.price.upsell.upsells_to, currency, item.price.product?.unit_label)}
79
+ {formatPrice(item.price.upsell.upsells_to, currency, item.price.product?.unit_label, 1, true, locale)}
80
80
  </Typography>
81
81
  </Stack>
82
82
  )}
@@ -102,7 +102,7 @@ export default function ProductItem({ item, session, currency, mode, children, o
102
102
  })}
103
103
  </Typography>
104
104
  <Typography component="span" sx={{ fontSize: 12 }}>
105
- {formatPrice(item.price, currency, item.price.product?.unit_label)}
105
+ {formatPrice(item.price, currency, item.price.product?.unit_label, 1, true, locale)}
106
106
  </Typography>
107
107
  </Stack>
108
108
  )}
@@ -41,10 +41,10 @@ export default function PaymentSummary({
41
41
  onApplyCrossSell,
42
42
  onCancelCrossSell,
43
43
  }: Props) {
44
- const { t } = useLocaleContext();
44
+ const { t, locale } = useLocaleContext();
45
45
  const [state, setState] = useSetState({ loading: false });
46
46
  const { data, runAsync } = useRequest(() => fetchCrossSell(checkoutSession.id));
47
- const headlines = formatCheckoutHeadlines(checkoutSession, currency);
47
+ const headlines = formatCheckoutHeadlines(checkoutSession, currency, locale);
48
48
 
49
49
  const handleUpsell = async (from: string, to: string) => {
50
50
  await onUpsell(from, to);
@@ -13,7 +13,7 @@ type Props = {
13
13
  };
14
14
 
15
15
  export default function CrossSellSelect({ data, onSelect }: Props) {
16
- const { t } = useLocaleContext();
16
+ const { t, locale } = useLocaleContext();
17
17
  const { products } = useProductsContext();
18
18
  const { settings } = useSettingsContext();
19
19
 
@@ -41,7 +41,7 @@ export default function CrossSellSelect({ data, onSelect }: Props) {
41
41
  <MenuItem key={x.id} value={x.id}>
42
42
  <InfoCard
43
43
  name={x.name}
44
- description={formatProductPrice(x as any, settings.baseCurrency)}
44
+ description={formatProductPrice(x as any, settings.baseCurrency, locale)}
45
45
  logo={x.images[0]}
46
46
  />
47
47
  </MenuItem>
@@ -14,6 +14,7 @@ import InfoRow from '../info-row';
14
14
  import CrossSellSelect from './cross-sell-select';
15
15
 
16
16
  export function CrossSellForm({ data, onChange }: { data: TProductExpanded; onChange: Function }) {
17
+ const { locale } = useLocaleContext();
17
18
  const { settings } = useSettingsContext();
18
19
  const [state, setState] = useSetState({
19
20
  loading: false,
@@ -55,7 +56,7 @@ export function CrossSellForm({ data, onChange }: { data: TProductExpanded; onCh
55
56
  <Stack spacing={1} direction="row" alignItems="center">
56
57
  <InfoCard
57
58
  name={to.name}
58
- description={formatProductPrice(to as any, settings.baseCurrency)}
59
+ description={formatProductPrice(to as any, settings.baseCurrency, locale)}
59
60
  logo={to.images[0]}
60
61
  />
61
62
  <IconButton size="small" sx={{ ml: 1 }} onClick={onRemoveUpsell}>
package/src/libs/util.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-nested-ternary */
1
2
  /* eslint-disable @typescript-eslint/indent */
2
3
  import type {
3
4
  LineItem,
@@ -17,6 +18,7 @@ import cloneDeep from 'lodash/cloneDeep';
17
18
  import isEqual from 'lodash/isEqual';
18
19
  import { defaultCountries } from 'react-international-phone';
19
20
 
21
+ import { t } from '../locales/index';
20
22
  import dayjs from './dayjs';
21
23
 
22
24
  export function getExplorerLink(chainHost: string, did: string, type: string) {
@@ -119,17 +121,18 @@ export const formatError = (err: any) => {
119
121
 
120
122
  export const formatProductPrice = (
121
123
  { prices, unit_label }: { prices: TPrice[]; unit_label: string },
122
- currency: TPaymentCurrency
124
+ currency: TPaymentCurrency,
125
+ locale: string = 'en'
123
126
  ): string => {
124
127
  if (prices.length > 1) {
125
- return `${prices.length} prices`;
128
+ return t('admin.price.count', locale, { count: prices.length });
126
129
  }
127
130
 
128
131
  if (prices.length === 1) {
129
- return formatPrice(prices[0] as TPrice, currency, unit_label);
132
+ return formatPrice(prices[0] as TPrice, currency, unit_label, 1, true, locale);
130
133
  }
131
134
 
132
- return 'No price';
135
+ return t('admin.price.empty', locale);
133
136
  };
134
137
 
135
138
  export const formatPrice = (
@@ -137,14 +140,15 @@ export const formatPrice = (
137
140
  currency: TPaymentCurrency,
138
141
  unit_label?: string,
139
142
  quantity: number = 1,
140
- bn: boolean = true
143
+ bn: boolean = true,
144
+ locale: string = 'en'
141
145
  ) => {
142
146
  const unit = getPriceUintAmountByCurrency(price, currency);
143
147
  const amount = bn
144
148
  ? fromUnitToToken(new BN(unit).mul(new BN(quantity)), currency.decimal).toString()
145
149
  : +unit * quantity;
146
150
  if (price?.type === 'recurring' && price.recurring) {
147
- const recurring = formatRecurring(price.recurring, false, '/');
151
+ const recurring = formatRecurring(price.recurring, false, 'slash', locale);
148
152
 
149
153
  if (unit_label) {
150
154
  return `${amount} ${currency.symbol} / ${unit_label} ${recurring}`;
@@ -219,7 +223,12 @@ export function getStatementDescriptor(items: any[]) {
219
223
  return window.blocklet.appName;
220
224
  }
221
225
 
222
- export function formatRecurring(recurring: PriceRecurring, translate: boolean = true, separator = 'per') {
226
+ export function formatRecurring(
227
+ recurring: PriceRecurring,
228
+ translate: boolean = true,
229
+ separator: string = 'per',
230
+ locale: string = 'en'
231
+ ) {
223
232
  const intervals = {
224
233
  hour: 'hourly',
225
234
  day: 'daily',
@@ -229,11 +238,15 @@ export function formatRecurring(recurring: PriceRecurring, translate: boolean =
229
238
  };
230
239
 
231
240
  if (+recurring.interval_count === 1) {
241
+ const interval = t(`common.${recurring.interval}`, locale);
232
242
  // @ts-ignore
233
- return translate ? intervals[recurring.interval] : `${separator} ${recurring.interval}`;
243
+ return translate ? t(`common.${intervals[recurring.interval]}`, locale) : separator ? t(`common.${separator}`, locale, { interval }) : interval; // prettier-ignore
234
244
  }
235
245
 
236
- return `every ${recurring.interval_count} ${recurring.interval}s`;
246
+ return t('common.recurring', locale, {
247
+ count: recurring.interval_count,
248
+ interval: t(`common.${recurring.interval}s`, locale),
249
+ });
237
250
  }
238
251
 
239
252
  export function getPriceUintAmountByCurrency(price: TPrice, currency: TPaymentCurrency) {
@@ -257,11 +270,12 @@ export function getPriceCurrencyOptions(price: TPrice): PriceCurrency[] {
257
270
  export function formatLineItemPricing(
258
271
  item: TLineItemExpanded,
259
272
  currency: TPaymentCurrency,
260
- trial: number
273
+ trial: number,
274
+ locale: string = 'en'
261
275
  ): { primary: string; secondary?: string; quantity: string } {
262
276
  const price = item.upsell_price || item.price;
263
277
 
264
- let quantity = `Qty ${item.quantity}`;
278
+ let quantity = t('common.qty', locale, { count: item.quantity });
265
279
  if (price.recurring?.usage_type === 'metered' || +item.quantity === 1) {
266
280
  quantity = '';
267
281
  }
@@ -274,20 +288,20 @@ export function formatLineItemPricing(
274
288
 
275
289
  const appendUnit = (v: string, alt: string) => {
276
290
  if (price.product.unit_label) {
277
- return `${v} / ${price.product.unit_label}`;
291
+ return `${v}/${price.product.unit_label}`;
278
292
  }
279
293
  if (price.recurring?.usage_type === 'metered' || item.quantity === 1) {
280
294
  return alt;
281
295
  }
282
296
 
283
- return quantity ? `${unit} each` : '';
297
+ return quantity ? t('common.each', locale, { unit }) : '';
284
298
  };
285
299
 
286
300
  if (price.type === 'recurring' && price.recurring) {
287
301
  if (trial > 0) {
288
302
  return {
289
- primary: `Free for ${trial} days`,
290
- secondary: `${appendUnit(total, total)} ${formatRecurring(price.recurring, false, '/')}`,
303
+ primary: t('common.trial', locale, { count: trial }),
304
+ secondary: `${appendUnit(total, total)} ${formatRecurring(price.recurring, false, 'slash', locale)}`,
291
305
  quantity,
292
306
  };
293
307
  }
@@ -408,7 +422,8 @@ export function formatUpsellSaving(session: TCheckoutSessionExpanded, currency:
408
422
 
409
423
  export function formatCheckoutHeadlines(
410
424
  session: TCheckoutSessionExpanded,
411
- currency: TPaymentCurrency
425
+ currency: TPaymentCurrency,
426
+ locale: string = 'en'
412
427
  ): {
413
428
  action: string;
414
429
  amount: string;
@@ -425,7 +440,7 @@ export function formatCheckoutHeadlines(
425
440
  // empty
426
441
  if (items.length === 0) {
427
442
  return {
428
- action: 'No thing to pay',
443
+ action: t('checkout.empty', locale),
429
444
  amount: '0',
430
445
  then: '',
431
446
  };
@@ -435,21 +450,27 @@ export function formatCheckoutHeadlines(
435
450
 
436
451
  // all one time
437
452
  if (items.every((x) => x.price.type === 'one_time')) {
453
+ const action = t('checkout.pay', locale, { payee: brand });
438
454
  if (items.length > 1) {
439
- return { action: `Pay ${brand}`, amount };
455
+ return { action, amount };
440
456
  }
441
457
 
442
- return { action: `Pay ${brand}`, amount, then: '' };
458
+ return { action, amount, then: '' };
443
459
  }
444
460
 
445
461
  const item = items.find((x) => x.price.type === 'recurring');
446
- const recurring = formatRecurring((item?.upsell_price || item?.price)?.recurring as PriceRecurring, false);
462
+ const recurring = formatRecurring(
463
+ (item?.upsell_price || item?.price)?.recurring as PriceRecurring,
464
+ false,
465
+ 'per',
466
+ locale
467
+ );
447
468
 
448
469
  // all recurring
449
470
  if (items.every((x) => x.price.type === 'recurring')) {
450
471
  const hasMetered = items.some((x) => x.price.type === 'recurring' && x.price.recurring?.usage_type === 'metered');
451
472
  const subscription = [
452
- hasMetered ? 'continue with at least' : '',
473
+ hasMetered ? t('checkout.least', locale) : '',
453
474
  fromUnitToToken(
454
475
  items.reduce((acc, x) => {
455
476
  if (x.price.recurring?.usage_type === 'metered') {
@@ -466,14 +487,14 @@ export function formatCheckoutHeadlines(
466
487
  if (items.length > 1) {
467
488
  if (trial > 0) {
468
489
  return {
469
- action: `Try ${name} and ${items.length - 1} more`,
470
- amount: `Free for ${trial} days`,
471
- then: `Then ${subscription} ${recurring}`,
490
+ action: t('checkout.try2', locale, { name, count: items.length - 1 }),
491
+ amount: t('checkout.free', locale, { count: trial }),
492
+ then: t('checkout.then', locale, { subscription, recurring }),
472
493
  };
473
494
  }
474
495
 
475
496
  return {
476
- action: `Subscribe to ${name} and ${items.length - 1} more`,
497
+ action: t('checkout.sub2', locale, { name, count: items.length - 1 }),
477
498
  amount,
478
499
  then: recurring,
479
500
  };
@@ -481,14 +502,14 @@ export function formatCheckoutHeadlines(
481
502
 
482
503
  if (trial > 0) {
483
504
  return {
484
- action: `Try ${name}`,
485
- amount: `${trial} days free`,
486
- then: `Then ${subscription} ${recurring}`,
505
+ action: t('checkout.try1', locale, { name }),
506
+ amount: t('checkout.free', locale, { count: trial }),
507
+ then: t('checkout.then', locale, { subscription, recurring }),
487
508
  };
488
509
  }
489
510
 
490
511
  return {
491
- action: `Subscribe to ${name}`,
512
+ action: t('checkout.sub1', locale, { name }),
492
513
  amount,
493
514
  then: recurring,
494
515
  };
@@ -508,9 +529,9 @@ export function formatCheckoutHeadlines(
508
529
  );
509
530
 
510
531
  return {
511
- action: `Pay ${brand}`,
532
+ action: t('checkout.pay', locale, { payee: brand }),
512
533
  amount,
513
- then: `Then ${subscription} ${currency.symbol} ${recurring}`,
534
+ then: t('checkout.then', locale, { subscription: `${subscription} ${currency.symbol}`, recurring }),
514
535
  };
515
536
  }
516
537
 
@@ -28,7 +28,8 @@ export default flat({
28
28
  confirm: 'Confirm',
29
29
  cancel: 'Cancel',
30
30
  every: 'every',
31
- per: 'per',
31
+ per: 'per {interval}',
32
+ slash: '/ {interval}',
32
33
  unit: 'units',
33
34
  edit: 'Edit',
34
35
  quantity: 'Quantity',
@@ -47,16 +48,29 @@ export default flat({
47
48
  copied: 'Copied',
48
49
  previous: 'Back',
49
50
  continue: 'Continue',
50
- hourly: 'Hourly',
51
- daily: 'Daily',
52
- weekly: 'Weekly',
53
- monthly: 'Monthly',
54
- yearly: 'Yearly',
55
- month3: 'Every 3 months',
56
- month6: 'Every 6 months',
57
- days: 'Days',
58
- weeks: 'Weeks',
59
- months: 'Months',
51
+ qty: 'Qty {count}',
52
+ each: '{unit} each',
53
+ trial: 'Free for {count} days',
54
+ billed: 'billed {rule}',
55
+ metered: 'based on usage',
56
+ hour: 'hour',
57
+ day: 'day',
58
+ week: 'week',
59
+ month: 'month',
60
+ year: 'year',
61
+ hourly: 'hourly',
62
+ daily: 'daily',
63
+ weekly: 'weekly',
64
+ monthly: 'monthly',
65
+ yearly: 'yearly',
66
+ month3: 'every 3 months',
67
+ month6: 'every 6 months',
68
+ recurring: 'every {count} {interval}',
69
+ hours: 'hours',
70
+ days: 'days',
71
+ weeks: 'weeks',
72
+ months: 'months',
73
+ years: 'years',
60
74
  metadata: {
61
75
  label: 'Metadata',
62
76
  add: 'Add more metadata',
@@ -145,6 +159,8 @@ export default flat({
145
159
  name: 'Price',
146
160
  type: 'Usage type',
147
161
  info: 'Price information',
162
+ count: '{count} prices',
163
+ empty: 'No price',
148
164
  lookupKey: 'Lookup key',
149
165
  setAsDefault: 'Set as default price',
150
166
  detail: 'Pricing details',
@@ -496,6 +512,15 @@ export default flat({
496
512
  login: 'Login to load and save contact information',
497
513
  portal: 'Manage subscriptions',
498
514
  cardPay: '{action} with card',
515
+ empty: 'No thing to pay',
516
+ pay: 'Pay {payee}',
517
+ try1: 'Try {name}',
518
+ try2: 'Try {name} and {count} more',
519
+ sub1: 'Subscribe to {name}',
520
+ sub2: 'Subscribe to {name} and {count} more',
521
+ then: 'Then {subscription} {recurring}',
522
+ free: '{count} days free',
523
+ least: 'continue with at least',
499
524
  completed: {
500
525
  payment: 'Thanks for your purchase',
501
526
  subscription: 'Thanks for your subscribing',
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-prototype-builtins */
1
2
  import en from './en';
2
3
  import zh from './zh';
3
4
 
@@ -6,3 +7,27 @@ export const translations = {
6
7
  zh,
7
8
  en,
8
9
  };
10
+
11
+ export const replace = (template: string, data: Record<string, any> = {}) =>
12
+ template.replace(/{(\w*)}/g, (_, key) => (data.hasOwnProperty(key) ? data[key] : ''));
13
+
14
+ export const createTranslator = ({ fallbackLocale = 'en' }: { fallbackLocale?: string }) => {
15
+ return (key: string, locale = fallbackLocale, data: Record<string, any> = {}) => {
16
+ // @ts-ignore
17
+ if (!translations[locale] || !translations[locale][key]) {
18
+ // @ts-ignore
19
+ if (fallbackLocale && translations[fallbackLocale]?.[key]) {
20
+ // @ts-ignore
21
+ return replace(translations[fallbackLocale]?.[key], data);
22
+ }
23
+
24
+ return key;
25
+ }
26
+
27
+ // @ts-ignore
28
+ return replace(translations[locale][key], data);
29
+ };
30
+ };
31
+
32
+ export const translate = createTranslator({ fallbackLocale: 'en' });
33
+ export const t = translate;
@@ -28,8 +28,9 @@ export default flat({
28
28
  confirm: '确认',
29
29
  cancel: '取消',
30
30
  every: '每',
31
- per: '每',
32
- unit: '个单位',
31
+ per: '每{interval}',
32
+ slash: '每{interval}',
33
+ unit: '件',
33
34
  edit: '编辑',
34
35
  quantity: '数量',
35
36
  yes: '是',
@@ -47,16 +48,29 @@ export default flat({
47
48
  copied: '已复制',
48
49
  previous: '返回',
49
50
  continue: '继续',
50
- hourly: '每小时',
51
- daily: '每日',
52
- weekly: '每周',
53
- monthly: '每月',
54
- yearly: '每年',
55
- month3: '每3个月',
56
- month6: '每6个月',
51
+ qty: '{count} 件',
52
+ each: '每件 {unit}',
53
+ trial: '免费试用 {count} 天',
54
+ billed: '{rule}收费',
55
+ metered: '按用量',
56
+ hour: '小时',
57
+ day: '',
58
+ week: '周',
59
+ month: '月',
60
+ year: '年',
61
+ hourly: '按小时',
62
+ daily: '按天',
63
+ weekly: '按周',
64
+ monthly: '按月',
65
+ yearly: '按年',
66
+ month3: '按季度',
67
+ month6: '按半年',
68
+ recurring: '每{count}{interval}',
69
+ hours: '小时',
57
70
  days: '天',
58
71
  weeks: '周',
59
72
  months: '月',
73
+ years: '年',
60
74
  metadata: {
61
75
  label: '元数据',
62
76
  add: '添加更多元数据',
@@ -488,6 +502,16 @@ export default flat({
488
502
  login: '登录以加载并保存联系信息',
489
503
  portal: '管理订阅',
490
504
  cardPay: '使用卡片{action}',
505
+ empty: '没有可支付的项目',
506
+ per: '每',
507
+ pay: '付款给 {payee}',
508
+ try1: '免费试用 {name}',
509
+ try2: '免费试用 {name} 等{count}个产品',
510
+ sub1: '订阅 {name}',
511
+ sub2: '订阅 {name} 等{count}个产品',
512
+ then: '然后 {subscription} {recurring}',
513
+ free: '{count} 天',
514
+ least: '至少',
491
515
  completed: {
492
516
  payment: '感谢您的购买',
493
517
  subscription: '感谢您的订阅',
@@ -158,7 +158,8 @@ export default function PaymentLinkDetail(props: { id: string }) {
158
158
  description={formatProductPrice(
159
159
  // @ts-ignore
160
160
  { ...item.price.product, prices: [item.price] },
161
- settings.baseCurrency
161
+ settings.baseCurrency,
162
+ locale
162
163
  )}
163
164
  logo={item.price.product.images[0]}
164
165
  />
@@ -27,7 +27,7 @@ const fetchData = (id: string): Promise<TPriceExpanded> => {
27
27
  };
28
28
 
29
29
  export default function PriceDetail(props: { id: string }) {
30
- const { t } = useLocaleContext();
30
+ const { t, locale } = useLocaleContext();
31
31
  const navigate = useNavigate();
32
32
  const [state, setState] = useSetState({
33
33
  adding: {
@@ -121,7 +121,11 @@ export default function PriceDetail(props: { id: string }) {
121
121
  value={<Link to={`/admin/products/${data.product_id}`}>{data.product.name}</Link>}
122
122
  divider
123
123
  />
124
- <InfoMetric label={t('admin.price.amount')} value={formatPrice(data, data.currency)} divider />
124
+ <InfoMetric
125
+ label={t('admin.price.amount')}
126
+ value={formatPrice(data, data.currency, data.product.unit_label, 1, true, locale)}
127
+ divider
128
+ />
125
129
  <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
126
130
  <InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
127
131
  </Stack>
@@ -221,7 +225,11 @@ export default function PriceDetail(props: { id: string }) {
221
225
  const item = data.currency_options[index] as any;
222
226
  return formatPrice(
223
227
  { type: data.type, unit_amount: item.unit_amount, recurring: data.recurring } as TPrice,
224
- item.currency
228
+ item.currency,
229
+ data.product.unit_label,
230
+ 1,
231
+ true,
232
+ locale
225
233
  );
226
234
  },
227
235
  },
@@ -13,7 +13,7 @@ import { formatPrice, formatTime } from '../../../../libs/util';
13
13
  import PriceActions from './actions';
14
14
 
15
15
  export default function PricesList({ product, onChange }: { product: Product; onChange: Function }) {
16
- const { t } = useLocaleContext();
16
+ const { t, locale } = useLocaleContext();
17
17
  const { settings } = useSettingsContext();
18
18
 
19
19
  const columns = [
@@ -32,7 +32,9 @@ export default function PricesList({ product, onChange }: { product: Product; on
32
32
  <LockOutlined sx={{ color: 'text.secondary' }} />
33
33
  </Tooltip>
34
34
  )}
35
- <Typography component="span">{formatPrice(price, settings.baseCurrency)}</Typography>
35
+ <Typography component="span">
36
+ {formatPrice(price, settings.baseCurrency, '', 1, true, locale)}
37
+ </Typography>
36
38
  <Typography component="span">
37
39
  {price.id === product.default_price_id && <Status label="default" color="info" sx={{ height: 18 }} />}
38
40
  </Typography>
@@ -138,7 +138,8 @@ export default function PricingTableDetail(props: { id: string }) {
138
138
  description={formatProductPrice(
139
139
  // @ts-ignore
140
140
  { ...item.product, prices: [item.price] },
141
- settings.baseCurrency
141
+ settings.baseCurrency,
142
+ locale
142
143
  )}
143
144
  logo={item.product.images[0]}
144
145
  />
@@ -18,7 +18,7 @@ import api from '../../../../libs/api';
18
18
  import { formatError, formatPrice } from '../../../../libs/util';
19
19
 
20
20
  export default function ProductsCreate() {
21
- const { t } = useLocaleContext();
21
+ const { t, locale } = useLocaleContext();
22
22
  const { settings } = useSettingsContext();
23
23
 
24
24
  const methods = useForm<Product>({
@@ -85,7 +85,7 @@ export default function ProductsCreate() {
85
85
  }
86
86
 
87
87
  // @ts-ignore
88
- return formatPrice(getPrice(index), settings.baseCurrency, getValues().unit_label, 1, false);
88
+ return formatPrice(getPrice(index), settings.baseCurrency, getValues().unit_label, 1, false, locale);
89
89
  }}>
90
90
  <PriceForm prefix={`prices.${index}`} />
91
91
  </Collapse>
@@ -29,7 +29,7 @@ const getProduct = (id: string): Promise<TProductExpanded> => {
29
29
  };
30
30
 
31
31
  export default function ProductDetail(props: { id: string }) {
32
- const { t } = useLocaleContext();
32
+ const { t, locale } = useLocaleContext();
33
33
  const navigate = useNavigate();
34
34
  const { settings } = useSettingsContext();
35
35
  const [state, setState] = useSetState({
@@ -128,7 +128,7 @@ export default function ProductDetail(props: { id: string }) {
128
128
  logo={data.images[0]}
129
129
  name={data.name}
130
130
  // @ts-ignore
131
- description={formatProductPrice(data, settings.baseCurrency)}
131
+ description={formatProductPrice(data, settings.baseCurrency, locale)}
132
132
  />
133
133
  <ProductActions data={data} onChange={onChange} variant="normal" />
134
134
  </Stack>
@@ -28,7 +28,7 @@ export default function ProductsList() {
28
28
  const listKey = 'products';
29
29
  const persisted = getDurableData(listKey);
30
30
 
31
- const { t } = useLocaleContext();
31
+ const { t, locale } = useLocaleContext();
32
32
  const navigate = useNavigate();
33
33
  const { settings } = useSettingsContext();
34
34
  const [search, setSearch] = useState<{ active: string; pageSize: number; page: number }>({
@@ -62,7 +62,7 @@ export default function ProductsList() {
62
62
  return (
63
63
  <InfoCard
64
64
  name={product.name}
65
- description={formatProductPrice(product as any, settings.baseCurrency)}
65
+ description={formatProductPrice(product as any, settings.baseCurrency, locale)}
66
66
  logo={product.images[0]}
67
67
  />
68
68
  );
@@ -58,7 +58,7 @@ const groupItemsByRecurring = (items: TPricingTableItem[]) => {
58
58
  };
59
59
 
60
60
  export default function PricingTable({ id }: Props) {
61
- const { t } = useLocaleContext();
61
+ const { t, locale } = useLocaleContext();
62
62
  const [params] = useSearchParams();
63
63
  const { error, loading, data } = useRequest(() => fetchData(id));
64
64
  const [state, setState] = useSetState({ interval: '', loading: '' });
@@ -149,7 +149,7 @@ export default function PricingTable({ id }: Props) {
149
149
  <ToggleButtonGroup value={state.interval} onChange={(_, value) => setState({ interval: value })} exclusive>
150
150
  {Object.keys(recurring).map((x) => (
151
151
  <ToggleButton key={x} value={x} sx={{ textTransform: 'capitalize' }}>
152
- {formatRecurring(recurring[x] as PriceRecurring)}
152
+ {formatRecurring(recurring[x] as PriceRecurring, true, '', locale)}
153
153
  </ToggleButton>
154
154
  ))}
155
155
  </ToggleButtonGroup>
@@ -186,10 +186,10 @@ export default function PricingTable({ id }: Props) {
186
186
  <PaymentAmount amount={formatPriceAmount(x.price, data.currency, x.product.unit_label)} />
187
187
  <Stack direction="column" alignItems="flex-start">
188
188
  <Typography component="span" color="text.secondary" fontSize="0.8rem">
189
- per
189
+ {t('checkout.per')}
190
190
  </Typography>
191
191
  <Typography component="span" color="text.secondary" fontSize="0.8rem">
192
- {formatRecurring(x.price.recurring as PriceRecurring, false, '')}
192
+ {formatRecurring(x.price.recurring as PriceRecurring, false, '', locale)}
193
193
  </Typography>
194
194
  </Stack>
195
195
  </Stack>