← Back to Blog

Flutter Localization for Desktop Apps: Windows, macOS, and Linux Guide

flutterdesktopwindowsmacoslinuxlocalization

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.