Locale Sweep은 여러 언어, 기기 크기, 텍스트 스케일 및 RTL 모드에서 Flutter 앱의 로컬라이제이션 테스트를 자동화하는 CI 품질 게이트입니다. 앱 흐름을 실행하고 다양한 구성에서 UI 문제를 탐지하며, 고장 난 화면의 증거로 스크린샷을 생성합니다. 개발 파이프라인 초기에 로컬라이제이션 회귀를 조기에 포착하도록 설계되어 글로벌 시장에서 일관된 사용자 경험을 보장합니다. 이 도구는 CI/CD 워크플로우에 원활하게 통합되며 모든 주요 Flutter 플랫폼을 지원합니다.
사용 사례
CI 파이프라인에서의 자동화된 로컬라이제이션 테스트
다국어 간 레이아웃 문제 탐지
RTL 지원 검증
텍스트 확대 호환성 보장
다중 로케일 앱 동작 검증
배포 전 품질 검사
주요 기능
다양한 로케일 및 디바이스 구성 간에 앱 흐름을 실행합니다
손상된 화면에 대한 스크린샷 증거를 생성합니다
RTL 및 동적 텍스트 크기 조정을 지원
CI/CD 시스템과의 통합
크로스 플랫폼 테스트(안드로이드, iOS, 리눅스, 맥OS, 윈도우)
간단한 명령줄 설정
적합한 대상
Flutter 팀은 국제 사용자들과 협력합니다
CI/CD 중심 개발 워크플로우
강력한 RTL 및 로컬라이제이션 지원을 필요로 하는 앱
번역에서 UI 회귀를 방지하려는 팀
접근성과 글로벌 UX를 우선시하는 개발자
고려 사항
정확한 결과를 얻으려면 테스트 흐름 정의가 필요합니다
복잡한 탐색을 위해 구성이 필요할 수 있습니다
스크린샷은 CI 실행 시간을 늘릴 수 있습니다
안정적인 테스트 환경과 함께 사용하는 것이 가장 좋습니다
시각적 검증에 국한되며 기능 로직 테스트는 포함되지 않음
원본 README
연결된 소스 저장소의 README 원문 스냅샷입니다. 최신 내용은 저장소에서 확인하세요.
LocaleSweep
Localization QA for Flutter — automated.
One function call. Every locale, viewport, text scale, and brightness. Screenshots + reports.
English
German
Arabic RTL
2x Scale
Why LocaleSweep?
Your app looks perfect in English. Then a German user opens Settings and "Benachrichtigungseinstellungen" overflows the row. An Arabic user sees left-aligned text. A Japanese user hits an untranslated screen.
LocaleSweep catches these before your users do. It multiplies a single test across every combination of locale, text scale, viewport, and brightness — then captures a golden screenshot of each variant and fails only the ones that break.
sweepTest(
'settings',
builder: () => constMyApp(),
locales: ['en', 'de', 'ar', 'ja'],
textScales: [1.0, 2.0],
viewports: [ViewportPreset.phone, ViewportPreset.tablet],
darkMode: true,
arbDir: 'lib/l10n',
);
// 4 locales x 2 scales x 2 viewports x 2 brightness = 32 test cases from one call
Pseudo variants render your base locale, then rewrite every visible string
before the checks run:
Settings → [Šéţţîñĝš one two]
Save changes → [Šáṽé çĥáñĝéš one two three]
Longer text (+30% for long strings, up to +100% for short labels, the
way real translations grow) exposes overflows and truncations.
Accented letters reveal clipped ascenders and diacritics.
Brackets make cut-off strings obvious in screenshots: a missing ] means
the text was truncated.
It works with any localization library because it rewrites the rendered text,
not your translation files. Numbers, icons and text without letters are left
alone. Widgets that rebuild after the screen settles show their original text
again.
Hardcoded text detection
Find user-facing strings that never went through localization:
sweepTest(
'settings',
builder: () => constMyApp(),
arbDir: 'lib/l10n',
detectHardcodedText: true,
hardcodedTextAllowlist: [r'^TaskFlow$'], // brand names, user data
);
Every rendered string is matched against the ARB messages for the variant's
locale, its language (pt for pt_BR) and the base locale. Placeholders match
any value, and each plural / select branch is understood, so
{count, plural, one{# item} other{# items}} matches 3 items. Anything that
matches no message is reported as hardcoded text:
Hardcoded text: "Buy now" matches no ARB message for de
Messages without any literal text, such as "userName": "{name}", can't tell
user data from hardcoded copy, so they never count as a match. Add patterns for
names and other data to hardcodedTextAllowlist. Strings built by joining
translations in code ('${l10n.hello}, $name') are reported too. They usually need a single message with a placeholder instead.
Hardcoded text is advisory, like truncations (see --fail-on).
Also configurable via YAML (hardcoded_text: true).
Localizations integration
Test individual screens with AppLocalizations.of(context) — no MaterialApp wrapper needed:
sweepTest(
'settings',
builder: () => constSettingsScreen(), // just the screen, not the full app
localizationsDelegates: AppLocalizations.localizationsDelegates,
locales: ['en', 'de', 'ar', 'ja'],
arbDir: 'lib/l10n',
);
LocaleSweep wraps the widget in a Localizations ancestor with your delegates, sets the locale per variant, and includes Material/Widgets fallback delegates automatically. Your screen's AppLocalizations.of(context) calls work as if it were inside a MaterialApp.
Passing the variant locale directly
If your screens read the locale from MaterialApp (rather than an injected
Localizations ancestor), use builderWithVariant to hand the variant's locale
to your own app shell:
This avoids relying on platform locale resolution, which depends on the host
machine's locale list and can silently render the wrong language. Use
variant.renderLocale rather than variant.locale, so
pseudo-localized variants render the base locale.
Capture-only screenshots
Set compareScreenshots: false to write PNGs for the report without asserting
against golden baselines. This is useful when screenshots are for diagnosis only
(for example, under Flutter's Ahem test font, where pixel diffs are not
meaningful):
Option 2 skips the MaterialApp overhead and tests the screen directly. LocaleSweep injects the Localizations ancestor, so AppLocalizations.of(context)! works in both cases.
See the example app for a full gen-l10n setup with 8 locales and intentional bugs.
Using with slang
slang uses a different file format than ARB, so the static analysis checks (missing keys, placeholders, untranslated strings) don't work against slang files directly. The runtime checks (overflow, RTL, dark mode, text scale, goldens) work out of the box since they're format-agnostic.
To get static analysis, use slang's built-in ARB export as a bridge:
# Export slang translations to ARB format
dart run slang:export --format arb --output lib/l10n/arb
This gives you the full static analysis (missing keys, placeholder mismatches, untranslated strings) on top of the runtime sweep. Re-export after updating translations to keep the checks in sync.
Screenshot diffing
Set tolerance to allow minor pixel differences (anti-aliasing, CI rendering jitter) without masking real regressions:
sweepTest(
'settings',
builder: () => constSettingsPage(),
tolerance: 0.5, // allow up to 0.5% pixel difference
);
Also configurable via YAML: tolerance: 0.5
Every comparison produces diff data, including the default tolerance: 0 (where
any pixel difference is a failure). When pixels differ, a 3-panel diff image
(Golden | Actual | Diff) is saved to .locale_sweep/diffs/. Diff data flows into
all report formats — percentage column in Markdown, badge + link in HTML,
structured object in JSON.
Golden comparison never writes the image it captured, so a failing variant also
writes its actual PNG to .locale_sweep/actual/ and the reports link to it. That
is what gives a first run (no baselines yet) screenshot proof instead of
"No screenshot" everywhere. Baselines are never overwritten.
Variant callbacks
Run interactions after the widget is pumped. Use body for simple cases, variantBody when you need the current locale/brightness/direction:
locales: [en, de, ar, ja]
text_scales: [1.0, 2.0]
viewports:- { name:"375x667", width:375, height:667 }
- { name:"768x1024", width:768, height:1024 }
dark_mode:truetolerance:0.5arb_dir:lib/l10nbase_locale:en# Values that are intentionally identical across locales (app name, language# self-names): exempt from the "untranslated" check, still checked for missing# keys and placeholder mismatches.untranslated_allowlist: [appName]
# Report rendered text that matches no ARB message (needs arb_dir).hardcoded_text:truehardcoded_text_allowlist: ["^TaskFlow$"]
# ARB files may use locale subtags: app_zh_Hans.arb, app_es_419.arb, en.arb,# app_en-US.arb are all recognized (previously only app_en.arb / app_en_US.arb).screenshot_dir:.locale_sweep/screenshotsreport_dir:.locale_sweep/reports
Any parameter passed directly to sweepTest() overrides the YAML config for that flow. Config validation warns about typos and type mismatches on stderr.
Ignoring intentional truncations
Some truncations are by design — list-row cells clamp long data with
maxLines: 1/2 + ellipsis, or a known ID pattern (RCP-…) is meant to be cut
off. Use declarative YAML rules to exclude them from reports and --fail-on truncation:
truncation_ignore:# ignore any truncation whose maxLines is set and <= 2 (list-row ellipsis)-max_lines:2# ignore truncations whose text matches a regex (e.g. known ID/brand patterns)-text_regex:"^(RCP-|INV-)"# ignore truncations of a specific overflow mode-overflow:ellipsis
A rule matches only when all of its fields match the issue; the issue is
ignored if any rule matches. overflow must be one of ellipsis / clip /
fade / visible (matching TruncationIssue.overflowMode).
A rule with an unknown key (a typo such as text_regexp) or an unsupported
overflow value is skipped with a warning on stderr rather than applied with
its remaining fields — otherwise a typo next to a valid max_lines would ignore
far more than intended.
For advanced cases that reference a widget type or key, pass ignoreTruncation
to sweepTest using the TruncationIgnores helpers:
Note: byWidgetType / byKey rely on RenderParagraph.debugCreator,
which Flutter populates only in debug/asserts builds (flutter test). In
other builds those matchers simply won't match; byMaxLines / byText work
everywhere since they inspect the issue data only.
Environment variable overrides
Override any config value in CI without modifying YAML:
LOCALE_SWEEP_LOCALES=en,de LOCALE_SWEEP_TOLERANCE=1.0 dart run locale_sweep run
Variable
Overrides
LOCALE_SWEEP_CONFIG
Path to the YAML config file (default locale_sweep.yaml; the CLI sets it from --config)
LOCALE_SWEEP_LOCALES
locales (comma-separated)
LOCALE_SWEEP_TEXT_SCALES
text_scales (comma-separated)
LOCALE_SWEEP_DARK_MODE
dark_mode (true/false)
LOCALE_SWEEP_TOLERANCE
tolerance
LOCALE_SWEEP_SCREENSHOT_DIR
screenshot_dir
LOCALE_SWEEP_REPORT_DIR
report_dir
LOCALE_SWEEP_ARB_DIR
arb_dir
LOCALE_SWEEP_BASE_LOCALE
base_locale
LOCALE_SWEEP_UNTRANSLATED_ALLOWLIST
untranslated_allowlist (comma-separated)
LOCALE_SWEEP_HARDCODED_TEXT
hardcoded_text (true/false)
Reports
Three formats generated on every run:
HTML — self-contained dashboard with filters, screenshot gallery, summary cards, dark/RTL badges
Markdown — failure table with locale summary, ideal for PR comments
JSON — machine-readable results for custom dashboards or trend tracking
Each CLI invocation gets a separate run directory, so previous results cannot
leak into the current report. The 10 most recent run directories are kept.
Direct flutter test runs continue to write flow results under
.locale_sweep/results/.
Control which categories trigger a non-zero exit code:
dart run locale_sweep run --fail-on overflow,golden # only these categories fail
dart run locale_sweep run --fail-on none # report-only mode
dart run locale_sweep run --fail-on all # everything (default)
Categories: overflow, arb, golden, truncation, hardcoded, all, none
Truncations and hardcoded text are advisory: they are reported but do not fail
a direct flutter test run or count towards the failed-variant total. Use
--fail-on truncation or --fail-on hardcoded to promote them; note that
--fail-on all (the CLI default) already includes both.
--fail-on none suppresses QA findings only. Compilation errors, broken test
setup or callbacks, incomplete runs, and unreadable result files always return
a non-zero exit code.
Parallel sharding
Split large variant matrices across CI jobs for faster runs:
The merge command combines shard reports into a single HTML/Markdown/JSON report and optionally posts to the PR.
Monorepo support
Run sweep tests across multiple packages in a monorepo:
# Auto-discover packages with test/sweep/ directories
dart run locale_sweep scan
# Run sweep in specific packages
dart run locale_sweep run --packages apps/auth,apps/dashboard
# Combine with sharding
dart run locale_sweep run --packages apps/auth,apps/dashboard --shards 4 --shard-index 0
Auto-discovery checks for melos.yaml first, then scans for packages containing a test/sweep/ directory. Reports are generated per-package and merged into a single aggregate report.
CLI reference
dart run locale_sweep run # Compare goldens
dart run locale_sweep run --flows onboarding,checkout # Specific flows
dart run locale_sweep run --github-pr # Post PR comment
dart run locale_sweep run --fail-on overflow,golden # Selective failure
dart run locale_sweep run --config my_config.yaml # Custom config
dart run locale_sweep run --test-dir test/l10n # Sweep tests outside test/sweep
dart run locale_sweep run --verbose # Print flutter test output
dart run locale_sweep run --timeout 600 # Kill the run after 10 min
dart run locale_sweep run --shards 3 --shard-index 0 # Parallel shard
dart run locale_sweep run --packages apps/a,apps/b # Monorepo
dart run locale_sweep update # Regenerate baselines
dart run locale_sweep update --flows settings # Update specific flows
dart run locale_sweep merge -i shard_0 -i shard_1 # Merge shard reports
dart run locale_sweep scan # Discover monorepo packages
dart run locale_sweep scan --root packages # Discover under a subfolder
API reference
sweepTest() parameters
Parameter
Type
Default
Description
flowName
String
required
Identifies this flow in test labels and filenames
builder
Widget Function()?
required*
The widget to render
builderWithVariant
Widget Function(SweepVariant)?
required*
Like builder, but receives the current variant (pass its locale directly to MaterialApp). Exactly one of builder / builderWithVariant must be provided