Flutter Localization Code Review Checklist: What to Check in Every PR
Code reviews are your last line of defense against localization bugs reaching production. A hardcoded string, a missing translation, or an incorrect plural form can frustrate users and damage your app's reputation in international markets. This checklist helps reviewers catch i18n issues before they ship.
Why Localization Code Reviews Matter
Common bugs that slip through:
- Hardcoded strings - "Loading..." instead of
l10n.loading - Missing ARB entries - New UI text without translations
- Broken placeholders -
{username}typo as{userName} - Incorrect plurals - Using simple conditionals instead of ICU format
- Concatenated strings - Building sentences that break in other languages
- Layout issues - Text overflow not considered for German/Finnish
- RTL bugs - Icons and layouts not flipped for Arabic/Hebrew
The Complete Checklist
1. No Hardcoded User-Facing Strings
// BAD - Hardcoded string
Text('Welcome back!')
// GOOD - Localized
Text(AppLocalizations.of(context)!.welcomeBack)
Check for:
- All
Text()widgets use localized strings - All button labels are localized
- All error messages are localized
- All placeholder/hint texts are localized
- All dialog titles and content are localized
- All snackbar messages are localized
- All tooltip texts are localized
Quick grep to find issues:
# Find potential hardcoded strings
grep -rn "Text('[A-Z]" lib/
grep -rn 'Text("[A-Z]' lib/
grep -rn "SnackBar.*content.*Text\(" lib/
2. ARB Files Updated
For every new user-facing string, verify:
- Entry added to
app_en.arb(or template ARB) - Proper
@keymetadata with description - Placeholders documented with type and example
- Key follows naming convention (camelCase, descriptive)
// GOOD - Complete ARB entry
{
"welcomeBackUser": "Welcome back, {userName}!",
"@welcomeBackUser": {
"description": "Greeting shown on home screen after login",
"placeholders": {
"userName": {
"type": "String",
"example": "John"
}
}
}
}
// BAD - Missing metadata
{
"welcomeBackUser": "Welcome back, {userName}!"
}
3. Placeholders Match
Verify placeholder names are consistent:
// ARB file
"orderConfirmation": "Order #{orderId} confirmed for {customerName}"
// Dart code - Check these match!
l10n.orderConfirmation(orderId, customerName) // Correct order?
Common mistakes:
- Placeholder names match exactly (case-sensitive)
- Placeholder order matches method signature
- All placeholders are actually used
- No extra/missing placeholders
4. Proper Plural Handling
// BAD - Simple conditional (breaks for Russian, Arabic, etc.)
Text(count == 1 ? '1 item' : '$count items')
// GOOD - ICU plural format in ARB
// "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}"
Text(l10n.itemCount(count))
Check for:
- No
count == 1 ? ... : ...patterns for plurals - ARB uses ICU plural syntax
- Languages with complex plural rules covered (Russian, Arabic, Polish)
5. No String Concatenation
// BAD - Breaks word order in many languages
Text(l10n.hello + ', ' + userName + '!')
// BAD - String interpolation without localization
Text('Hello, $userName!')
// GOOD - Single localized string with placeholder
Text(l10n.helloUser(userName))
Why it matters:
- Japanese: "田中さん、こんにちは!" (name comes first)
- English: "Hello, Tanaka!"
- Word order varies by language
6. Layout Handles Text Expansion
German and Finnish can be 30-40% longer than English:
// BAD - Fixed width that will overflow
SizedBox(
width: 100,
child: Text(l10n.submitButton),
)
// GOOD - Flexible width
Flexible(
child: Text(
l10n.submitButton,
overflow: TextOverflow.ellipsis,
),
)
// BETTER - Constrained with min/max
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 80,
maxWidth: 200,
),
child: Text(l10n.submitButton),
)
Check for:
- No fixed-width containers for text
-
TextOverflowhandling specified -
FittedBoxorFlexibleused where appropriate - Row/Column doesn't overflow with longer text
7. RTL Layout Support
// BAD - Hardcoded directional values
Padding(padding: EdgeInsets.only(left: 16))
Row(children: [icon, Spacer(), text])
// GOOD - Directional-aware
Padding(padding: EdgeInsetsDirectional.only(start: 16))
Row(children: [icon, Spacer(), text]) // Automatically flips in RTL
Check for:
-
EdgeInsetsDirectionalinstead ofEdgeInsetswith left/right -
AlignmentDirectionalinstead ofAlignmentwith left/right -
TextDirectionconsidered for manual layouts - Icons with directional meaning flip correctly
- Progress indicators and sliders work in RTL
8. Date, Time, and Number Formatting
// BAD - Locale-unaware formatting
Text('${DateTime.now().month}/${DateTime.now().day}')
Text('\$${price.toStringAsFixed(2)}')
// GOOD - Locale-aware formatting
Text(DateFormat.yMd(localeName).format(DateTime.now()))
Text(NumberFormat.currency(locale: localeName, symbol: '\$').format(price))
Check for:
- Dates use
DateFormatwith locale - Numbers use
NumberFormatwith locale - Currency uses
NumberFormat.currencywith locale - No hardcoded date/number separators
9. Accessibility Labels Localized
// BAD - Missing or hardcoded semantics
IconButton(
icon: Icon(Icons.search),
onPressed: _search,
)
// GOOD - Localized semantics
IconButton(
icon: Icon(Icons.search),
onPressed: _search,
tooltip: l10n.searchTooltip,
)
// Or with Semantics wrapper
Semantics(
label: l10n.searchButtonLabel,
button: true,
child: IconButton(
icon: Icon(Icons.search),
onPressed: _search,
),
)
Check for:
- All
IconButtonwidgets havetooltip - Images have localized
semanticLabel - Custom widgets have
Semanticswrapper -
excludeFromSemanticsused appropriately
10. Error Messages Provide Context
// BAD - Generic error
catch (e) {
showSnackBar(l10n.errorOccurred);
}
// GOOD - Contextual error with action
catch (e) {
showSnackBar(l10n.errorSavingProfile);
// Or with parameter
showSnackBar(l10n.errorWithDetails(e.message));
}
Check for:
- Error messages are specific to the operation
- User knows what failed and what to do
- Technical details hidden but available
Quick Reference Card
## PR Localization Checklist
### Strings
- [ ] No hardcoded user-facing text
- [ ] ARB entries added with descriptions
- [ ] Placeholders match code and ARB
### Plurals & Formatting
- [ ] ICU plural format used
- [ ] Dates/numbers use intl formatters
- [ ] No string concatenation
### Layout
- [ ] Flexible widths for text
- [ ] EdgeInsetsDirectional used
- [ ] RTL layout tested
### Accessibility
- [ ] Semantic labels localized
- [ ] Tooltips on icon buttons
### Testing
- [ ] Tested in at least 2 locales
- [ ] Tested with long strings (German)
- [ ] Tested RTL layout (Arabic)
Automated Checks
Add these to your CI pipeline:
Lint Rules
# analysis_options.yaml
linter:
rules:
# Catch some hardcoded strings
avoid_print: true
analyzer:
plugins:
- custom_lint # For custom localization rules
Custom Lint Rule Example
// lib/lints/no_hardcoded_strings.dart
import 'package:custom_lint_builder/custom_lint_builder.dart';
class NoHardcodedStrings extends DartLintRule {
@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addStringLiteral((node) {
final parent = node.parent;
// Check if inside Text widget
if (parent is ArgumentList) {
final invocation = parent.parent;
if (invocation is InstanceCreationExpression) {
final name = invocation.constructorName.type.name.name;
if (name == 'Text' && _containsUserFacingText(node.stringValue)) {
reporter.reportErrorForNode(
LintCode(
name: 'no_hardcoded_strings',
problemMessage: 'Use localized string instead of hardcoded text',
),
node,
);
}
}
}
});
}
bool _containsUserFacingText(String? value) {
if (value == null) return false;
// Simple heuristic - starts with capital letter
return RegExp(r'^[A-Z][a-z]').hasMatch(value);
}
}
Pre-commit Hook
#!/bin/bash
# .git/hooks/pre-commit
# Check for potential hardcoded strings
HARDCODED=$(grep -rn "Text('[A-Z]" lib/ --include="*.dart" | grep -v "// i18n-ok")
if [ -n "$HARDCODED" ]; then
echo "WARNING: Potential hardcoded strings found:"
echo "$HARDCODED"
echo ""
echo "If these are intentional, add '// i18n-ok' comment"
exit 1
fi
# Verify ARB files are valid JSON
for arb in lib/l10n/*.arb; do
if ! python3 -m json.tool "$arb" > /dev/null 2>&1; then
echo "ERROR: Invalid JSON in $arb"
exit 1
fi
done
echo "Localization checks passed"
GitHub Actions Workflow
# .github/workflows/i18n-check.yml
name: Localization Check
on: [pull_request]
jobs:
i18n-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for hardcoded strings
run: |
ISSUES=$(grep -rn "Text('[A-Z]" lib/ --include="*.dart" | grep -v "// i18n-ok" || true)
if [ -n "$ISSUES" ]; then
echo "::warning::Potential hardcoded strings found"
echo "$ISSUES"
fi
- name: Validate ARB files
run: |
for arb in lib/l10n/*.arb; do
python3 -m json.tool "$arb" > /dev/null
done
- name: Check placeholder consistency
run: |
# Extract placeholders from ARB and compare with Dart usage
node scripts/check-placeholders.js
- name: Run localization tests
run: |
flutter test test/l10n/
Review Comments Templates
Save time with template comments:
Hardcoded String
This string should be localized. Please:
1. Add entry to `lib/l10n/app_en.arb`
2. Include `@key` description
3. Use `AppLocalizations.of(context)!.keyName`
Missing Plural
This count needs proper plural handling. Consider using ICU format:
```json
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}"
String Concatenation
Concatenating strings breaks word order in other languages. Use a single localized string with placeholders instead:
```dart
l10n.greeting(userName) // "Hello, {userName}!"
Layout Issue
This fixed width may cause text overflow in languages like German (30% longer). Consider using `Flexible` or `ConstrainedBox` instead.
Summary
A thorough localization code review checks:
- No hardcoded strings - All text uses l10n
- ARB entries complete - Descriptions, placeholders documented
- Placeholders match - Names and order consistent
- Proper plurals - ICU format, not conditionals
- No concatenation - Single strings with placeholders
- Flexible layouts - Handles text expansion
- RTL support - Directional APIs used
- Formatted values - intl package for dates/numbers
- Accessible - Labels localized
- Contextual errors - Helpful error messages
Use this checklist in every PR to catch i18n issues before they reach users.