diff --git a/console/package-lock.json b/console/package-lock.json index c5b7d5e15..c0f00e3ad 100644 --- a/console/package-lock.json +++ b/console/package-lock.json @@ -16,6 +16,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@vvo/tzdb": "^6.198.0", "ahooks": "^3.9.6", "antd": "^5.29.1", "antd-style": "^3.7.1", @@ -6976,6 +6977,12 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vvo/tzdb": { + "version": "6.198.0", + "resolved": "https://registry.npmmirror.com/@vvo/tzdb/-/tzdb-6.198.0.tgz", + "integrity": "sha512-bNRWBhWYl0edVgyX6AYbhoCM2tk2lXJjGCyO2VDc2xn6Dw8dLd7WGj2DDXkVOkmOIQTNjEAcxrEpIzz5pWVwFg==", + "license": "MIT" + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", diff --git a/console/package.json b/console/package.json index 81dcdcffb..1cc751cf6 100644 --- a/console/package.json +++ b/console/package.json @@ -24,6 +24,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@vvo/tzdb": "^6.198.0", "ahooks": "^3.9.6", "antd": "^5.29.1", "antd-style": "^3.7.1", diff --git a/console/src/constants/timezone.ts b/console/src/constants/timezone.ts index d88d5096f..fe7eb0b26 100644 --- a/console/src/constants/timezone.ts +++ b/console/src/constants/timezone.ts @@ -1,21 +1,60 @@ -export const TIMEZONE_OPTIONS = [ - { value: "America/Los_Angeles", label: "America/Los_Angeles (UTC-8)" }, - { value: "America/Denver", label: "America/Denver (UTC-7)" }, - { value: "America/Chicago", label: "America/Chicago (UTC-6)" }, - { value: "America/New_York", label: "America/New_York (UTC-5)" }, - { value: "America/Toronto", label: "America/Toronto (UTC-5)" }, - { value: "UTC", label: "UTC" }, - { value: "Europe/London", label: "Europe/London (UTC+0)" }, - { value: "Europe/Paris", label: "Europe/Paris (UTC+1)" }, - { value: "Europe/Berlin", label: "Europe/Berlin (UTC+1)" }, - { value: "Europe/Moscow", label: "Europe/Moscow (UTC+3)" }, - { value: "Asia/Dubai", label: "Asia/Dubai (UTC+4)" }, - { value: "Asia/Shanghai", label: "Asia/Shanghai (UTC+8)" }, - { value: "Asia/Hong_Kong", label: "Asia/Hong_Kong (UTC+8)" }, - { value: "Asia/Singapore", label: "Asia/Singapore (UTC+8)" }, - { value: "Asia/Tokyo", label: "Asia/Tokyo (UTC+9)" }, - { value: "Asia/Seoul", label: "Asia/Seoul (UTC+9)" }, - { value: "Australia/Sydney", label: "Australia/Sydney (UTC+10)" }, - { value: "Australia/Melbourne", label: "Australia/Melbourne (UTC+10)" }, - { value: "Pacific/Auckland", label: "Pacific/Auckland (UTC+12)" }, -]; +import { getTimeZones } from "@vvo/tzdb"; + +const TIMEZONE_ID_SET = new Set([ + "America/Los_Angeles", + "America/Denver", + "America/Chicago", + "America/New_York", + "America/Toronto", + "UTC", + "Europe/London", + "Europe/Paris", + "Europe/Berlin", + "Europe/Moscow", + "Asia/Dubai", + "Asia/Shanghai", + "Asia/Hong_Kong", + "Asia/Singapore", + "Asia/Tokyo", + "Asia/Seoul", + "Australia/Sydney", + "Australia/Melbourne", + "Pacific/Auckland", +]); + +export interface TimezoneOption { + value: string; // for timezone id + label: string; // for display text +} + +function getLocalizedName(tzName: string, lang: string): string { + const locale = { zh: "zh-CN", en: "en", ru: "ru", ja: "ja" }[lang] || "en"; + try { + const parts = new Intl.DateTimeFormat(locale, { + timeZone: tzName, + timeZoneName: "long", + }).formatToParts(new Date()); + return parts.find((p) => p.type === "timeZoneName")?.value || tzName; + } catch { + return tzName; + } +} + +export function getTimezoneOptions(lang: string = "en"): TimezoneOption[] { + return getTimeZones({ includeUtc: true }) + .filter( + (tz) => + TIMEZONE_ID_SET.has(tz.name) || + (tz.name === "Etc/UTC" && TIMEZONE_ID_SET.has("UTC")), + ) + .sort((a, b) => a.currentTimeOffsetInMinutes - b.currentTimeOffsetInMinutes) + .map((tz) => { + const value = tz.name === "Etc/UTC" ? "UTC" : tz.name; + return { + value, + label: `${getLocalizedName(tz.name, lang)} (${ + tz.currentTimeFormat.split(" ")[0] + }, ${value})`, + }; + }); +} diff --git a/console/src/hooks/useTimezoneOptions.ts b/console/src/hooks/useTimezoneOptions.ts new file mode 100644 index 000000000..acfa4072d --- /dev/null +++ b/console/src/hooks/useTimezoneOptions.ts @@ -0,0 +1,8 @@ +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { getTimezoneOptions, type TimezoneOption } from "../constants/timezone"; + +export function useTimezoneOptions(): TimezoneOption[] { + const { i18n } = useTranslation(); + return useMemo(() => getTimezoneOptions(i18n.language), [i18n.language]); +} diff --git a/console/src/pages/Agent/Config/components/ReactAgentCard.tsx b/console/src/pages/Agent/Config/components/ReactAgentCard.tsx index 4f84431d3..6a9841694 100644 --- a/console/src/pages/Agent/Config/components/ReactAgentCard.tsx +++ b/console/src/pages/Agent/Config/components/ReactAgentCard.tsx @@ -1,6 +1,6 @@ import { Form, InputNumber, Select, Card, Alert } from "@agentscope-ai/design"; import { useTranslation } from "react-i18next"; -import { TIMEZONE_OPTIONS } from "../../../../constants/timezone"; +import { useTimezoneOptions } from "../../../../hooks/useTimezoneOptions"; import styles from "../index.module.less"; const LANGUAGE_OPTIONS = [ @@ -63,7 +63,7 @@ export function ReactAgentCard({ .toLowerCase() .includes(input.toLowerCase()) } - options={TIMEZONE_OPTIONS} + options={useTimezoneOptions()} onChange={onTimezoneChange} loading={savingTimezone} disabled={savingTimezone} diff --git a/console/src/pages/Control/CronJobs/components/JobDrawer.tsx b/console/src/pages/Control/CronJobs/components/JobDrawer.tsx index b58341a7d..6edd91eb7 100644 --- a/console/src/pages/Control/CronJobs/components/JobDrawer.tsx +++ b/console/src/pages/Control/CronJobs/components/JobDrawer.tsx @@ -12,7 +12,8 @@ import { TimePicker } from "antd"; import { useTranslation } from "react-i18next"; import type { FormInstance } from "antd"; import type { CronJobSpecOutput } from "../../../../api/types"; -import { TIMEZONE_OPTIONS, DEFAULT_FORM_VALUES } from "./constants"; +import { DEFAULT_FORM_VALUES } from "./constants"; +import { useTimezoneOptions } from "../../../../hooks/useTimezoneOptions"; import styles from "../index.module.less"; type CronJob = CronJobSpecOutput; @@ -230,7 +231,7 @@ export function JobDrawer({ .toLowerCase() .includes(input.toLowerCase()) } - options={TIMEZONE_OPTIONS} + options={useTimezoneOptions()} /> diff --git a/console/src/pages/Control/CronJobs/components/constants.ts b/console/src/pages/Control/CronJobs/components/constants.ts index 52fb65d2b..f63200a16 100644 --- a/console/src/pages/Control/CronJobs/components/constants.ts +++ b/console/src/pages/Control/CronJobs/components/constants.ts @@ -1,7 +1,5 @@ import dayjs from "dayjs"; -export { TIMEZONE_OPTIONS } from "../../../../constants/timezone"; - export const DEFAULT_FORM_VALUES = { enabled: false, schedule: { diff --git a/console/src/pages/Control/CronJobs/components/index.ts b/console/src/pages/Control/CronJobs/components/index.ts index e2248d43f..3cc5581d1 100644 --- a/console/src/pages/Control/CronJobs/components/index.ts +++ b/console/src/pages/Control/CronJobs/components/index.ts @@ -1,4 +1,4 @@ export { createColumns } from "./columns"; export { JobDrawer } from "./JobDrawer"; export { useCronJobs } from "../useCronJobs"; -export { TIMEZONE_OPTIONS, DEFAULT_FORM_VALUES } from "./constants"; +export { DEFAULT_FORM_VALUES } from "./constants";