Flutter Localization Without Context: Access Translations Anywhere
One of the most common frustrations Flutter developers face is needing BuildContext to access translations. What if you need localized strings in a service class, repository, or utility function? This guide covers every technique to access Flutter localizations without context.
The Problem: Context Dependency
Flutter's standard localization approach requires BuildContext:
// Standard approach - requires context
final l10n = AppLocalizations.of(context)!;
Text(l10n.welcomeMessage);
But what about these scenarios?
// ❌ No context available in services
class ApiService {
String getErrorMessage() {
// How do I access translations here?
}
}
// ❌ No context in models
class User {
String get displayRole {
// Need localized role names
}
}
// ❌ No context in utilities
String formatCurrency(double amount) {
// Need locale-aware formatting
}
Solution 1: Global Navigator Key
The most popular approach uses a global navigator key to access context anywhere.
Setup
// lib/main.dart
import 'package:flutter/material.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey, // Attach the key
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomePage(),
);
}
}
Create a Localization Helper
// lib/helpers/localization_helper.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
class L10n {
static AppLocalizations get current {
final context = navigatorKey.currentContext;
if (context == null) {
throw Exception('Navigator context is null. Ensure the app is initialized.');
}
return AppLocalizations.of(context)!;
}
// Convenience getters for common strings
static String get welcomeMessage => current.welcomeMessage;
static String get errorGeneric => current.errorGeneric;
// Methods with parameters
static String greeting(String name) => current.greeting(name);
static String itemCount(int count) => current.itemCount(count);
}
Usage Anywhere
// In a service class - no context needed!
class ApiService {
Future<void> fetchData() async {
try {
final response = await http.get(uri);
// ...
} catch (e) {
throw Exception(L10n.current.networkError);
}
}
}
// In a utility function
String getErrorMessage(ErrorCode code) {
switch (code) {
case ErrorCode.network:
return L10n.current.networkError;
case ErrorCode.auth:
return L10n.current.authError;
default:
return L10n.current.unknownError;
}
}
Solution 2: GetIt Service Locator
For apps already using GetIt, this integrates naturally.
Setup
// lib/injection.dart
import 'package:get_it/get_it.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
final getIt = GetIt.instance;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void setupLocator() {
getIt.registerLazySingleton<AppLocalizations>(
() => AppLocalizations.of(navigatorKey.currentContext!)!,
);
}
// Convenience function
AppLocalizations get l10n => getIt<AppLocalizations>();
Usage
class OrderService {
String getOrderStatus(OrderStatus status) {
switch (status) {
case OrderStatus.pending:
return l10n.orderPending;
case OrderStatus.shipped:
return l10n.orderShipped;
case OrderStatus.delivered:
return l10n.orderDelivered;
}
}
}
Note: Re-register when locale changes:
void onLocaleChanged() {
getIt.unregister<AppLocalizations>();
getIt.registerLazySingleton<AppLocalizations>(
() => AppLocalizations.of(navigatorKey.currentContext!)!,
);
}
Solution 3: Static Lookup Table
For simple cases, use a static map that's populated on app start.
Setup
// lib/l10n/strings.dart
class Strings {
static late AppLocalizations _l10n;
static void init(BuildContext context) {
_l10n = AppLocalizations.of(context)!;
}
// Static getters
static String get appName => _l10n.appName;
static String get welcomeMessage => _l10n.welcomeMessage;
static String get loginButton => _l10n.loginButton;
static String get errorNetwork => _l10n.errorNetwork;
// Parameterized strings need methods
static String greeting(String name) => _l10n.greeting(name);
static String itemCount(int count) => _l10n.itemCount(count);
}
Initialize on App Start
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// ...
home: Builder(
builder: (context) {
Strings.init(context); // Initialize here
return const HomePage();
},
),
);
}
}
Usage
// Anywhere in your app
showSnackBar(Strings.errorNetwork);
// With parameters
showDialog(
title: Strings.greeting('John'),
);
Solution 4: Extension on BuildContext
Create an extension that makes accessing translations easier with context.
// lib/extensions/context_extensions.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension LocalizationExtension on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
// Usage in widgets
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(context.l10n.welcomeMessage); // Clean!
}
}
Solution 5: Riverpod Provider
For Riverpod users, create a localization provider.
// lib/providers/l10n_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
final navigatorKeyProvider = Provider((ref) => GlobalKey<NavigatorState>());
final l10nProvider = Provider<AppLocalizations>((ref) {
final navigatorKey = ref.watch(navigatorKeyProvider);
final context = navigatorKey.currentContext;
if (context == null) {
throw Exception('Context not available');
}
return AppLocalizations.of(context)!;
});
Usage
class MyService {
final Ref ref;
MyService(this.ref);
String getWelcome() {
return ref.read(l10nProvider).welcomeMessage;
}
}
Solution 6: Pass Strings as Parameters
The simplest and most testable approach: pass translated strings as parameters.
// Instead of this
class NotificationService {
void sendReminder() {
final message = L10n.current.reminderMessage; // Dependency on L10n
// send notification
}
}
// Do this
class NotificationService {
void sendReminder(String message) {
// send notification
}
}
// Call from widget
notificationService.sendReminder(l10n.reminderMessage);
Benefits:
- No global state
- Easy to test
- Explicit dependencies
- Works with any localization approach
Best Practices
1. Initialize Early, Check Always
class L10n {
static AppLocalizations? _instance;
static void init(BuildContext context) {
_instance = AppLocalizations.of(context);
}
static AppLocalizations get current {
if (_instance == null) {
throw StateError(
'L10n not initialized. Call L10n.init(context) in your app root.',
);
}
return _instance!;
}
}
2. Handle Locale Changes
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Re-initialize when locale changes
L10n.init(context);
}
}
3. Provide Fallbacks
static String get welcomeMessage {
try {
return current.welcomeMessage;
} catch (e) {
return 'Welcome'; // Fallback for edge cases
}
}
4. Type-Safe Error Messages
enum AppError {
network,
auth,
validation,
unknown,
}
extension AppErrorL10n on AppError {
String get message {
switch (this) {
case AppError.network:
return L10n.current.errorNetwork;
case AppError.auth:
return L10n.current.errorAuth;
case AppError.validation:
return L10n.current.errorValidation;
case AppError.unknown:
return L10n.current.errorUnknown;
}
}
}
// Usage
throw AppException(AppError.network.message);
Comparison Table
| Approach | Pros | Cons |
|---|---|---|
| Navigator Key | Simple, widely used | Global state, testing harder |
| GetIt | Integrates with DI | Requires re-registration on locale change |
| Static Lookup | Very simple | Must reinit on locale change |
| Pass as Parameter | Most testable, explicit | More verbose |
| Riverpod | Reactive, type-safe | Requires Riverpod |
Testing Without Context
// Mock the localization helper
class MockL10n implements AppLocalizations {
@override
String get welcomeMessage => 'Test Welcome';
@override
String greeting(String name) => 'Hello, $name';
// ... implement other methods
}
// In tests
void main() {
setUp(() {
L10n.setInstance(MockL10n());
});
test('service returns correct message', () {
final service = MyService();
expect(service.getMessage(), 'Test Welcome');
});
}
FlutterLocalisation Approach
Managing localization across services, models, and utilities is complex. FlutterLocalisation simplifies this by:
- Generating type-safe localization classes
- Providing helper utilities out of the box
- Supporting context-free access patterns
- Handling locale changes automatically
Conclusion
While Flutter's localization requires BuildContext by default, you have several options to access translations anywhere:
- Global Navigator Key - Simple and effective for most apps
- GetIt/Service Locator - Great for apps already using DI
- Pass as Parameters - Most testable and explicit
- Static Lookup - Simplest for small apps
Choose based on your app's architecture and testing requirements. For production apps, consider using FlutterLocalisation to automate these patterns.
Struggling with Flutter localization complexity? Try FlutterLocalisation free and get built-in helpers for context-free translation access, plus AI-powered translations and Git integration.