diff --git a/l10n/messages.pot b/l10n/messages.pot
index 01710efedd..3601505f05 100644
--- a/l10n/messages.pot
+++ b/l10n/messages.pot
@@ -211,10 +211,6 @@ msgstr ""
msgid "Frequently used"
msgstr ""
-#. TRANSLATORS: This refers to global timezones in the timezone picker
-msgid "Global"
-msgstr ""
-
msgid "Go back to the list"
msgstr ""
@@ -416,9 +412,6 @@ msgstr ""
msgid "Search emoji"
msgstr ""
-msgid "Search for timezone"
-msgstr ""
-
msgid "Search results"
msgstr ""
diff --git a/package-lock.json b/package-lock.json
index b0edd8fc20..e4b4d58f65 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,7 +21,6 @@
"@nextcloud/logger": "^3.0.2",
"@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.3.0",
- "@nextcloud/timezones": "^1.0.0",
"@vuepic/vue-datepicker": "^11.0.2",
"@vueuse/components": "^13.9.0",
"@vueuse/core": "^13.9.0",
@@ -3523,18 +3522,6 @@
"stylelint-config-recommended-vue": "^1.5.0"
}
},
- "node_modules/@nextcloud/timezones": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@nextcloud/timezones/-/timezones-1.0.0.tgz",
- "integrity": "sha512-9b7Wms2mzB4RAltf8s9dY40PcU5ova5QjQJw1Gty35e54alKyx33BqUOy4gEbkzmYSrW4aZcLcMrOO5Bj2eIzg==",
- "license": "AGPL-3.0-or-later",
- "dependencies": {
- "ical.js": "^2.1.0"
- },
- "engines": {
- "node": "^20 || ^22"
- }
- },
"node_modules/@nextcloud/typings": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.9.1.tgz",
@@ -13457,12 +13444,6 @@
"dev": true,
"license": "BSD-3-Clause"
},
- "node_modules/ical.js": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.1.tgz",
- "integrity": "sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg==",
- "license": "MPL-2.0"
- },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
diff --git a/package.json b/package.json
index 138da927f8..87f25b25b0 100644
--- a/package.json
+++ b/package.json
@@ -87,7 +87,6 @@
"@nextcloud/logger": "^3.0.2",
"@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.3.0",
- "@nextcloud/timezones": "^1.0.0",
"@vuepic/vue-datepicker": "^11.0.2",
"@vueuse/components": "^13.9.0",
"@vueuse/core": "^13.9.0",
diff --git a/src/components/NcTimezonePicker/NcTimezonePicker.vue b/src/components/NcTimezonePicker/NcTimezonePicker.vue
index 02d512f8ed..2106fc2504 100644
--- a/src/components/NcTimezonePicker/NcTimezonePicker.vue
+++ b/src/components/NcTimezonePicker/NcTimezonePicker.vue
@@ -16,7 +16,7 @@
export default {
data() {
return {
- tz: 'Hawaiian Standard Time',
+ tz: 'Europe/Berlin',
}
},
}
@@ -25,21 +25,27 @@ export default {
+ :reduce="(option) => option.timezoneId"
+ label="label">
+
+
+
+ {{ option.label }}
+ {{ option.time }}
+
+
+ {{ option.timezoneId }}
+ {{ option.localeOffset }}
+
+
+
+
+
+
diff --git a/src/components/NcTimezonePicker/timezoneDataProviderService.ts b/src/components/NcTimezonePicker/timezoneDataProviderService.ts
deleted file mode 100644
index 923391e358..0000000000
--- a/src/components/NcTimezonePicker/timezoneDataProviderService.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import { getTimezoneManager } from '@nextcloud/timezones'
-
-const timezoneManager = getTimezoneManager()
-let initialized = false
-
-/**
- * Gets the timezone-manager
- * initializes it if necessary
- */
-export default function() {
- if (!initialized) {
- timezoneManager.registerDefaultTimezones()
- initialized = true
- }
-
- return timezoneManager
-}
diff --git a/src/components/NcTimezonePicker/timezoneUtils.ts b/src/components/NcTimezonePicker/timezoneUtils.ts
new file mode 100644
index 0000000000..1d72bccbdb
--- /dev/null
+++ b/src/components/NcTimezonePicker/timezoneUtils.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getLanguage } from '@nextcloud/l10n'
+import { memoize } from '../../utils/utils.ts'
+
+const now = new Date()
+
+/**
+ * Convert timezone ID in IANA format (e.g. "Europe/Berlin") to specified format via Intl.DateTimeFormat
+ *
+ * @param timeZone - IANA timezone ID (e.g. "Europe/Berlin")
+ * @param timeZoneName - Intl.DateTimeFormatOptions['timeZoneName']
+ * @param lang - Language code (e.g. 'en'), defaults to the current user's language
+ */
+export function formatTimezone(timeZone: string, timeZoneName: NonNullable, lang: string = getLanguage()): string | undefined {
+ return new Intl.DateTimeFormat(lang, { timeZone, timeZoneName })
+ .formatToParts(now)
+ .find((part) => part.type === 'timeZoneName')
+ ?.value
+}
+
+/**
+ * Get offset in ms for a given timezone ID in IANA format (e.g. "Europe/Berlin")
+ *
+ * @param timeZone - IANA timezone ID (e.g. "Europe/Berlin")
+ * @return - Offset in milliseconds (e.g. 3600000 for GMT+01:00)
+ */
+const getTimezoneOffset = memoize((timeZone: string): number => {
+ // 'en-US' gives predictable GMT+00:00 or GMT+00:00:00 format
+ const gmt = formatTimezone(timeZone, 'longOffset', 'en-US')
+ const [isMatched, sign, h = '0', m = '0', s = '0'] = gmt?.match(/GMT([+-])(\d+):(\d+)(?::(\d+))?/) ?? []
+ if (!isMatched) {
+ return 0
+ }
+ return (sign === '+' ? 1 : -1) * (parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s)) * 1000
+})
+
+/**
+ * Get a list of supported IANA timezone IDs (e.g. "Europe/Berlin") with human-readable labels,
+ * excluding Etc/* administrative zones not used by users (see: https://en.wikipedia.org/wiki/Tz_database#Areas)
+ */
+export const getTimezones = memoize(() => {
+ const zones = Intl.supportedValuesOf('timeZone')
+ .filter((tz) => !tz.startsWith('Etc/'))
+ console.time('Adding formatted timezone labels')
+ const result = zones.map((timezoneId) => ({
+ timezoneId,
+ label: formatTimezone(timezoneId, 'shortGeneric'), // Alternative: longGeneric, but too long our select design..
+ offset: getTimezoneOffset(timezoneId),
+ localeOffset: formatTimezone(timezoneId, 'shortOffset'),
+ }))
+ console.timeEnd('Adding formatted timezone labels')
+ // Sort by offset first, then by name
+ result.sort((a, b) => a.offset - b.offset || a.timezoneId.localeCompare(b.timezoneId))
+ return result
+})
+
+// See:
+// https://play.vuejs.org/#eNqdVdtO20AQ/ZWpX+yoiRNEpaooCWoLregDoIa2EnUfjD1ODOu1tbsOl5B/78xu7IQQqFQpsnZnzlzOXDYL72NVhfMavQNvqBOVVwY0mroaRzIvqlIZWIDCDJaQqbIAn6B+JCOZlFIbELGcahjBbx+l3wU/Rf6i5q+q+Zsp/88mmsDkLmB8h/1ktUxMXkqYopnUFUfE9CIv8KGUqIMOLAhlFKWkJJxII0LdoH7GokZ9Rs4M4S8Jb12ayIRZLgyqwDzAaAxvzEOoTayM/pWbWeAfm6TvdxqoJl9BEHfhqsNgSqOJfpZlVIog7kBvh5jgj48Qh6JMYoGfy6KKFZKU/C63iW2ZNumuyLniTAtDtclKVcQtvkVSIUUpp87ellj2fkwcXWdeWtXEqNzWmLwdhlrkCQb7NlH/ktvWFFLiLRzFBoNBJ5xzGamKTLOV+3sf3g96gz36XQwGB/YXDgYDH94+CbVhvs37RSrN6TQu6MZD8bTLnITtNGfC9l+sp4CRXRrH3Z5g2TbfBS7PueXBBtX1dMg0CKhfxracD6G5rxBGoxG0w8ROm4E6dDRXFF3F7YByqXcPbiSHfbdQtEp0MVhUgvKgG8BQo8DEwLxXlCmKUeQxucizSlKXlS3hvEdcSJmTMeTSrVuLAlgswKqWlJa16ztDF6PvgriLia8Ex2Y2Q6NaF0MzG+sZJe9ma9inu0MxbjZuGMHJ0TOdtfuKElWePFPyvO4UPjegIye0Sm3NOmXOtpyRBwc3eG+FG/yHJh1TDbYmLU9pQTY40b64IVsuKVK6kZC1pjC7FS+6XTH4h99d5sz/P81eDdpWkA6rRtNpPXFe1zOaxjbLp+G1LiW99rRxAJGX0MOVC1RndnCo0Ae8i1zcyIuFKG+/WZlRNXYbeTLD5GaH/FrfsSzyzhVqVHOMvFZH7y8tilMfT07xjs6tkpagFoR+RfkddSlqztHBPtUypbQ3cDbbE/ufRQ/ThT6+Myh1Q4oTZeTS4iOP/sf4zX6J+jrd/fCdtaPd95Z/AZWWeP8=
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
new file mode 100644
index 0000000000..c9779b4771
--- /dev/null
+++ b/src/utils/utils.ts
@@ -0,0 +1,47 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+export function once any>(func: F): F
+export function once void>(func: F): F
+/**
+ * Singletone function decorator
+ *
+ * @param func - Function
+ */
+export function once any) | ((...args: any[]) => void)>(func: F): F {
+ let wasCalled = false
+ let result: ReturnType
+
+ return ((...args: Parameters): ReturnType => {
+ if (!wasCalled) {
+ wasCalled = true
+ result = func(...args)
+ }
+ return result
+ }) as F
+}
+
+/**
+ * Memoization function decorator
+ *
+ * @param func - Function to memoize with a single argument
+ */
+export function memoize any>(func: F): F {
+ const cache = new Map[0], ReturnType>()
+
+ return ((arg: Parameters[0]): ReturnType => {
+ if (cache.has(arg)) {
+ return cache.get(arg)!
+ }
+
+ const result = func(arg)
+ cache.set(arg, result)
+ return result
+ }) as F
+}
+
+/* eslint-enable @typescript-eslint/no-explicit-any */