string_literal_finder
Finds string literals in Dart code which should have been externalized for translation. Useful when localizing an app, and as a CI check that keeps new hardcoded strings from creeping back in.
It ships two front ends over the same analysis:
- a command line tool, for CI, and
- an analyzer plugin, for warnings directly in your IDE.
Both apply the same rules and read the same configuration.
Command line
dart pub global activate string_literal_finder
dart pub global run string_literal_finder --path=lib
lib/example.dart:17:30 'not translated'
Found 1 literal in 1 file.
{
"stringLiterals": 1,
"stringLiteralsFiles": 1,
"filesAnalyzed": 1,
"filesSkipped": 0,
"filesWithoutLiterals": 0
}
Exit codes:
| code | meaning |
|---|---|
| 0 | nothing to report |
| 1 | literals were found which the configured gate does not allow |
| 2 | the command line could not be understood |
| 70 | something went wrong during analysis |
Adopting this on a project that already has literals
A real code base has thousands of string literals, and a check that fails on day one gets switched off. Record the current state once, commit it, and gate on additions:
# once, and commit the result
dart run string_literal_finder --path=lib \
--baseline=string_literals_baseline.json --write-baseline
# in CI
dart run string_literal_finder --path=lib \
--baseline=string_literals_baseline.json
The second command exits 0 while nothing new appears, and exits 1 listing only
the literals that are not in the baseline. As strings get externalized, re-run
--write-baseline to shrink it; the run tells you how many recorded entries
have gone.
Baseline entries are keyed by file and by the literal as written, not by line number, so reformatting, moving code within a file, or editing an unrelated line will not make the check fail. Adding a second copy of a literal to a file that already had one is still reported.
--max-literals=<n> is the cruder alternative if you would rather track a
single number than a file.
Filters are applied before the baseline is recorded, so use the same filter flags when recording and when checking. Adding a filter later is harmless; removing one makes previously filtered literals look new.
Machine-readable output
--format=json writes every finding to stdout, for CI annotations:
{
"ok": false,
"metrics": { "stringLiterals": 1, "...": "..." },
"literals": [
{
"path": "example.dart",
"line": 17, "column": 30, "endLine": 17, "endColumn": 46,
"literal": "'not translated'",
"value": "not translated"
}
]
}
Diagnostics go to stderr, so stdout stays parseable.
Speed
Resolving Dart source dominates a run — the analysis itself is a rounding error next to it. Two things help.
--cache-dir keeps the analyzer's linked summaries between runs, which
roughly halves the time. Compiling to a binary removes JIT warm-up, which
dominates once the analysis is cached, and is worth about as much again:
dart compile exe bin/string_literal_finder.dart -o slf
./slf --path=lib --cache-dir=.dart_tool/string_literal_finder
On a mid-sized Flutter app that took a run from roughly fifteen seconds to under two. Your mileage will differ; measure your own.
A compiled binary cannot work out where the SDK is — the analyzer derives that
from the running executable — so it looks for DART_SDK and then a dart on
PATH. Pass --dart-sdk=<path> if neither applies.
The cache runs to a hundred megabytes or so for a Flutter app, and is safe to cache in CI:
- uses: actions/cache@v4
with:
path: .dart_tool/string_literal_finder
key: slf-${{ hashFiles('pubspec.lock') }}
- run: dart run string_literal_finder --path=lib
--cache-dir=.dart_tool/string_literal_finder
--baseline=string_literals_baseline.json
Reducing noise
Out of the box the tool reports every literal it cannot prove is
non-translatable. Expect most findings not to be user-visible copy — map keys,
asset extensions, switch cases and punctuation usually outnumber it several
times over.
The best fix is to mark them at the source, with the suppressions below — that is precise, and it documents intent. Where that is too much work up front, these filters trade recall for signal. They are all off by default, because a tool that silently hides a real untranslated string is worse than a noisy one.
| flag | cuts | risk |
|---|---|---|
--ignore-symbols |
a modest slice | low: drops literals with no word in them ('', '/', '0', '2026-08-12') and pure substitutions ('$error'). Keeps ' $unit' and '$a – $b' — see below |
--min-length=<n> |
about the same at n=2 |
blunter version of the same idea; also drops 'OK', 'No', 'de' |
--ignore-pattern=<regex> |
depends | yours to choose; repeatable |
--prose-only |
most of them | high — also drops 'Cancel', 'Back', 'Hidden'. Use it to triage the biggest wins, not as a gate |
Start with --ignore-symbols; it is the only one that has never been observed
to discard a translatable string. Measure the rest against your own code with
--format=json before trusting them — the proportions vary a lot between code
bases, so a number from someone else's project would not tell you much.
One rule protects all of them
No filter — including a --ignore-pattern you write yourself — can discard an
interpolated literal that has text between the holes.
That rule is not cosmetic. Filters match against a literal's visible text,
and for an interpolation that is only the fragments: ' $unit' reduces to
' ', and '$a – $b' to ' – '. So the most obvious, most conservative
pattern anyone would reach for —
--ignore-pattern='^\s*$' # "just empty or whitespace, surely that's safe"
— would otherwise throw away ' $unit', '$monthDay, ${format.year(local)}'
and '$count $noun${count == 1 ? '' : 's'}': a unit suffix, a date
composition, and English pluralisation compiled into the source. Those are
among the most valuable things this tool finds, so they are never filtered.
A literal made of nothing but holes ('$error') is dropped — there is no
text between them to translate.
One consequence worth knowing: --ignore-symbols is exactly equivalent to
--ignore-pattern='^\P{L}*$', and exists only so the common case does not
have to be spelled as a regular expression.
Integration with the IDE analyzer

