diff --git a/docs/content/docs/en/config/clients.mdx b/docs/content/docs/en/config/clients.mdx index 05c7df5df..fa62c28c0 100644 --- a/docs/content/docs/en/config/clients.mdx +++ b/docs/content/docs/en/config/clients.mdx @@ -75,6 +75,16 @@ explicit button can set the first calendar cutoff. Selecting a mode alone never rewrites an existing expiry. First-use clients keep their initial duration, and their calendar dates are available after activation. +Preview dates use the Gregorian format `YYYY-MM-DD HH:mm:ss UTC±HH:mm`, +preserving any fractional seconds in the cutoff. Each date includes its own +UTC offset, so a daylight-saving change can show different offsets before and +after renewal. The heading identifies the panel time zone; **Server local time** +means the server's `Local` setting, not the browser's time zone. For example, +`2026-09-27 05:00:00 UTC+00:00` is 13:00 in UTC+08:00. **Estimated renewals used** +is the allowance this preview would consume, including any catch-up periods, +not a count of renewals already performed. The first-cutoff button uses the same +date format; clicking it still requires saving the form to persist the change. + For legacy last-second calendar cutoffs, the renewal boundary includes the existing free alignment to the following midnight. The last-valid-second preview still uses the **stored expiry**, not that alignment: an exclusive diff --git a/docs/content/docs/zh/config/clients.mdx b/docs/content/docs/zh/config/clients.mdx index e395adedc..9814c9fbf 100644 --- a/docs/content/docs/zh/config/clients.mdx +++ b/docs/content/docs/zh/config/clients.mdx @@ -67,6 +67,14 @@ icon: Users 明确点击按钮设置首次日历截止时间;仅选择模式不会修改已有到期时间。 “首次使用后开始”保留原来的初始天数,激活后才能确定日历日期。 +预览日期使用公历格式 `YYYY-MM-DD HH:mm:ss UTC±HH:mm`,截止时间中的小数秒 +会原样保留。每个日期分别标注 UTC 偏移,因此跨夏令时续期前后的偏移可能不同。 +标题注明面板时区;**服务器本地时区**指服务器的 `Local` 设置,不是浏览器时区。 +例如 `2026-09-27 05:00:00 UTC+00:00` 对应 UTC+08:00 的 13:00。 +**预计消耗续期次数**是本次预览预计使用的额度,包含补续周期,不是已经执行的 +续期次数。设置首个周期截止时间的按钮使用同样的日期格式;点击后仍需保存表单 +才会持久化修改。 + 旧配置以最后一秒为日历截止时间时,续期边界包含原有的不计次数向下个 零点对齐规则。但最后有效秒仍按**已存储的到期时间**计算,不会假装 初始时间已被修改:排他截止时间 `23:59:59` 实际有效至 `23:59:58`。 diff --git a/frontend/src/pages/clients/ClientRenewalFields.tsx b/frontend/src/pages/clients/ClientRenewalFields.tsx index 006fcfdcb..f3215a767 100644 --- a/frontend/src/pages/clients/ClientRenewalFields.tsx +++ b/frontend/src/pages/clients/ClientRenewalFields.tsx @@ -12,6 +12,19 @@ import type { ClientFormValues } from '@/schemas/client'; type RenewalFields = Pick; type RenewalMode = 'none' | 'interval' | 'weekly' | 'monthly'; +function RenewalDate({ value }: { value: string }) { + // Keep the server's wall time and per-date offset, including across DST. + // Parsing into a browser-local Date would show a different renewal boundary. + const display = value + .replace('T', ' ') + .replace(/(Z|[+-]\d{2}:\d{2})$/, (offset) => ` UTC${offset === 'Z' ? '+00:00' : offset}`); + return ( + + ); +} + export default function ClientRenewalFields({ active, expiryTime, @@ -146,9 +159,17 @@ export default function ClientRenewalFields({ )} {preview && ( - + - {t('pages.clients.renewPreview', { zone: preview.timeZone })} + {t('pages.clients.renewPreview', { + zone: + preview.timeZone === 'Local' + ? t('pages.clients.renewServerLocal') + : preview.timeZone, + })} + + + {t('pages.clients.renewPreviewHint')} {delayedStart || preview.delayedStart ? ( @@ -160,22 +181,30 @@ export default function ClientRenewalFields({ {t('pages.clients.renewNeedsExpiry')} {preview.suggestedExpiryTime > 0 && ( - )} ) : ( <> - {t('pages.clients.renewAt')}: {preview.renewAt} + {t('pages.clients.renewAt')}: - {t('pages.clients.renewValidThrough')}: {preview.validThrough} + {t('pages.clients.renewValidThrough')}:{' '} + {preview.nextExpiry && ( - {t('pages.clients.renewNextExpiry')}: {preview.nextExpiry} + {t('pages.clients.renewNextExpiry')}:{' '} + )} diff --git a/frontend/src/test/client-bulk-calendar-renewal.test.tsx b/frontend/src/test/client-bulk-calendar-renewal.test.tsx index ccbc92fe5..64c833ce5 100644 --- a/frontend/src/test/client-bulk-calendar-renewal.test.tsx +++ b/frontend/src/test/client-bulk-calendar-renewal.test.tsx @@ -74,7 +74,11 @@ it('keeps bulk renewal disabled by default and requires explicit cutoff selectio }), ]), ); - fireEvent.click(screen.getByRole('button', { name: /Set first cycle cutoff/ })); + fireEvent.click( + screen.getByRole('button', { + name: 'Set first cycle cutoff: 2030-01-01 00:00:00 UTC+08:00', + }), + ); await submit(); await waitFor(() => expect(bulkCreate).toHaveBeenLastCalledWith([ diff --git a/frontend/src/test/client-calendar-renewal.test.tsx b/frontend/src/test/client-calendar-renewal.test.tsx index 05290987b..b28bdffe5 100644 --- a/frontend/src/test/client-calendar-renewal.test.tsx +++ b/frontend/src/test/client-calendar-renewal.test.tsx @@ -50,7 +50,9 @@ it('preserves monthly clients, previews backend dates, and saves exclusive weekl ); const mode = screen.getByLabelText('Auto renewal'); expect(mode.closest('.ant-select')?.textContent).toContain('Calendar monthly'); - await waitFor(() => expect(document.body.textContent).toContain('2029-12-31T23:59:59+08:00')); + await waitFor(() => + expect(document.body.textContent).toContain('2029-12-31 23:59:59 UTC+08:00'), + ); expect(post).toHaveBeenCalledWith( '/panel/api/clients/renewalPreview', expect.objectContaining({ resetMax: 3, resetCount: 2 }), diff --git a/frontend/src/test/client-renewal-preview-display.test.tsx b/frontend/src/test/client-renewal-preview-display.test.tsx new file mode 100644 index 000000000..21291caa6 --- /dev/null +++ b/frontend/src/test/client-renewal-preview-display.test.tsx @@ -0,0 +1,80 @@ +import { expect, it, vi } from 'vitest'; +import { screen } from '@testing-library/react'; +import { FormProvider, useForm } from 'react-hook-form'; + +import ClientRenewalFields from '@/pages/clients/ClientRenewalFields'; +import { HttpUtil, Msg } from '@/utils'; +import { renderWithProviders } from './test-utils'; + +function PreviewForm() { + const form = useForm({ defaultValues: { reset: 1, resetDay: 0, resetWeekday: 0, resetMax: 0 } }); + return ( + + {}} /> + + ); +} + +it.each([ + { + name: 'explains Local UTC without converting into the browser timezone', + zone: 'Local', + label: 'Server local time', + dates: ['2026-09-27T05:00:00Z', '2026-09-27T04:59:59Z', '2026-09-28T05:00:00Z'], + displayed: [ + '2026-09-27 05:00:00 UTC+00:00', + '2026-09-27 04:59:59 UTC+00:00', + '2026-09-28 05:00:00 UTC+00:00', + ], + }, + { + name: 'preserves different offsets across a daylight-saving transition', + zone: 'America/New_York', + label: 'America/New_York', + dates: ['2030-03-10T00:00:00-05:00', '2030-03-09T23:59:59-05:00', '2030-03-17T00:00:00-04:00'], + displayed: [ + '2030-03-10 00:00:00 UTC-05:00', + '2030-03-09 23:59:59 UTC-05:00', + '2030-03-17 00:00:00 UTC-04:00', + ], + }, + { + name: 'keeps fractional cutoffs and fractional-hour server offsets', + zone: 'Local', + label: 'Server local time', + dates: [ + '2030-01-01T00:00:00.123+05:45', + '2030-01-01T00:00:00+05:45', + '2030-01-02T00:00:00.123+05:45', + ], + displayed: [ + '2030-01-01 00:00:00.123 UTC+05:45', + '2030-01-01 00:00:00 UTC+05:45', + '2030-01-02 00:00:00.123 UTC+05:45', + ], + }, +])('$name', async ({ zone, label, dates, displayed }) => { + const post = vi.spyOn(HttpUtil, 'post').mockResolvedValue( + new Msg(true, '', { + timeZone: zone, + renewAt: dates[0], + validThrough: dates[1], + nextExpiry: dates[2], + suggestedExpiryTime: 0, + suggestedExpiry: '', + renewals: 1, + canRenew: true, + delayedStart: false, + }), + ); + try { + renderWithProviders(); + await screen.findByText(`Renewal preview (panel time zone: ${label})`); + const times = Array.from(document.querySelectorAll('time')); + expect(times.map((el) => el.textContent)).toEqual(displayed); + expect(times.map((el) => el.dateTime)).toEqual(dates); + expect(times.map((el) => el.dir)).toEqual(['ltr', 'ltr', 'ltr']); + } finally { + post.mockRestore(); + } +}); diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index 7e6c740af..2dc6db296 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "معاينة التجديد (المنطقة الزمنية للوحة: {zone})", + "renewServerLocal": "التوقيت المحلي للخادم", + "renewPreviewHint": "تُعرض التواريخ حسب المنطقة الزمنية للوحة. المعاينة لا تجدد العميل.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "عدد التجديدات المتوقع استهلاكها: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 83b25e14b..741cedf04 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "Renewal preview (panel time zone: {zone})", + "renewServerLocal": "Server local time", + "renewPreviewHint": "Dates use the panel time zone. Previewing does not renew the client.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "Estimated renewals used: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index e92ee5c7a..3d2cd152b 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "Vista previa de renovación (zona horaria del panel: {zone})", + "renewServerLocal": "Hora local del servidor", + "renewPreviewHint": "Las fechas usan la zona horaria del panel. La vista previa no renueva el cliente.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "Renovaciones que se consumirían: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 075640682..c5f3b2025 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "پیش‌نمایش تمدید (منطقهٔ زمانی پنل: {zone})", + "renewServerLocal": "زمان محلی سرور", + "renewPreviewHint": "تاریخ‌ها بر اساس منطقهٔ زمانی پنل نمایش داده می‌شوند. پیش‌نمایش باعث تمدید کاربر نمی‌شود.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "تعداد تمدیدهای قابل مصرف در این پیش‌نمایش: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 239e8b26b..17e816b3c 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "Pratinjau perpanjangan (zona waktu panel: {zone})", + "renewServerLocal": "Waktu lokal server", + "renewPreviewHint": "Tanggal menggunakan zona waktu panel. Pratinjau tidak memperpanjang klien.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "Perkiraan jumlah perpanjangan yang digunakan: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index f98d0b15e..df48d3090 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "更新プレビュー(パネルのタイムゾーン:{zone})", + "renewServerLocal": "サーバーのローカル時刻", + "renewPreviewHint": "日時はパネルのタイムゾーンで表示されます。プレビューではクライアントは更新されません。", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "消費予定の更新回数:{count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 45402b211..a6077b28b 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "Prévia da renovação (fuso horário do painel: {zone})", + "renewServerLocal": "Hora local do servidor", + "renewPreviewHint": "As datas usam o fuso horário do painel. A prévia não renova o cliente.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "Renovações que seriam consumidas: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 5e3d78989..6a5a087f3 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "Предпросмотр продления (часовой пояс панели: {zone})", + "renewServerLocal": "Местное время сервера", + "renewPreviewHint": "Даты указаны в часовом поясе панели. Предпросмотр не продлевает клиента.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "Будет использовано продлений: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index dc76b4b50..fbb1c3f53 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "Yenileme önizlemesi (panel saat dilimi: {zone})", + "renewServerLocal": "Sunucunun yerel saati", + "renewPreviewHint": "Tarihler panelin saat dilimine göre gösterilir. Önizleme istemciyi yenilemez.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "Kullanılması beklenen yenileme sayısı: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 21194f97c..05e22839a 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "Попередній перегляд продовження (часовий пояс панелі: {zone})", + "renewServerLocal": "Місцевий час сервера", + "renewPreviewHint": "Дати вказано в часовому поясі панелі. Попередній перегляд не продовжує клієнта.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "Буде використано продовжень: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 78ad5ec64..29b8c80e4 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -953,11 +953,13 @@ "renewModeMonthly": "Calendar monthly", "renewWeekday": "Renew on weekday", "renewScheduleDesc": "Calendar renewal uses midnight in the panel timezone and also resets the client's traffic. Keep independent traffic resets disabled unless an additional reset is intended. Monthly day 1 means valid through the preceding month; day 31 is not the same boundary.", - "renewPreview": "Renewal preview ({zone})", + "renewPreview": "Xem trước gia hạn (múi giờ bảng điều khiển: {zone})", + "renewServerLocal": "Giờ địa phương của máy chủ", + "renewPreviewHint": "Ngày giờ dùng múi giờ của bảng điều khiển. Xem trước không thực hiện gia hạn máy khách.", "renewAt": "Renewal boundary", "renewValidThrough": "Valid through", "renewNextExpiry": "Cutoff after renewal", - "renewPeriods": "Periods charged: {count}", + "renewPeriods": "Số lượt gia hạn dự kiến sử dụng: {count}", "renewUnavailable": "The remaining renewal allowances cannot provide a future cutoff.", "renewFirstUse": "Dates are available after first-use activation. The initial duration is unchanged.", "renewSetExpiry": "Set first cycle cutoff", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index a70d00e39..9fed783f0 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -953,11 +953,13 @@ "renewModeMonthly": "日历月", "renewWeekday": "每周续期日", "renewScheduleDesc": "日历续期按面板时区的午夜执行,并重置客户端流量。若不需要额外重置,请关闭独立的流量重置。每月 1 日续期表示有效至上个月末;每月 31 日不是同一边界。", - "renewPreview": "续期预览({zone})", + "renewPreview": "续期预览(面板时区:{zone})", + "renewServerLocal": "服务器本地时区", + "renewPreviewHint": "日期按面板时区显示,查看预览不会执行续期。", "renewAt": "续期边界", "renewValidThrough": "本期有效至", "renewNextExpiry": "续期后的截止时间", - "renewPeriods": "计入续期次数:{count}", + "renewPeriods": "预计消耗续期次数:{count}", "renewUnavailable": "剩余续期次数不足以提供未来的截止时间。", "renewFirstUse": "首次使用激活后才能确定日期,首次使用后的初始时长保持不变。", "renewSetExpiry": "设置首个周期截止时间", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index 9f3a518d4..e326cd2f1 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -953,11 +953,13 @@ "renewModeMonthly": "日曆月", "renewWeekday": "每週續期日", "renewScheduleDesc": "日曆續期按面板時區的午夜執行,並重置客戶端流量。若不需要額外重置,請關閉獨立的流量重置。每月 1 日續期表示有效至上個月末;每月 31 日不是同一邊界。", - "renewPreview": "續期預覽({zone})", + "renewPreview": "續期預覽(面板時區:{zone})", + "renewServerLocal": "伺服器本地時區", + "renewPreviewHint": "日期依面板時區顯示,查看預覽不會執行續期。", "renewAt": "續期邊界", "renewValidThrough": "本期有效至", "renewNextExpiry": "續期後的截止時間", - "renewPeriods": "計入續期次數:{count}", + "renewPeriods": "預計消耗續期次數:{count}", "renewUnavailable": "剩餘續期次數不足以提供未來的截止時間。", "renewFirstUse": "首次使用啟用後才能確定日期,首次使用後的初始時長保持不變。", "renewSetExpiry": "設定首個週期截止時間",