A button that fits "Pay now" in English often clips "Payer maintenant" in French. Arabic flips the whole row. You usually find out late: someone switches the device language, opens the screen, and files a bug.
Flutter's Widget Previewer can show you all of this while you write the widget. One @Preview annotation renders a widget without running the app, and with a small MultiPreview class you get one preview per language, a right-to-left copy and a pseudo-locale that makes every string longer.

In the screenshot, the French and pseudo-locale buttons already show the clipping that English hides.
Widget previews in two lines
Widget previews are part of Flutter (package:flutter/widget_previews.dart). Annotate a public top-level function, a static method or a constructor without required arguments:
import 'package:flutter/widget_previews.dart';
@Preview(name: 'Checkout card')
Widget checkoutCardPreview() => const CheckoutCard();
Then run flutter widget-preview start in the project, or open the Flutter Widget Preview panel in your IDE. The previewer reloads as you edit.
Preview takes a localizations callback, which is how a preview gets your AppLocalizations:
PreviewLocalizationsData french() => const PreviewLocalizationsData(
locale: Locale('fr'),
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
);
@Preview(name: 'French', localizations: french)
Widget checkoutCardFrench() => const CheckoutCard();
Writing one function per language gets old fast, and the list goes stale every time you add a locale.
One annotation for every locale
MultiPreview turns one annotation into several previews. Annotation arguments must be constant, but transform() runs at preview time, so it can read AppLocalizations.supportedLocales and build a preview for each:
final class LocalePreview extends MultiPreview {
const LocalePreview();
@override
List<Preview> get previews => const <Preview>[];
@override
List<Preview> transform() => <Preview>[
...super.transform(),
for (final locale in AppLocalizations.supportedLocales)
Preview(
name: locale.toLanguageTag(),
localizations: () => PreviewLocalizationsData(
locale: locale,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
),
),
];
}
@LocalePreview()
Widget checkoutCardPreview() => const CheckoutCard();
Add a locale to your ARB files and it shows up in the previewer on the next reload. This needs Flutter 3.38 or later, the first stable release where MultiPreview has transform().
Right to left, even without an RTL language
If the app has an Arabic, Hebrew, Persian or Urdu locale, its preview is already right to left, because GlobalWidgetsLocalizations sets the direction from the locale. If it has none yet, preview the template language with the direction forced, so you see the mirrored layout before the first RTL translation arrives:
Preview(
name: 'en, right to left',
localizations: () => PreviewLocalizationsData(locale: const Locale('en'), /* ... */),
wrapper: (child) => Directionality(textDirection: TextDirection.rtl, child: child),
),
Look for icons that should flip (a forward arrow), EdgeInsets.only(left: ...) that should be EdgeInsetsDirectional, and text aligned with TextAlign.left instead of start.
A pseudo-locale for text that has not been translated yet
Translations come late, and layout bugs are cheaper before they do. A pseudo-locale fakes them: every message keeps its meaning but gets accents and about 40% more characters, wrapped in brackets so a clipped end is obvious.
Pay now -> [Páý ñöw~~~]
Total: €42 -> [Tötál: €42~~~~]
Forty percent is roughly how much longer German or French text runs than English in short UI labels. The accents catch fonts without the glyphs and fixed line heights that cut off diacritics.
gen-l10n has no pseudo-locale, but its generated classes make one easy: subclass the template locale's class and wrap every message.
class _Pseudo extends AppLocalizationsEn {
@override
String get payNow => _p(super.payNow);
@override
String total(double amount) => _p(super.total(amount));
}
Plurals, selects and number formats still go through the generated code, so items(3) becomes [3 ítéms~~~], not raw ICU. A LocalizationsDelegate that returns _Pseudo(), placed before AppLocalizations.delegate, makes it the active localizations in that one preview.
The catch is that this class has to list every message and follow your ARB files as they change.
The one-click version
Our free VS Code extension writes and maintains all of this. Put the cursor on a widget class and choose Preview in all locales from the light bulb or the context menu. It:
- writes
locale_preview.dartnext to your generated localizations, withLocalePreview, the right-to-left preview and the pseudo-locale; - adds the
@LocalePreview()function under your widget, withconstwhen the constructor allows it, and selects the call when the widget needs arguments; - rewrites the pseudo-locale every time the localizations are regenerated, so new keys are covered.
It works in VS Code, Cursor and Windsurf, with plain gen-l10n and with the flutter_localisation package (context.tr). Options: @LocalePreview(size: Size(360, 640)), group: and pseudo: false.
Install it with:
code --install-extension flutterlocalisation.flutter-l10n
Limits worth knowing
- The previewer runs your widgets on Flutter web. Widgets that call a mobile-only plugin need a fake or a wrapper.
- Previews show one widget, not a navigation flow. Preview the screens and cards where text has the least room.
- A pseudo-locale only catches length and glyph problems. Grammar, plural rules and word order still need real translations and someone who reads the language.
Checklist before shipping a new language
- Every screen with tight text has a
@LocalePreview(). - Nothing clips in the pseudo-locale, or you decided to allow
TextOverflow.ellipsisthere. - The right-to-left preview mirrors icons and padding.
- The new locale's ARB has no missing keys.
- You looked at the new language's preview once it was translated.