Localization
Frankly uses Flutter's ARB-based localization workflow. The English template is
client/lib/l10n/app_en.arb; the translated files are:
app_es.arb(Spanish)app_zh.arb(Simplified Chinese)app_zh_Hant_TW.arb(Traditional Chinese, Taiwan)
Flutter generates AppLocalizations as a synthetic package. Application code
normally accesses it through the context.l10n extension from
client/lib/core/localization/localization_helper.dart.
Adding a string
- Add a lower-camel-case key and its English value to
app_en.arb. - Add placeholder metadata when the string contains placeholders. Keep the placeholder names and types identical in every locale.
- Add the same key to
app_es.arb,app_zh.arb, andapp_zh_Hant_TW.arb. - Replace the hardcoded UI value with
context.l10n.keyName(orcontext.l10n.keyName(argument)for a placeholder). Add the localization helper import if the file does not already have it. - Generate and inspect the localization output:
cd client
fvm flutter gen-l10n
cat missing.json
missing.json is generated by gen-l10n, is intentionally gitignored, and
must contain {}. It is a completeness report, not a source file to commit.
Do not edit generated localization output by hand.
CI gates
.github/workflows/l10n_check.yml runs for pull requests that change
client/** (the path filter skips the job for unrelated changes). It uses the
pinned Flutter 3.22.2 toolchain and runs two blocking gates:
- Translation completeness:
flutter gen-l10nmust createmissing.json. The inline checker fails loudly if the file is absent or malformed. For a non-empty object it prints one readable block per locale followed by each missing key. Add the key to every ARB file and regenerate;{}passes. - New hardcoded strings:
dart run tool/check_hardcoded_strings.dartruns in ratchet mode. It only fails for findings that are not in the checked-in baseline. A diagnostic has the formrelative/file.dart:LINE:COLand the normalized string. Use the source location to replace the value with a localization key; do not add a baseline entry merely to silence a real user-facing string.
The repository currently has pre-existing analyzer diagnostics, so a blocking
repo-wide flutter analyze --fatal-warnings is deliberately not part of this
workflow. Run analyzer locally when changing application code.
Hardcoded-string scanner coverage
The scanner uses the analyzer's unresolved AST API. It does not need
build_runner or a resolved element model. Its current coverage includes:
- first positional strings in
Text,SelectableText, and the projectHeightConstrainedTextwidget (including implicit constructorMethodInvocationnodes); RichTextandTextSpan(text:)strings;Tooltip(message:);- contextual custom-widget parameters:
ActionButton(text:),CommunityPageFloatingActionButton(text:),ThickOutlineButton(text:),_IconButton(text:),AppBar(title:), andConfirmDialog'stitle,mainText,subText,cancelText, andconfirmText; - user-facing named parameters used by
CustomTextFieldand dialog APIs, includinglabelText,hintText,helperText,errorText,counterText,prefixText,suffixText,message,semanticLabel,tooltip,positiveButtonText, andnegativeButtonText.
Text nested inside a SnackBar, AppBar, ElevatedButton, showDialog, or
another container is covered because the visitor walks descendants. Adjacent
strings and interpolations are supported. Conditional (a ? 'X' : 'Y') and
null-coalescing (value ?? 'X') arguments are unwrapped, including expressions
inside ${...}. For example,
Text('$count ${count == 1 ? 'comment' : 'comments'}') reports both comment
and comments. That unwrapping deliberately stops at call boundaries, including
inside interpolations, so arguments to context.l10n.someKey('argument') are
not findings.
Escape sequences are decoded before letter detection, which uses the Unicode
letter property: CJK, kana, and Hangul labels such as Text('设置') are
reported, while spacing, punctuation, separator, and emoji-only strings are
ignored.
The scanner intentionally does not infer arbitrary custom widget types from
unresolved syntax. It does not recursively attribute literals returned inside
callbacks such as validator closures, does not inspect arbitrary named
parameters on every custom dialog, and does not treat semantics IDs, URLs,
asset names, or other non-user text as localization keys. Generated files
(*.g.dart, *.freezed.dart, *.mocks.dart, generated localization output,
and everything under lib/l10n) are skipped. These are known limits; add a
specific contextual widget/parameter rule when the project adopts another
widely used user-facing entry point.
Ratchet baseline
client/tool/hardcoded_strings_baseline.txt contains the accepted existing
findings. The current baseline has 193 counted entries whose occurrence
counts sum to 199 findings. Each line has four tab-separated columns:
- the relative path;
- the first 12 hexadecimal characters of the SHA-256 digest of the normalized string (the comparison key);
- the number of occurrences for that path/digest pair; and
- the normalized string, for reviewer readability. Strings longer than 120
characters are truncated with
…in this display-only column.
The scanner compares only the path, digest, and occurrence count; the display
column is ignored. It deliberately does not use line numbers: moving code or
adding a line above a finding must not rewrite the baseline. If a current
count exceeds the baseline count, only the excess occurrences fail with their
source locations. If a count decreases or a key disappears, the stale
baseline entry is reported as a non-failing reminder to run
--update-baseline. Because duplicate occurrences are counted, adding another
copy of an existing hardcoded string in the same file is detected and fails
the gate.
Default mode compares the current counts with the baseline. New findings fail;
stale entries are reported as a non-failing reminder. Use --all (or
--no-baseline) to print every finding while doing burn-down work:
cd client
fvm dart run tool/check_hardcoded_strings.dart --all
Run --update-baseline only after reviewing every current finding and deciding
that it is an intentional, accepted baseline item. It rewrites the file in
stable path/digest order:
fvm dart run tool/check_hardcoded_strings.dart --update-baseline
Review the baseline diff as part of that change. A new product string should normally be localized instead of added to the baseline.
The explicit escape hatch is // ignore: hardcoded_string on the offending
line or the line above, optionally followed by a reason. It is reserved for
true development-only values (for example a deliberately displayed build
version or a feature gated by ENABLE_FAKE_PARTICIPANTS). Every use must state
why it is safe; it is not a way to suppress ordinary product copy.
Optional IDE automation
The VS Code extension
generate-l10n can automate parts
of ARB generation. It is optional and is not required by CI. Issue #544 notes a
known bug in the extension, so verify its output and run the repository's
gen-l10n and CI checks before committing.