diff --git a/.gitignore b/.gitignore index 4a09f882ea..c719d39134 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ # Pycharm Files .idea/ +# FVM +.fvm/ + # mac specific .DS_Store *.bkp diff --git a/packages/flet/lib/src/controls/page.dart b/packages/flet/lib/src/controls/page.dart index d8cf0d7dfc..f54a55aad4 100644 --- a/packages/flet/lib/src/controls/page.dart +++ b/packages/flet/lib/src/controls/page.dart @@ -65,6 +65,7 @@ class _PageControlState extends State with WidgetsBindingObserver { Control? _windowControl; bool? _prevOnKeyboardEvent; bool _keyboardHandlerSubscribed = false; + List? _locales; String? _prevViewRoutes; final Set _pendingPoppedViewRoutes = {}; final Set _sentViewPopEventsForRoutes = {}; @@ -151,6 +152,25 @@ class _PageControlState extends State with WidgetsBindingObserver { _updateMultiViews(); } + @override + void didChangeLocales(List? locales) { + super.didChangeLocales(locales); + + final effectiveLocales = + locales ?? WidgetsBinding.instance.platformDispatcher.locales; + final nextLocales = List.unmodifiable(effectiveLocales); + if (_locales != null && + const ListEquality().equals(_locales!, nextLocales)) { + return; + } + + _locales = nextLocales; + widget.control.triggerEvent( + 'locale_change', + {'locales': nextLocales.map((l) => l.toMap()).toList(growable: false)}, + ); + } + @override void dispose() { debugPrint("Page.dispose: ${widget.control.id}"); diff --git a/packages/flet/lib/src/utils/device_info.dart b/packages/flet/lib/src/utils/device_info.dart index aab08ee9c5..25de2a4392 100644 --- a/packages/flet/lib/src/utils/device_info.dart +++ b/packages/flet/lib/src/utils/device_info.dart @@ -1,7 +1,10 @@ +import 'dart:ui'; + import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flet/src/utils/locale.dart'; import 'package:flutter/services.dart'; -import 'enums.dart'; +import 'enums.dart'; import 'platform.dart'; /// Returns device information as a Map. @@ -10,10 +13,15 @@ Future> getDeviceInfo() async { return deviceInfo.asMap(); } +List> getDeviceLocales() => + PlatformDispatcher.instance.locales + .map((locale) => locale.toMap()) + .toList(); + extension DeviceInfoExtension on BaseDeviceInfo { Map asMap() { var deviceInfo = this; - + final deviceLocales = getDeviceLocales(); if (isWebPlatform()) { deviceInfo = (deviceInfo as WebBrowserInfo); return { @@ -32,6 +40,7 @@ extension DeviceInfoExtension on BaseDeviceInfo { "vendor_sub": deviceInfo.vendorSub, "max_touch_points": deviceInfo.maxTouchPoints, "hardware_concurrency": deviceInfo.hardwareConcurrency, + "locales": deviceLocales, }; } else { if (isAndroidMobile()) { @@ -71,6 +80,7 @@ extension DeviceInfoExtension on BaseDeviceInfo { 'preview_sdk': deviceInfo.version.previewSdkInt, 'security_patch': deviceInfo.version.securityPatch, }, + "locales": deviceLocales, }; } else if (isIOSMobile()) { deviceInfo = (deviceInfo as IosDeviceInfo); @@ -95,6 +105,7 @@ extension DeviceInfoExtension on BaseDeviceInfo { "version": deviceInfo.utsname.version, }, "identifier_for_vendor": deviceInfo.identifierForVendor, + "locales": deviceLocales, }; } else if (isLinuxDesktop()) { deviceInfo = (deviceInfo as LinuxDeviceInfo); @@ -110,6 +121,7 @@ extension DeviceInfoExtension on BaseDeviceInfo { "variant": deviceInfo.variant, "variant_id": deviceInfo.variantId, "machine_id": deviceInfo.machineId, + "locales": deviceLocales, }; } else if (isMacOSDesktop()) { deviceInfo = (deviceInfo as MacOsDeviceInfo); @@ -128,6 +140,7 @@ extension DeviceInfoExtension on BaseDeviceInfo { "os_release": deviceInfo.osRelease, "patch_version": deviceInfo.patchVersion, "system_guid": deviceInfo.systemGUID, + "locales": deviceLocales, }; } else if (isWindowsDesktop()) { deviceInfo = (deviceInfo as WindowsDeviceInfo); @@ -157,6 +170,7 @@ extension DeviceInfoExtension on BaseDeviceInfo { "registered_owner": deviceInfo.registeredOwner, "release_id": deviceInfo.releaseId, "device_id": deviceInfo.deviceId, + "locales": deviceLocales, }; } return {}; diff --git a/packages/flet/lib/src/utils/locale.dart b/packages/flet/lib/src/utils/locale.dart index d2cdcbabe8..b1260ca332 100644 --- a/packages/flet/lib/src/utils/locale.dart +++ b/packages/flet/lib/src/utils/locale.dart @@ -72,4 +72,12 @@ extension LocaleExtention on Locale { ]]) { return delegates.every((d) => d.isSupported(this)); } + + Map toMap() { + return { + "language_code": languageCode, + "country_code": countryCode, + "script_code": scriptCode, + }; + } } diff --git a/sdk/python/examples/controls/page/device_locale.py b/sdk/python/examples/controls/page/device_locale.py new file mode 100644 index 0000000000..e240c53f18 --- /dev/null +++ b/sdk/python/examples/controls/page/device_locale.py @@ -0,0 +1,26 @@ +import flet as ft + + +async def main(page: ft.Page): + def format_locales(locales: list[ft.Locale]) -> str: + """Format locale list for display.""" + return ", ".join(str(loc) for loc in locales) + + def handle_locale_change(e: ft.LocaleChangeEvent): + page.add(ft.Text(f"Locales changed: {format_locales(e.locales)}")) + + page.on_locale_change = handle_locale_change + page.scroll = ft.ScrollMode.AUTO + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + + initial_locales = (await page.get_device_info()).locales + page.add( + ft.Text(f"Initial locales: {format_locales(initial_locales)}"), + ft.Text( + "Change your system or browser language to trigger on_locale_change.", + color=ft.Colors.BLUE, + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/page/window_hidden_on_start.py b/sdk/python/examples/controls/page/window_hidden_on_start.py index 331bc2b459..1750371282 100644 --- a/sdk/python/examples/controls/page/window_hidden_on_start.py +++ b/sdk/python/examples/controls/page/window_hidden_on_start.py @@ -1,7 +1,7 @@ # # Use -n (--hidden) option to run this example with `flet run` command: # -# flet run -n examples/controls/page/window_hidden_on_start.py +# flet run --hidden window_hidden_on_start.py # import asyncio @@ -12,11 +12,17 @@ async def main(page: ft.Page): print("Window is hidden on start. Will show after 3 seconds...") page.add(ft.Text("Hello!")) + + # some configuration that we want to do before showing the window page.window.width = 300 page.window.height = 200 page.update() await page.window.center() + + # wait for 3 seconds before showing the window await asyncio.sleep(3) + + # show the window page.window.visible = True page.update() diff --git a/sdk/python/packages/flet/docs/controls/page.md b/sdk/python/packages/flet/docs/controls/page.md index 2cbe5d586e..b2a5e58740 100644 --- a/sdk/python/packages/flet/docs/controls/page.md +++ b/sdk/python/packages/flet/docs/controls/page.md @@ -50,4 +50,10 @@ If you need this feature when packaging a desktop app using --8<-- "{{ examples }}/semantics_debugger.py" ``` +### Get device locales + +```python +--8<-- "{{ examples }}/device_locale.py" +``` + {{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/publish/index.md b/sdk/python/packages/flet/docs/publish/index.md index 2ea412ceb8..89b8810590 100644 --- a/sdk/python/packages/flet/docs/publish/index.md +++ b/sdk/python/packages/flet/docs/publish/index.md @@ -846,6 +846,7 @@ Its value is determined in the following order of precedence: - `[tool.flet..app].hide_window_on_start`, where `` can be `windows`, `macos` or `linux` - `[tool.flet.app].hide_window_on_start` +- [`FLET_HIDE_WINDOW_ON_START`](../reference/environment-variables.md#flet_hide_window_on_start) #### Example diff --git a/sdk/python/packages/flet/docs/reference/environment-variables.md b/sdk/python/packages/flet/docs/reference/environment-variables.md index bf3ac35a49..da55bf7baa 100644 --- a/sdk/python/packages/flet/docs/reference/environment-variables.md +++ b/sdk/python/packages/flet/docs/reference/environment-variables.md @@ -44,6 +44,12 @@ Whether to skip running `flutter doctor` when a build fails. Defaults to `False`. +### `FLET_HIDE_WINDOW_ON_START` + +Set to `true` to start app with the main window hidden. + +Defaults to `False`. + ### `FLET_FORCE_WEB_SERVER` Set to `true` to force running app as a web app. Automatically set on headless Linux hosts. diff --git a/sdk/python/packages/flet/docs/types/localechangeevent.md b/sdk/python/packages/flet/docs/types/localechangeevent.md new file mode 100644 index 0000000000..d46bf019d0 --- /dev/null +++ b/sdk/python/packages/flet/docs/types/localechangeevent.md @@ -0,0 +1 @@ +{{ class_all_options("flet.LocaleChangeEvent") }} diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index 6484c5990b..146295cd35 100644 --- a/sdk/python/packages/flet/mkdocs.yml +++ b/sdk/python/packages/flet/mkdocs.yml @@ -922,6 +922,7 @@ nav: - KeyDownEvent: types/keydownevent.md - KeyRepeatEvent: types/keyrepeatevent.md - KeyUpEvent: types/keyupevent.md + - LocaleChangeEvent: types/localechangeevent.md - LoginEvent: types/loginevent.md - LongPressEndEvent: types/longpressendevent.md - LongPressStartEvent: types/longpressstartevent.md diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index 93db7e72b9..daf0b5d857 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -396,6 +396,7 @@ from flet.controls.page import ( AppLifecycleStateChangeEvent, KeyboardEvent, + LocaleChangeEvent, LoginEvent, MultiViewAddEvent, MultiViewRemoveEvent, @@ -839,6 +840,7 @@ "ListTileTitleAlignment", "ListView", "Locale", + "LocaleChangeEvent", "LocaleConfiguration", "LoginEvent", "LongPressDownEvent", diff --git a/sdk/python/packages/flet/src/flet/controls/device_info.py b/sdk/python/packages/flet/src/flet/controls/device_info.py index d7856f35b4..99cfb5f79e 100644 --- a/sdk/python/packages/flet/src/flet/controls/device_info.py +++ b/sdk/python/packages/flet/src/flet/controls/device_info.py @@ -16,8 +16,10 @@ "WindowsDeviceInfo", ] +from flet.controls.types import Locale -@dataclass + +@dataclass(kw_only=True) class DeviceInfo: """ Base class for device information. @@ -31,8 +33,22 @@ class DeviceInfo: - [`WindowsDeviceInfo`][flet.] """ + locales: list[Locale] + """ + The full system-reported supported locales of the device. -@dataclass + This establishes the language and formatting conventions that application + should, if possible, use to render their user interface. + + The list is ordered in order of priority, with lower-indexed locales being + preferred over higher-indexed ones. The first element is the primary locale. + + The [`Page.on_locale_change`][flet.] event is called + whenever this value changes. + """ + + +@dataclass(kw_only=True) class MacOsDeviceInfo(DeviceInfo): """ Device information snapshot for macOS hosts. @@ -130,7 +146,7 @@ class WebBrowserName(Enum): """Unknown web browser""" -@dataclass +@dataclass(kw_only=True) class WebDeviceInfo(DeviceInfo): """ Information derived from `navigator`. @@ -257,7 +273,7 @@ class WebDeviceInfo(DeviceInfo): """ -@dataclass +@dataclass(kw_only=True) class AndroidBuildVersion: """ Android OS version details derived from `android.os.Build.VERSION`. @@ -306,7 +322,7 @@ class AndroidBuildVersion: """ -@dataclass +@dataclass(kw_only=True) class AndroidDeviceInfo(DeviceInfo): """ Device information snapshot for Android devices and emulators. @@ -466,7 +482,7 @@ class AndroidDeviceInfo(DeviceInfo): """ -@dataclass +@dataclass(kw_only=True) class LinuxDeviceInfo(DeviceInfo): """ Device information for a Linux system. @@ -601,7 +617,7 @@ class LinuxDeviceInfo(DeviceInfo): """ -@dataclass +@dataclass(kw_only=True) class WindowsDeviceInfo(DeviceInfo): """ Device information snapshot for Windows systems. @@ -776,7 +792,7 @@ class IosUtsname: """Version level.""" -@dataclass +@dataclass(kw_only=True) class IosDeviceInfo(DeviceInfo): """ Device information snapshot for iOS/iPadOS runtimes. diff --git a/sdk/python/packages/flet/src/flet/controls/page.py b/sdk/python/packages/flet/src/flet/controls/page.py index d37290580f..a41c7be8b5 100644 --- a/sdk/python/packages/flet/src/flet/controls/page.py +++ b/sdk/python/packages/flet/src/flet/controls/page.py @@ -57,6 +57,7 @@ AppLifecycleState, Brightness, DeviceOrientation, + Locale, PagePlatform, Url, UrlTarget, @@ -175,6 +176,24 @@ class PlatformBrightnessChangeEvent(Event["Page"]): """ +@dataclass +class LocaleChangeEvent(Event["Page"]): + """ + Event payload describing a change in the host platform's locale preferences. + + See also: + - [`Page.on_locale_change`][flet.]: event called when locale preferences/settings + of the host platform have changed. + """ + + locales: list[Locale] + """ + The full, ordered list of locales reported by the host platform. + + The first item represents the highest-priority locale. + """ + + @dataclass class ViewPopEvent(Event["Page"]): """ @@ -460,11 +479,19 @@ class Page(BasePage): Called when brightness of app host platform has changed. """ + on_locale_change: Optional[EventHandler[LocaleChangeEvent]] = None + """ + Called when the locale preferences/settings of the host platform have changed. + + For example, when the user updates device language + settings or browser preferred languages. + """ + on_app_lifecycle_state_change: Optional[ EventHandler[AppLifecycleStateChangeEvent] ] = None """ - Triggers when app lifecycle state changes. + Called when app lifecycle state changes. """ on_route_change: Optional[EventHandler[RouteChangeEvent]] = None diff --git a/sdk/python/packages/flet/src/flet/controls/types.py b/sdk/python/packages/flet/src/flet/controls/types.py index 513a2e5158..8dc7abadf4 100644 --- a/sdk/python/packages/flet/src/flet/controls/types.py +++ b/sdk/python/packages/flet/src/flet/controls/types.py @@ -1144,6 +1144,42 @@ def __post_init__(self): if self.language_code == "": raise ValueError("language_code cannot be empty") + @property + def language_tag(self) -> str: + """ + Returns a syntactically valid Unicode BCP47 Locale Identifier. + + See [this](https://www.unicode.org/reports/tr35) for technical details. + + Examples: `en`, `es-419`, `hi-Deva-IN`, `zh-Hans-CN` + """ + return self._raw_to_string("-") + + def __str__(self) -> str: + return self._raw_to_string("_") + + def _raw_to_string(self, separator: str) -> str: + """Returns the locale identifier joined by the given separator. + + Components are ordered as language, script (if any), and country + (if any). Empty or `None` values are omitted. + + Args: + separator: String used to join the subtags. + + Returns: + The formatted locale identifier. + """ + out_parts: list[str] = [self.language_code] + + if self.script_code is not None and self.script_code != "": + out_parts.append(self.script_code) + + if self.country_code is not None and self.country_code != "": + out_parts.append(self.country_code) + + return separator.join(out_parts) + @dataclass class LocaleConfiguration: