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 { + + 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 */