← Back to Blog

Flutter String Extraction Best Practices: From Hardcoded to Localized

flutterlocalizationstring-extractionbest-practicesarbi18n

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:

  1. Flutter Intl - Generates localization code
  2. ARB Editor - Visual ARB file editing
  3. 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:

  1. Establish conventions before starting
  2. Work systematically by screen
  3. Include context for translators
  4. Use automation tools
  5. 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.