string_literal_finder 2.0.0-dev.2 copy "string_literal_finder: ^2.0.0-dev.2" to clipboard
string_literal_finder: ^2.0.0-dev.2 copied to clipboard

Find non translated strings: Searches all dart files for string literals, excluding non translatable strings. Useful for i18n/l10n.

Pub Version Dart SDK Version Pub popularity codecov

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 #

IDE Warnings

  1. Add the plugin to analysis_options.yaml. Note this is a top-level plugins: key — the older analyzer: plugins: form is the legacy plugin mechanism and will not load this plugin.

    plugins:
      string_literal_finder: ^2.0.0
    

    A local path works too:

    plugins:
      string_literal_finder:
        path: ../string_literal_finder
    
  2. Restart your analyzer.

    Restart analyzer

Requires Dart 3.11 or newer. dart analyze runs analyzer plugins; flutter analyze does not.

Ignoring literals #

  • Any argument annotated with @NonNls or @NonNlsArg()
  • Anything passed to the nonNls() function
  • Anything in a function, method or class annotated with @NonNls
  • Anything passed to the logging package's Logger
  • Arguments to annotations, and import / part / part of URIs
  • Constructor arguments of Uri, RegExp, Exception, Error, AssetImage, RouteSettings, ValueKey and MethodChannel
  • Any line with a trailing // NON-NLS comment
  • 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.

13
likes
160
points
58
downloads

Documentation

API reference

Publisher

verified publishercodeux.design

Weekly Downloads

Find non translated strings: Searches all dart files for string literals, excluding non translatable strings. Useful for i18n/l10n.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

analysis_server_plugin, analyzer, args, glob, logging, logging_appenders, path, source_gen, string_literal_finder_annotations, yaml

More

Packages that depend on string_literal_finder