diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48263efb0a..df6b9eb6f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -167,10 +167,6 @@ jobs: tee -a testproject/settings/local.py << EOF from warnings import filterwarnings SILENCED_SYSTEM_CHECKS = ["wagtailadmin.W001"] - # Remove when l18n dependency is updated or removed - filterwarnings( - "ignore", "'locale.getdefaultlocale' is deprecated .*" - ) # Remove when all experimental jobs use Django >= 6.0 filterwarnings( "ignore", "The FORMS_URLFIELD_ASSUME_HTTPS transitional setting is deprecated." diff --git a/client/src/controllers/LocaleController.test.js b/client/src/controllers/LocaleController.test.js new file mode 100644 index 0000000000..154ed2ccec --- /dev/null +++ b/client/src/controllers/LocaleController.test.js @@ -0,0 +1,137 @@ +import { Application } from '@hotwired/stimulus'; +import { LocaleController } from './LocaleController'; +import { InitController } from './InitController'; + +describe('LocaleController', () => { + let app; + let select; + + const setup = async (html) => { + document.body.innerHTML = `<main>${html}</main>`; + select = document.querySelector('select'); + select.innerHTML = /* html */ ` + <option value="" selected>Use server time zone</option> + <option value="Africa/Abidjan">Africa/Abidjan</option> + <option value="America/Argentina/Jujuy">America/Argentina/Jujuy</option> + <option value="America/Indiana/Knox">America/Indiana/Knox</option> + <option value="Antarctica/Rothera">Antarctica/Rothera</option> + <option value="Arctic/Longyearbyen">Arctic/Longyearbyen</option> + <option value="Asia/Katmandu">Asia/Katmandu</option> + <option value="Atlantic/Canary">Atlantic/Canary</option> + <option value="Australia/South">Australia/South</option> + <option value="Brazil/East">Brazil/East</option> + <option value="Canada/Atlantic">Canada/Atlantic</option> + <option value="Chile/Continental">Chile/Continental</option> + <option value="EST">EST</option> + <option value="Etc/GMT-7">Etc/GMT-7</option> + <option value="Europe/Brussels">Europe/Brussels</option> + <option value="GMT">GMT</option> + <option value="Indian/Maldives">Indian/Maldives</option> + <option value="Pacific/Tarawa">Pacific/Tarawa</option> + <option value="UTC">UTC</option> + <option value="Universal">Universal</option> + <option value="Zulu">Zulu</option> + `; + + app = Application.start(); + app.register('w-locale', LocaleController); + app.register('w-init', InitController); + + await Promise.resolve(); + }; + + afterEach(() => { + app?.stop(); + jest.clearAllMocks(); + }); + + describe('localizing time zone options', () => { + it('should append localized time zone labels to the options', async () => { + document.documentElement.lang = 'en-US'; + await setup(/* html */ ` + <select + name="locale-current_time_zone" + data-controller="w-init w-locale" + data-action="w-init:ready->w-locale#localizeTimeZoneOptions" + data-w-locale-server-time-zone-param="Europe/London" + > + </select> + `); + + expect(select.getAttribute('data-controller')).toEqual('w-locale'); + const selected = select.selectedOptions[0]; + expect(selected).toBeTruthy(); + expect(selected.value).toEqual(''); + expect(selected.textContent).toEqual( + 'Use server time zone: GMT (Greenwich Mean Time)', + ); + expect(select).toMatchSnapshot(); + }); + }); + + it('should localize to the current HTML locale and use the server time zone param for the default', async () => { + document.documentElement.lang = 'id-ID'; + await setup(/* html */ ` + <select + name="locale-current_time_zone" + data-controller="w-init w-locale" + data-action="w-init:ready->w-locale#localizeTimeZoneOptions" + data-w-locale-server-time-zone-param="Asia/Jakarta" + > + </select> + `); + + expect(select.getAttribute('data-controller')).toEqual('w-locale'); + const selected = select.selectedOptions[0]; + expect(selected).toBeTruthy(); + expect(selected.value).toEqual(''); + expect(selected.textContent).toEqual( + 'Use server time zone: WIB (Waktu Indonesia Barat)', + ); + expect(select).toMatchSnapshot(); + }); + + it('should skip updating the default option if server time zone is not provided', async () => { + document.documentElement.lang = 'ar'; + await setup(/* html */ ` + <select + name="locale-current_time_zone" + data-controller="w-init w-locale" + data-action="w-init:ready->w-locale#localizeTimeZoneOptions" + > + </select> + `); + + expect(select.getAttribute('data-controller')).toEqual('w-locale'); + const selected = select.selectedOptions[0]; + expect(selected).toBeTruthy(); + expect(selected.value).toEqual(''); + expect(selected.textContent).toEqual('Use server time zone'); + expect(select).toMatchSnapshot(); + }); + + it('should allow updating the time zone options on an uncontrolled select element via events', async () => { + document.documentElement.lang = 'id-ID'; + await setup(/* html */ ` + <form data-controller="w-locale"> + <select + name="locale-current_time_zone" + data-action="custom:event->w-locale#localizeTimeZoneOptions" + data-w-locale-server-time-zone-param="Asia/Tokyo" + > + </select> + </form> + `); + select.dispatchEvent(new CustomEvent('custom:event')); + await Promise.resolve(); + + expect(select.hasAttribute('data-controller')).toBe(false); + const selected = select.selectedOptions[0]; + expect(selected).toBeTruthy(); + expect(selected.value).toEqual(''); + expect(selected.textContent).toEqual( + 'Use server time zone: GMT+9 (Waktu Standar Jepang)', + ); + expect(select).toMatchSnapshot(); + }); +}); diff --git a/client/src/controllers/LocaleController.ts b/client/src/controllers/LocaleController.ts new file mode 100644 index 0000000000..887e2b77c7 --- /dev/null +++ b/client/src/controllers/LocaleController.ts @@ -0,0 +1,55 @@ +import { Controller } from '@hotwired/stimulus'; + +/** + * Localizes elements in the current locale. + */ +export class LocaleController extends Controller<HTMLSelectElement> { + /** + * Localize an IANA time zone in the current locale. + * + * @param timeZone An IANA time zone string + * @param format Time zone name formatting option + * @returns formatted time zone name in the current locale + */ + static localizeTimeZone( + timeZone: string, + format: Intl.DateTimeFormatOptions['timeZoneName'], + ) { + const df = new Intl.DateTimeFormat(document.documentElement.lang, { + timeZone, + timeZoneName: format, + }); + const parts = df.formatToParts(new Date()); + return parts.find((part) => part.type === 'timeZoneName')!.value; + } + + /** + * + * @param timeZone An IANA time zone string + * @returns formatted time zone name in the current locale with short and long + * labels, e.g. `"GMT+7 (Western Indonesia Time)"` + */ + static getTZLabel(timeZone: string) { + const shortLabel = LocaleController.localizeTimeZone(timeZone, 'short'); + const longLabel = LocaleController.localizeTimeZone(timeZone, 'long'); + return `${shortLabel} (${longLabel})`; + } + + /** + * Localize the time zone `<options>` of a `<select>` element in the current + * locale. + */ + localizeTimeZoneOptions( + event?: Event & { params?: { serverTimeZone?: string } }, + ) { + const element = (event?.target as HTMLSelectElement) || this.element; + const serverTimeZone = event?.params?.serverTimeZone; + Array.from(element.options).forEach((opt) => { + const timeZone = opt.value || serverTimeZone; + if (!timeZone) return; + const localized = LocaleController.getTZLabel(timeZone); + const option = opt; + option.textContent = `${option.textContent}: ${localized}`; + }); + } +} diff --git a/client/src/controllers/__snapshots__/LocaleController.test.js.snap b/client/src/controllers/__snapshots__/LocaleController.test.js.snap new file mode 100644 index 0000000000..36538f3f0e --- /dev/null +++ b/client/src/controllers/__snapshots__/LocaleController.test.js.snap @@ -0,0 +1,639 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocaleController localizing time zone options should append localized time zone labels to the options 1`] = ` +<select + data-action="w-init:ready->w-locale#localizeTimeZoneOptions" + data-controller="w-locale" + data-w-locale-server-time-zone-param="Europe/London" + name="locale-current_time_zone" +> + + + <option + selected="" + value="" + > + Use server time zone: GMT (Greenwich Mean Time) + </option> + + + <option + value="Africa/Abidjan" + > + Africa/Abidjan: GMT (Greenwich Mean Time) + </option> + + + <option + value="America/Argentina/Jujuy" + > + America/Argentina/Jujuy: GMT-3 (Argentina Standard Time) + </option> + + + <option + value="America/Indiana/Knox" + > + America/Indiana/Knox: CST (Central Standard Time) + </option> + + + <option + value="Antarctica/Rothera" + > + Antarctica/Rothera: GMT-3 (Rothera Time) + </option> + + + <option + value="Arctic/Longyearbyen" + > + Arctic/Longyearbyen: GMT+1 (Central European Standard Time) + </option> + + + <option + value="Asia/Katmandu" + > + Asia/Katmandu: GMT+5:45 (Nepal Time) + </option> + + + <option + value="Atlantic/Canary" + > + Atlantic/Canary: GMT (Western European Standard Time) + </option> + + + <option + value="Australia/South" + > + Australia/South: GMT+10:30 (Australian Central Daylight Time) + </option> + + + <option + value="Brazil/East" + > + Brazil/East: GMT-3 (Brasilia Standard Time) + </option> + + + <option + value="Canada/Atlantic" + > + Canada/Atlantic: AST (Atlantic Standard Time) + </option> + + + <option + value="Chile/Continental" + > + Chile/Continental: GMT-3 (Chile Summer Time) + </option> + + + <option + value="EST" + > + EST: EST (Eastern Standard Time) + </option> + + + <option + value="Etc/GMT-7" + > + Etc/GMT-7: GMT+7 (GMT+07:00) + </option> + + + <option + value="Europe/Brussels" + > + Europe/Brussels: GMT+1 (Central European Standard Time) + </option> + + + <option + value="GMT" + > + GMT: UTC (Coordinated Universal Time) + </option> + + + <option + value="Indian/Maldives" + > + Indian/Maldives: GMT+5 (Maldives Time) + </option> + + + <option + value="Pacific/Tarawa" + > + Pacific/Tarawa: GMT+12 (Gilbert Islands Time) + </option> + + + <option + value="UTC" + > + UTC: UTC (Coordinated Universal Time) + </option> + + + <option + value="Universal" + > + Universal: UTC (Coordinated Universal Time) + </option> + + + <option + value="Zulu" + > + Zulu: UTC (Coordinated Universal Time) + </option> + + +</select> +`; + +exports[`LocaleController should allow updating the time zone options on an uncontrolled select element via events 1`] = ` +<select + data-action="custom:event->w-locale#localizeTimeZoneOptions" + data-w-locale-server-time-zone-param="Asia/Tokyo" + name="locale-current_time_zone" +> + + + <option + selected="" + value="" + > + Use server time zone: GMT+9 (Waktu Standar Jepang) + </option> + + + <option + value="Africa/Abidjan" + > + Africa/Abidjan: GMT (Greenwich Mean Time) + </option> + + + <option + value="America/Argentina/Jujuy" + > + America/Argentina/Jujuy: GMT-3 (Waktu Standar Argentina) + </option> + + + <option + value="America/Indiana/Knox" + > + America/Indiana/Knox: CST (Waktu Standar Tengah) + </option> + + + <option + value="Antarctica/Rothera" + > + Antarctica/Rothera: GMT-3 (Waktu Rothera) + </option> + + + <option + value="Arctic/Longyearbyen" + > + Arctic/Longyearbyen: GMT+1 (Waktu Standar Eropa Tengah) + </option> + + + <option + value="Asia/Katmandu" + > + Asia/Katmandu: GMT+5.45 (Waktu Nepal) + </option> + + + <option + value="Atlantic/Canary" + > + Atlantic/Canary: GMT (Waktu Standar Eropa Barat) + </option> + + + <option + value="Australia/South" + > + Australia/South: GMT+10.30 (Waktu Musim Panas Tengah Australia) + </option> + + + <option + value="Brazil/East" + > + Brazil/East: GMT-3 (Waktu Standar Brasil) + </option> + + + <option + value="Canada/Atlantic" + > + Canada/Atlantic: AST (Waktu Standar Atlantik) + </option> + + + <option + value="Chile/Continental" + > + Chile/Continental: GMT-3 (Waktu Musim Panas Cile) + </option> + + + <option + value="EST" + > + EST: EST (Waktu Standar Timur) + </option> + + + <option + value="Etc/GMT-7" + > + Etc/GMT-7: GMT+7 (GMT+07.00) + </option> + + + <option + value="Europe/Brussels" + > + Europe/Brussels: GMT+1 (Waktu Standar Eropa Tengah) + </option> + + + <option + value="GMT" + > + GMT: UTC (Waktu Universal Terkoordinasi) + </option> + + + <option + value="Indian/Maldives" + > + Indian/Maldives: GMT+5 (Waktu Maladewa) + </option> + + + <option + value="Pacific/Tarawa" + > + Pacific/Tarawa: GMT+12 (Waktu Kep. Gilbert) + </option> + + + <option + value="UTC" + > + UTC: UTC (Waktu Universal Terkoordinasi) + </option> + + + <option + value="Universal" + > + Universal: UTC (Waktu Universal Terkoordinasi) + </option> + + + <option + value="Zulu" + > + Zulu: UTC (Waktu Universal Terkoordinasi) + </option> + + +</select> +`; + +exports[`LocaleController should localize to the current HTML locale and use the server time zone param for the default 1`] = ` +<select + data-action="w-init:ready->w-locale#localizeTimeZoneOptions" + data-controller="w-locale" + data-w-locale-server-time-zone-param="Asia/Jakarta" + name="locale-current_time_zone" +> + + + <option + selected="" + value="" + > + Use server time zone: WIB (Waktu Indonesia Barat) + </option> + + + <option + value="Africa/Abidjan" + > + Africa/Abidjan: GMT (Greenwich Mean Time) + </option> + + + <option + value="America/Argentina/Jujuy" + > + America/Argentina/Jujuy: GMT-3 (Waktu Standar Argentina) + </option> + + + <option + value="America/Indiana/Knox" + > + America/Indiana/Knox: CST (Waktu Standar Tengah) + </option> + + + <option + value="Antarctica/Rothera" + > + Antarctica/Rothera: GMT-3 (Waktu Rothera) + </option> + + + <option + value="Arctic/Longyearbyen" + > + Arctic/Longyearbyen: GMT+1 (Waktu Standar Eropa Tengah) + </option> + + + <option + value="Asia/Katmandu" + > + Asia/Katmandu: GMT+5.45 (Waktu Nepal) + </option> + + + <option + value="Atlantic/Canary" + > + Atlantic/Canary: GMT (Waktu Standar Eropa Barat) + </option> + + + <option + value="Australia/South" + > + Australia/South: GMT+10.30 (Waktu Musim Panas Tengah Australia) + </option> + + + <option + value="Brazil/East" + > + Brazil/East: GMT-3 (Waktu Standar Brasil) + </option> + + + <option + value="Canada/Atlantic" + > + Canada/Atlantic: AST (Waktu Standar Atlantik) + </option> + + + <option + value="Chile/Continental" + > + Chile/Continental: GMT-3 (Waktu Musim Panas Cile) + </option> + + + <option + value="EST" + > + EST: EST (Waktu Standar Timur) + </option> + + + <option + value="Etc/GMT-7" + > + Etc/GMT-7: GMT+7 (GMT+07.00) + </option> + + + <option + value="Europe/Brussels" + > + Europe/Brussels: GMT+1 (Waktu Standar Eropa Tengah) + </option> + + + <option + value="GMT" + > + GMT: UTC (Waktu Universal Terkoordinasi) + </option> + + + <option + value="Indian/Maldives" + > + Indian/Maldives: GMT+5 (Waktu Maladewa) + </option> + + + <option + value="Pacific/Tarawa" + > + Pacific/Tarawa: GMT+12 (Waktu Kep. Gilbert) + </option> + + + <option + value="UTC" + > + UTC: UTC (Waktu Universal Terkoordinasi) + </option> + + + <option + value="Universal" + > + Universal: UTC (Waktu Universal Terkoordinasi) + </option> + + + <option + value="Zulu" + > + Zulu: UTC (Waktu Universal Terkoordinasi) + </option> + + +</select> +`; + +exports[`LocaleController should skip updating the default option if server time zone is not provided 1`] = ` +<select + data-action="w-init:ready->w-locale#localizeTimeZoneOptions" + data-controller="w-locale" + name="locale-current_time_zone" +> + + + <option + selected="" + value="" + > + Use server time zone + </option> + + + <option + value="Africa/Abidjan" + > + Africa/Abidjan: غرينتش (توقيت غرينتش) + </option> + + + <option + value="America/Argentina/Jujuy" + > + America/Argentina/Jujuy: غرينتش-٣ (توقيت الأرجنتين الرسمي) + </option> + + + <option + value="America/Indiana/Knox" + > + America/Indiana/Knox: غرينتش-٦ (التوقيت الرسمي المركزي لأمريكا الشمالية) + </option> + + + <option + value="Antarctica/Rothera" + > + Antarctica/Rothera: غرينتش-٣ (توقيت روثيرا) + </option> + + + <option + value="Arctic/Longyearbyen" + > + Arctic/Longyearbyen: غرينتش+١ (توقيت وسط أوروبا الرسمي) + </option> + + + <option + value="Asia/Katmandu" + > + Asia/Katmandu: غرينتش+٥:٤٥ (توقيت نيبال) + </option> + + + <option + value="Atlantic/Canary" + > + Atlantic/Canary: غرينتش (توقيت غرب أوروبا الرسمي) + </option> + + + <option + value="Australia/South" + > + Australia/South: غرينتش+١٠:٣٠ (توقيت وسط أستراليا الصيفي) + </option> + + + <option + value="Brazil/East" + > + Brazil/East: غرينتش-٣ (توقيت برازيليا الرسمي) + </option> + + + <option + value="Canada/Atlantic" + > + Canada/Atlantic: غرينتش-٤ (التوقيت الرسمي الأطلسي) + </option> + + + <option + value="Chile/Continental" + > + Chile/Continental: غرينتش-٣ (توقيت تشيلي الصيفي) + </option> + + + <option + value="EST" + > + EST: غرينتش-٥ (التوقيت الرسمي الشرقي لأمريكا الشمالية) + </option> + + + <option + value="Etc/GMT-7" + > + Etc/GMT-7: غرينتش+٧ (غرينتش+٠٧:٠٠) + </option> + + + <option + value="Europe/Brussels" + > + Europe/Brussels: غرينتش+١ (توقيت وسط أوروبا الرسمي) + </option> + + + <option + value="GMT" + > + GMT: UTC (التوقيت العالمي المنسق) + </option> + + + <option + value="Indian/Maldives" + > + Indian/Maldives: غرينتش+٥ (توقيت جزر المالديف) + </option> + + + <option + value="Pacific/Tarawa" + > + Pacific/Tarawa: غرينتش+١٢ (توقيت جزر جيلبرت) + </option> + + + <option + value="UTC" + > + UTC: UTC (التوقيت العالمي المنسق) + </option> + + + <option + value="Universal" + > + Universal: UTC (التوقيت العالمي المنسق) + </option> + + + <option + value="Zulu" + > + Zulu: UTC (التوقيت العالمي المنسق) + </option> + + +</select> +`; diff --git a/client/src/controllers/index.ts b/client/src/controllers/index.ts index d274a900db..fbb72093a2 100644 --- a/client/src/controllers/index.ts +++ b/client/src/controllers/index.ts @@ -32,6 +32,7 @@ import { TooltipController } from './TooltipController'; import { UnsavedController } from './UnsavedController'; import { UpgradeController } from './UpgradeController'; import { ZoneController } from './ZoneController'; +import { LocaleController } from './LocaleController'; /** * Important: Only add default core controllers that should load with the base admin JS bundle. @@ -53,6 +54,7 @@ export const coreControllerDefinitions: Definition[] = [ { controllerConstructor: FormsetController, identifier: 'w-formset' }, { controllerConstructor: InitController, identifier: 'w-init' }, { controllerConstructor: KeyboardController, identifier: 'w-kbd' }, + { controllerConstructor: LocaleController, identifier: 'w-locale' }, { controllerConstructor: OrderableController, identifier: 'w-orderable' }, { controllerConstructor: PreviewController, identifier: 'w-preview' }, { controllerConstructor: ProgressController, identifier: 'w-progress' }, diff --git a/setup.py b/setup.py index 467f34dbee..70564c8f08 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ install_requires = [ "beautifulsoup4>=4.8,<4.13", "Willow[heif]>=1.8.0,<2", "requests>=2.11.1,<3.0", - "l18n>=2018.5", "openpyxl>=3.0.10,<4.0", "anyascii>=0.1.5", "telepath>=0.3.1,<1", diff --git a/wagtail/admin/auth.py b/wagtail/admin/auth.py index d3e5282599..c97cb46e4c 100644 --- a/wagtail/admin/auth.py +++ b/wagtail/admin/auth.py @@ -1,7 +1,6 @@ import types from functools import wraps -import l18n from django.conf import settings from django.core.exceptions import PermissionDenied from django.shortcuts import redirect @@ -141,7 +140,6 @@ def require_admin_access(view_func): preferred_language = ( user.wagtail_userprofile.get_preferred_language() ) - l18n.set_language(preferred_language) time_zone = user.wagtail_userprofile.get_current_time_zone() else: time_zone = settings.TIME_ZONE diff --git a/wagtail/admin/forms/account.py b/wagtail/admin/forms/account.py index 9733595c3d..268801086a 100644 --- a/wagtail/admin/forms/account.py +++ b/wagtail/admin/forms/account.py @@ -1,8 +1,7 @@ import warnings -from operator import itemgetter -import l18n from django import forms +from django.conf import settings from django.contrib.auth import get_user_model from django.db.models.fields import BLANK_CHOICE_DASH from django.utils.translation import get_language_info @@ -59,12 +58,9 @@ def _get_language_choices(): def _get_time_zone_choices(): - time_zones = [ - (tz, str(l18n.tz_fullnames.get(tz, tz))) - for tz in get_available_admin_time_zones() + return [("", _("Use server time zone"))] + [ + (tz, tz) for tz in get_available_admin_time_zones() ] - time_zones.sort(key=itemgetter(1)) - return BLANK_CHOICE_DASH + time_zones class LocalePreferencesForm(forms.ModelForm): @@ -82,7 +78,16 @@ class LocalePreferencesForm(forms.ModelForm): ) current_time_zone = forms.ChoiceField( - required=False, choices=_get_time_zone_choices, label=_("Current time zone") + required=False, + choices=_get_time_zone_choices, + label=_("Current time zone"), + widget=forms.Select( + attrs={ + "data-controller": "w-init w-locale", + "data-action": "w-init:ready->w-locale#localizeTimeZoneOptions", + "data-w-locale-server-time-zone-param": settings.TIME_ZONE, + }, + ), ) class Meta: diff --git a/wagtail/admin/tests/test_account_management.py b/wagtail/admin/tests/test_account_management.py index b73ce52048..367e63c074 100644 --- a/wagtail/admin/tests/test_account_management.py +++ b/wagtail/admin/tests/test_account_management.py @@ -588,6 +588,22 @@ class TestAccountSection( sorted(zoneinfo.available_timezones()), ) + response = self.client.get(reverse("wagtailadmin_account")) + self.assertEqual(response.status_code, 200) + soup = self.get_soup(response.content) + + select = soup.select_one('select[name="locale-current_time_zone"]') + self.assertIsNotNone(select) + self.assertEqual(select.get("data-controller"), "w-init w-locale") + self.assertEqual( + select.get("data-action"), + "w-init:ready->w-locale#localizeTimeZoneOptions", + ) + self.assertEqual( + select.get("data-w-locale-server-time-zone-param"), + settings.TIME_ZONE, + ) + @unittest.skipUnless(settings.USE_TZ, "Timezone support is disabled") @override_settings(WAGTAIL_USER_TIME_ZONES=["Europe/London"]) def test_not_show_options_if_only_one_time_zone_is_permitted(self):