Flutter String Extraction Best Practices: From Hardcoded to Localized
Starting a new localization project? The first step is extracting hardcoded strings from your codebase. Do it wrong, and you'll spend weeks fixing issues. This guide shows you the professional approach to string extraction that scales.
Why String Extraction Matters
Poor string extraction leads to:
- Missing translations: Hardcoded strings slip through
- Broken UI: Extracted strings lose context
- Translation errors: Keys that don't convey meaning
- Maintenance nightmares: Inconsistent naming conventions
Before You Start: Preparation Checklist
1. Set Up Your Localization Infrastructure
# pubspec.yaml
dependencies:
flutter_localizations:
sdk: flutter
intl: ^0.18.0
flutter:
generate: true
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
2. Create Your Base ARB File
{
"@@locale": "en",
"@@last_modified": "2025-12-14T10:00:00.000Z"
}
3. Establish Naming Conventions
Before extracting a single string, define your key naming convention:
[screen]_[element]_[descriptor]
Examples:
- home_title_welcome
- login_button_submit
- cart_label_total
- error_message_network
- dialog_title_confirm
The Extraction Process
Step 1: Find All Hardcoded Strings
Use regex to find strings in your codebase:
# Find Text widgets with hardcoded strings
grep -rn "Text(\s*['\"]" lib/
# Find strings in common patterns
grep -rn "title:\s*['\"]" lib/
grep -rn "label:\s*['\"]" lib/
grep -rn "hintText:\s*['\"]" lib/
Or use VS Code's search with regex:
Text\(\s*['"][^'"]+['"]
Step 2: Categorize Strings
Group strings by type for better organization:
UI Labels (buttons, titles, labels):
// Before
ElevatedButton(child: Text('Submit'))
AppBar(title: Text('Settings'))
// After - use descriptive keys
ElevatedButton(child: Text(l10n.button_submit))
AppBar(title: Text(l10n.settings_title))
Messages (success, error, info):
// Before
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Saved successfully!')),
);
// After
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.message_saved_success)),
);
Dynamic Content (with placeholders):
// Before
Text('Hello, $userName!')
Text('You have $count items')
// After
Text(l10n.greeting_user(userName))
Text(l10n.cart_item_count(count))
Step 3: Extract Systematically by Screen
Work through your app screen by screen:
// lib/screens/home/home_screen.dart
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Get localization instance once at the top
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.home_title), // 'Home'
),
body: Column(
children: [
Text(l10n.home_welcome_message), // 'Welcome back!'
Text(l10n.home_subtitle_recent), // 'Recent Activity'
ElevatedButton(
onPressed: () {},
child: Text(l10n.home_button_view_all), // 'View All'
),
],
),
);
}
}
Creating Quality ARB Entries
Include Descriptions for Translators
{
"home_title": "Home",
"@home_title": {
"description": "Title shown in the app bar on the home screen"
},
"home_welcome_message": "Welcome back!",
"@home_welcome_message": {
"description": "Greeting message shown to returning users on the home screen"
},
"cart_item_count": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@cart_item_count": {
"description": "Shows the number of items in the shopping cart",
"placeholders": {
"count": {
"type": "int",
"example": "3"
}
}
}
}
Handle Pluralization Correctly
{
"notification_count": "{count, plural, =0{No notifications} =1{1 notification} other{{count} notifications}}",
"@notification_count": {
"description": "Badge showing number of unread notifications",
"placeholders": {
"count": {
"type": "int",
"format": "compact"
}
}
}
}
Handle Gender-Specific Text
{
"profile_greeting": "{gender, select, male{Welcome back, Mr. {name}} female{Welcome back, Ms. {name}} other{Welcome back, {name}}}",
"@profile_greeting": {
"description": "Personalized greeting on profile page",
"placeholders": {
"gender": {"type": "String"},
"name": {"type": "String"}
}
}
}
Common Extraction Mistakes to Avoid
Mistake 1: Extracting Technical Strings
// DON'T extract these:
const apiUrl = 'https://api.example.com'; // Config, not UI
final logMessage = 'User clicked button'; // Debug only
const routeName = '/home'; // Navigation constant
// DO extract these:
Text('Welcome!') // User-visible
SnackBar(content: Text('Error occurred')) // User message
Mistake 2: Concatenating Strings
// BAD - Can't translate properly
Text('Hello ' + userName + '!')
Text('You have ' + count.toString() + ' items')
// GOOD - Use placeholders
Text(l10n.greeting(userName)) // "Hello {name}!"
Text(l10n.itemCount(count)) // "You have {count} items"
Mistake 3: Splitting Sentences
// BAD - Translators can't reorder
Row(children: [
Text('Click '),
TextButton(child: Text('here')),
Text(' to continue'),
])
// GOOD - Keep sentence together
Text.rich(TextSpan(
children: [
TextSpan(text: l10n.click_to_continue_prefix), // "Click "
TextSpan(
text: l10n.click_to_continue_link, // "here"
recognizer: TapGestureRecognizer()..onTap = () {},
),
TextSpan(text: l10n.click_to_continue_suffix), // " to continue"
],
))
// BETTER - Use rich text with placeholder
// ARB: "click_to_continue": "Click {link} to continue"
Mistake 4: Inconsistent Key Names
// BAD - Inconsistent naming
{
"homeTitle": "Home",
"home-subtitle": "Welcome",
"HOME_BUTTON": "Click",
"home button label": "Submit"
}
// GOOD - Consistent snake_case with hierarchy
{
"home_title": "Home",
"home_subtitle": "Welcome",
"home_button_primary": "Click",
"home_button_submit": "Submit"
}
Mistake 5: Missing Context
// BAD - Ambiguous
{
"save": "Save"
}
// GOOD - Clear context
{
"settings_button_save": "Save",
"@settings_button_save": {
"description": "Button to save settings changes"
},
"document_button_save": "Save",
"@document_button_save": {
"description": "Button to save the current document"
}
}
Automation Tools
VS Code Extension Setup
Install helpful extensions:
- Flutter Intl - Generates localization code
- ARB Editor - Visual ARB file editing
- i18n Ally - Inline translation preview
Custom Extraction Script
Create a script to help identify hardcoded strings:
// scripts/find_hardcoded_strings.dart
import 'dart:io';
void main() async {
final libDir = Directory('lib');
final patterns = [
RegExp(r"Text\(\s*'[^']+'\s*\)"),
RegExp(r'Text\(\s*"[^"]+"\s*\)'),
RegExp(r"title:\s*'[^']+'"),
RegExp(r'title:\s*"[^"]+"'),
RegExp(r"hintText:\s*'[^']+'"),
RegExp(r'hintText:\s*"[^"]+"'),
RegExp(r"label:\s*'[^']+'"),
RegExp(r'label:\s*"[^"]+"'),
];
await for (final file in libDir.list(recursive: true)) {
if (file is File && file.path.endsWith('.dart')) {
final content = await file.readAsString();
final lines = content.split('\n');
for (var i = 0; i < lines.length; i++) {
for (final pattern in patterns) {
if (pattern.hasMatch(lines[i])) {
// Skip if already using l10n
if (!lines[i].contains('l10n.') &&
!lines[i].contains('AppLocalizations')) {
print('${file.path}:${i + 1}: ${lines[i].trim()}');
}
}
}
}
}
}
}
Run with:
dart run scripts/find_hardcoded_strings.dart
Extraction Workflow with FlutterLocalisation
Step 1: Connect Your Repository
Link your Git repo to FlutterLocalisation for automatic sync.
Step 2: Import Existing Translations
If you have existing ARB files, import them:
flutter_localisation import --source lib/l10n/
Step 3: Add New Keys via Web Interface
Use the visual editor to:
- Add keys with descriptions
- Set placeholder types
- Preview in context
Step 4: Sync Back to Code
flutter_localisation pull
flutter pub run intl_utils:generate
Quality Checklist
Before considering extraction complete, verify:
- All user-visible strings extracted
- Consistent key naming convention
- Descriptions for all keys
- Placeholders typed correctly
- Pluralization handled properly
- No concatenated strings
- No split sentences
- Technical strings excluded
- Build succeeds with
flutter gen-l10n - App works in all supported locales
Conclusion
String extraction is foundational work that affects your entire localization workflow. Take time to:
- Establish conventions before starting
- Work systematically by screen
- Include context for translators
- Use automation tools
- Review thoroughly before translating
With clean extraction, your translators will thank you, and adding new languages becomes effortless.
Ready to extract and manage your Flutter strings? Try FlutterLocalisation free with visual ARB editing.