Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
# Pycharm Files
.idea/

# FVM
.fvm/

# mac specific
.DS_Store
*.bkp
Expand Down
20 changes: 20 additions & 0 deletions packages/flet/lib/src/controls/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
Control? _windowControl;
bool? _prevOnKeyboardEvent;
bool _keyboardHandlerSubscribed = false;
List<Locale>? _locales;
String? _prevViewRoutes;
final Set<String> _pendingPoppedViewRoutes = <String>{};
final Set<String> _sentViewPopEventsForRoutes = <String>{};
Expand Down Expand Up @@ -151,6 +152,25 @@ class _PageControlState extends State<PageControl> with WidgetsBindingObserver {
_updateMultiViews();
}

@override
void didChangeLocales(List<Locale>? locales) {
super.didChangeLocales(locales);

final effectiveLocales =
locales ?? WidgetsBinding.instance.platformDispatcher.locales;
final nextLocales = List<Locale>.unmodifiable(effectiveLocales);
if (_locales != null &&
const ListEquality<Locale>().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}");
Expand Down
18 changes: 16 additions & 2 deletions packages/flet/lib/src/utils/device_info.dart
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -10,10 +13,15 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
return deviceInfo.asMap();
}

List<Map<String, String?>> getDeviceLocales() =>
PlatformDispatcher.instance.locales
.map((locale) => locale.toMap())
.toList();

extension DeviceInfoExtension on BaseDeviceInfo {
Map<String, dynamic> asMap() {
var deviceInfo = this;

final deviceLocales = getDeviceLocales();
if (isWebPlatform()) {
deviceInfo = (deviceInfo as WebBrowserInfo);
return {
Expand All @@ -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()) {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -157,6 +170,7 @@ extension DeviceInfoExtension on BaseDeviceInfo {
"registered_owner": deviceInfo.registeredOwner,
"release_id": deviceInfo.releaseId,
"device_id": deviceInfo.deviceId,
"locales": deviceLocales,
};
}
return {};
Expand Down
8 changes: 8 additions & 0 deletions packages/flet/lib/src/utils/locale.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,12 @@ extension LocaleExtention on Locale {
]]) {
return delegates.every((d) => d.isSupported(this));
}

Map<String, String?> toMap() {
return {
"language_code": languageCode,
"country_code": countryCode,
"script_code": scriptCode,
};
}
}
26 changes: 26 additions & 0 deletions sdk/python/examples/controls/page/device_locale.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 7 additions & 1 deletion sdk/python/examples/controls/page/window_hidden_on_start.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()

Expand Down
6 changes: 6 additions & 0 deletions sdk/python/packages/flet/docs/controls/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) }}
1 change: 1 addition & 0 deletions sdk/python/packages/flet/docs/publish/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ Its value is determined in the following order of precedence:

- `[tool.flet.<PLATFORM>.app].hide_window_on_start`, where `<PLATFORM>` 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions sdk/python/packages/flet/docs/types/localechangeevent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ class_all_options("flet.LocaleChangeEvent") }}
1 change: 1 addition & 0 deletions sdk/python/packages/flet/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/packages/flet/src/flet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@
from flet.controls.page import (
AppLifecycleStateChangeEvent,
KeyboardEvent,
LocaleChangeEvent,
LoginEvent,
MultiViewAddEvent,
MultiViewRemoveEvent,
Expand Down Expand Up @@ -839,6 +840,7 @@
"ListTileTitleAlignment",
"ListView",
"Locale",
"LocaleChangeEvent",
"LocaleConfiguration",
"LoginEvent",
"LongPressDownEvent",
Expand Down
32 changes: 24 additions & 8 deletions sdk/python/packages/flet/src/flet/controls/device_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
"WindowsDeviceInfo",
]

from flet.controls.types import Locale

@dataclass

@dataclass(kw_only=True)
class DeviceInfo:
"""
Base class for device information.
Expand All @@ -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.
Expand Down Expand Up @@ -130,7 +146,7 @@ class WebBrowserName(Enum):
"""Unknown web browser"""


@dataclass
@dataclass(kw_only=True)
class WebDeviceInfo(DeviceInfo):
"""
Information derived from `navigator`.
Expand Down Expand Up @@ -257,7 +273,7 @@ class WebDeviceInfo(DeviceInfo):
"""


@dataclass
@dataclass(kw_only=True)
class AndroidBuildVersion:
"""
Android OS version details derived from `android.os.Build.VERSION`.
Expand Down Expand Up @@ -306,7 +322,7 @@ class AndroidBuildVersion:
"""


@dataclass
@dataclass(kw_only=True)
class AndroidDeviceInfo(DeviceInfo):
"""
Device information snapshot for Android devices and emulators.
Expand Down Expand Up @@ -466,7 +482,7 @@ class AndroidDeviceInfo(DeviceInfo):
"""


@dataclass
@dataclass(kw_only=True)
class LinuxDeviceInfo(DeviceInfo):
"""
Device information for a Linux system.
Expand Down Expand Up @@ -601,7 +617,7 @@ class LinuxDeviceInfo(DeviceInfo):
"""


@dataclass
@dataclass(kw_only=True)
class WindowsDeviceInfo(DeviceInfo):
"""
Device information snapshot for Windows systems.
Expand Down Expand Up @@ -776,7 +792,7 @@ class IosUtsname:
"""Version level."""


@dataclass
@dataclass(kw_only=True)
class IosDeviceInfo(DeviceInfo):
"""
Device information snapshot for iOS/iPadOS runtimes.
Expand Down
29 changes: 28 additions & 1 deletion sdk/python/packages/flet/src/flet/controls/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
AppLifecycleState,
Brightness,
DeviceOrientation,
Locale,
PagePlatform,
Url,
UrlTarget,
Expand Down Expand Up @@ -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"]):
"""
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading