Flutter Localization for Desktop Apps: Windows, macOS, and Linux Guide
Building a multi-language Flutter desktop application requires understanding platform-specific nuances that don't exist in mobile development. This comprehensive guide covers everything you need to know about localizing Flutter apps for Windows, macOS, and Linux.
Why Desktop Localization Is Different
Desktop applications face unique localization challenges that mobile apps don't encounter:
- System menu integration - Desktop apps have native menus that need localization
- Keyboard shortcuts - Different keyboard layouts across regions
- File system paths - Localized folder names on different operating systems
- Window titles - Dynamic title bar text
- System dialogs - Native file pickers and alerts
- Right-click context menus - Platform-native context menus
Setting Up Desktop Localization
Step 1: Enable Desktop Support
First, ensure desktop platforms are enabled:
flutter config --enable-windows-desktop
flutter config --enable-macos-desktop
flutter config --enable-linux-desktop
Step 2: Configure l10n.yaml
Create your localization configuration:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
synthetic-package: false
output-dir: lib/l10n/generated
nullable-getter: false
Step 3: Create ARB Files
Create lib/l10n/app_en.arb:
{
"@@locale": "en",
"appTitle": "My Desktop App",
"@appTitle": {
"description": "The title of the application shown in window title bar"
},
"menuFile": "File",
"@menuFile": {
"description": "File menu label"
},
"menuFileNew": "New",
"menuFileOpen": "Open...",
"menuFileSave": "Save",
"menuFileSaveAs": "Save As...",
"menuFileExit": "Exit",
"menuEdit": "Edit",
"menuEditUndo": "Undo",
"menuEditRedo": "Redo",
"menuEditCut": "Cut",
"menuEditCopy": "Copy",
"menuEditPaste": "Paste",
"menuHelp": "Help",
"menuHelpAbout": "About",
"dialogSaveChanges": "Do you want to save changes?",
"dialogSaveChangesDescription": "Your changes will be lost if you don't save them.",
"buttonSave": "Save",
"buttonDontSave": "Don't Save",
"buttonCancel": "Cancel",
"fileFilterAllFiles": "All Files",
"fileFilterDocuments": "Documents"
}
Platform-Specific Considerations
Windows Localization
Windows has specific requirements for desktop apps:
Window Title Localization
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'l10n/generated/app_localizations.dart';
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WindowListener {
@override
void initState() {
super.initState();
windowManager.addListener(this);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
builder: (context, child) {
// Update window title when locale changes
_updateWindowTitle(context);
return child!;
},
home: HomePage(),
);
}
void _updateWindowTitle(BuildContext context) {
final l10n = AppLocalizations.of(context);
windowManager.setTitle(l10n.appTitle);
}
}
Native Menu Bar
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'l10n/generated/app_localizations.dart';
class LocalizedMenuBar extends StatelessWidget {
final Widget child;
const LocalizedMenuBar({required this.child});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return PlatformMenuBar(
menus: [
PlatformMenu(
label: l10n.menuFile,
menus: [
PlatformMenuItem(
label: l10n.menuFileNew,
shortcut: const SingleActivator(LogicalKeyboardKey.keyN, control: true),
onSelected: () => _handleNew(),
),
PlatformMenuItem(
label: l10n.menuFileOpen,
shortcut: const SingleActivator(LogicalKeyboardKey.keyO, control: true),
onSelected: () => _handleOpen(),
),
PlatformMenuItem(
label: l10n.menuFileSave,
shortcut: const SingleActivator(LogicalKeyboardKey.keyS, control: true),
onSelected: () => _handleSave(),
),
PlatformMenuItemGroup(
members: [
PlatformMenuItem(
label: l10n.menuFileExit,
onSelected: () => _handleExit(),
),
],
),
],
),
PlatformMenu(
label: l10n.menuEdit,
menus: [
PlatformMenuItem(
label: l10n.menuEditUndo,
shortcut: const SingleActivator(LogicalKeyboardKey.keyZ, control: true),
onSelected: () => _handleUndo(),
),
PlatformMenuItem(
label: l10n.menuEditRedo,
shortcut: const SingleActivator(LogicalKeyboardKey.keyY, control: true),
onSelected: () => _handleRedo(),
),
],
),
],
child: child,
);
}
}
macOS Localization
macOS has its own conventions:
Localized Menu Items
macOS uses Command (⌘) instead of Ctrl:
PlatformMenuItem(
label: l10n.menuFileSave,
shortcut: const SingleActivator(
LogicalKeyboardKey.keyS,
meta: true, // Command key on macOS
),
onSelected: () => _handleSave(),
),
macOS App Bundle Localization
Add localized strings to macos/Runner/Base.lproj/InfoPlist.strings:
CFBundleName = "My App";
CFBundleDisplayName = "My Application";
For each language, create macos/Runner/de.lproj/InfoPlist.strings:
CFBundleName = "Meine App";
CFBundleDisplayName = "Meine Anwendung";
Linux Localization
Linux desktop environments have their own requirements:
Desktop Entry Localization
Create .desktop files with translations:
[Desktop Entry]
Name=My App
Name[de]=Meine App
Name[fr]=Mon Application
Name[es]=Mi Aplicación
Comment=A Flutter desktop application
Comment[de]=Eine Flutter-Desktop-Anwendung
Comment[fr]=Une application de bureau Flutter
Comment[es]=Una aplicación de escritorio Flutter
Exec=my_app
Icon=my_app
Type=Application
Categories=Utility;
Handling System Locale Changes
Desktop apps should respond to system locale changes:
import 'dart:ui' as ui;
class LocaleObserver extends WidgetsBindingObserver {
final Function(List<Locale>) onLocaleChanged;
LocaleObserver(this.onLocaleChanged);
@override
void didChangeLocales(List<Locale>? locales) {
if (locales != null && locales.isNotEmpty) {
onLocaleChanged(locales);
}
}
}
class _MyAppState extends State<MyApp> {
late LocaleObserver _localeObserver;
Locale? _currentLocale;
@override
void initState() {
super.initState();
_localeObserver = LocaleObserver(_handleLocaleChange);
WidgetsBinding.instance.addObserver(_localeObserver);
}
void _handleLocaleChange(List<Locale> locales) {
setState(() {
_currentLocale = locales.first;
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(_localeObserver);
super.dispose();
}
}
File Picker Localization
Desktop apps frequently use file dialogs:
import 'package:file_picker/file_picker.dart';
import 'l10n/generated/app_localizations.dart';
Future<String?> pickFile(BuildContext context) async {
final l10n = AppLocalizations.of(context);
final result = await FilePicker.platform.pickFiles(
dialogTitle: l10n.dialogOpenFile,
type: FileType.custom,
allowedExtensions: ['txt', 'md', 'json'],
);
return result?.files.single.path;
}
Future<String?> saveFile(BuildContext context) async {
final l10n = AppLocalizations.of(context);
final result = await FilePicker.platform.saveFile(
dialogTitle: l10n.dialogSaveFile,
fileName: l10n.defaultFileName,
type: FileType.custom,
allowedExtensions: ['txt'],
);
return result;
}
Keyboard Shortcut Considerations
Different keyboard layouts require different shortcuts:
class LocalizedShortcuts {
static SingleActivator getSaveShortcut() {
if (Platform.isMacOS) {
return const SingleActivator(LogicalKeyboardKey.keyS, meta: true);
}
return const SingleActivator(LogicalKeyboardKey.keyS, control: true);
}
static SingleActivator getUndoShortcut() {
if (Platform.isMacOS) {
return const SingleActivator(LogicalKeyboardKey.keyZ, meta: true);
}
return const SingleActivator(LogicalKeyboardKey.keyZ, control: true);
}
// Handle keyboard layout differences
static String getShortcutLabel(SingleActivator shortcut, Locale locale) {
final buffer = StringBuffer();
if (shortcut.control) {
buffer.write(Platform.isMacOS ? '⌃' : 'Ctrl+');
}
if (shortcut.meta) {
buffer.write('⌘');
}
if (shortcut.alt) {
buffer.write(Platform.isMacOS ? '⌥' : 'Alt+');
}
if (shortcut.shift) {
buffer.write(Platform.isMacOS ? '⇧' : 'Shift+');
}
buffer.write(shortcut.trigger.keyLabel);
return buffer.toString();
}
}
System Tray Localization
For apps with system tray icons:
import 'package:system_tray/system_tray.dart';
import 'l10n/generated/app_localizations.dart';
class LocalizedSystemTray {
final SystemTray _systemTray = SystemTray();
Future<void> initSystemTray(BuildContext context) async {
final l10n = AppLocalizations.of(context);
await _systemTray.initSystemTray(
title: l10n.appTitle,
iconPath: 'assets/app_icon.ico',
toolTip: l10n.systemTrayTooltip,
);
final menu = Menu();
await menu.buildFrom([
MenuItemLabel(
label: l10n.menuShow,
onClicked: (menuItem) => _showWindow(),
),
MenuSeparator(),
MenuItemLabel(
label: l10n.menuExit,
onClicked: (menuItem) => _exitApp(),
),
]);
await _systemTray.setContextMenu(menu);
}
}
Testing Desktop Localization
Test your desktop localization thoroughly:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'l10n/generated/app_localizations.dart';
void main() {
testWidgets('Menu items are localized in German', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: const Locale('de'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: MenuTestWidget(),
),
);
expect(find.text('Datei'), findsOneWidget); // File in German
expect(find.text('Bearbeiten'), findsOneWidget); // Edit in German
});
testWidgets('Window title updates with locale', (tester) async {
await tester.pumpWidget(
MaterialApp(
locale: const Locale('fr'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) {
final l10n = AppLocalizations.of(context);
return Text(l10n.appTitle);
},
),
),
);
expect(find.text('Mon Application'), findsOneWidget);
});
}
Best Practices for Desktop Localization
1. Use Platform-Aware Shortcuts
String getShortcutHint(BuildContext context, String action) {
final l10n = AppLocalizations.of(context);
final modifier = Platform.isMacOS ? '⌘' : 'Ctrl';
switch (action) {
case 'save':
return '${l10n.menuFileSave} ($modifier+S)';
case 'open':
return '${l10n.menuFileOpen} ($modifier+O)';
default:
return action;
}
}
2. Handle Text Expansion
Desktop UIs often have fixed-width menus:
class MenuSizer {
static double calculateMenuWidth(List<String> items) {
double maxWidth = 150.0; // Minimum width
for (final item in items) {
final textPainter = TextPainter(
text: TextSpan(text: item),
textDirection: TextDirection.ltr,
)..layout();
maxWidth = max(maxWidth, textPainter.width + 50); // Add padding
}
return maxWidth;
}
}
3. Localize Error Messages
String getErrorMessage(BuildContext context, Exception error) {
final l10n = AppLocalizations.of(context);
if (error is FileSystemException) {
return l10n.errorFileAccess(error.path ?? '');
}
if (error is SocketException) {
return l10n.errorNetwork;
}
return l10n.errorUnknown;
}
Conclusion
Desktop localization in Flutter requires attention to platform-specific details that mobile development doesn't require. By following these guidelines, you can create desktop applications that feel native and natural to users across Windows, macOS, and Linux in any language.
Key takeaways:
- Localize window titles - Update dynamically with locale changes
- Handle platform menus - Use PlatformMenuBar with localized strings
- Consider keyboard layouts - Different platforms use different modifiers
- Test on all platforms - Each OS has unique requirements
- Use platform conventions - Command on Mac, Ctrl on Windows/Linux
FlutterLocalisation makes managing translations across desktop platforms simple with AI-powered translation and instant sync. Try it free to streamline your desktop app localization.