-
Add the plugin to
analysis_options.yaml. Note this is a top-levelplugins:key — the olderanalyzer: plugins:form is the legacy plugin mechanism and will not load this plugin.plugins: string_literal_finder: ^2.0.0A local path works too:
plugins: string_literal_finder: path: ../string_literal_finder -
Restart your analyzer.

Requires Dart 3.11 or newer. dart analyze runs analyzer plugins;
flutter analyze does not.
Ignoring literals
- Any argument annotated with
@NonNlsor@NonNlsArg() - Anything passed to the
nonNls()function - Anything in a function, method or class annotated with
@NonNls - Anything passed to the
loggingpackage'sLogger - Arguments to annotations, and
import/part/part ofURIs - Constructor arguments of
Uri,RegExp,Exception,Error,AssetImage,RouteSettings,ValueKeyandMethodChannel - Any line with a trailing
// NON-NLScomment - Files matching
exclude_globs, and anything ending in.g.dart
The annotations live in a separate, dependency-free package:
dart pub add string_literal_finder_annotations
// NON-NLS applies to the line the literal ends on. For a call spread over
several lines, put the comment on the line of the literal itself, or annotate
the parameter instead.
exclude_globs
string_literal_finder:
exclude_globs:
- '_tools/**'
- '**/*.freezed.dart'
Globs are relative to the directory holding analysis_options.yaml. Both the
plugin and the command line read this; pass --no-analysis-options to the CLI
to ignore it.
Example
import 'package:string_literal_finder_annotations/string_literal_finder_annotations.dart';
import 'package:logging/logging.dart';
final _logger = Logger('example');
void exampleFunc(@NonNls String ignored, String warning) {}
void main() {
exampleFunc('Hello world', 'not translated');
_logger.finer('Lorem ipsum');
final testMap = nonNls({
'key': 'value',
});
}
@NonNls
String ignoreFunction() {
// all strings in this function will be ignored.
return 'foo';
}
Only 'not translated' is reported.
Libraries
- main
- string_literal_finder
- Classes used by the command line utility to find string literals in dart code.