← Back to Blog

Flutter Baseline Localization: Text Alignment Across Languages

flutterbaselinetext-alignmenttypographylocalizationforms

Flutter Baseline Localization: Text Alignment Across Languages

The Baseline widget in Flutter provides precise control over text alignment by positioning children according to their text baselines. In multilingual applications, proper baseline alignment is crucial for maintaining visual harmony across different scripts and character sets.

Understanding Baseline in Localization Context

Baseline aligns its child based on the child's text baseline. This becomes particularly important when:

  • Mixing text elements with different font sizes
  • Aligning icons with localized text
  • Creating consistent form layouts across languages
  • Handling scripts with varying baseline positions (Arabic, Thai, Hindi)

Why Baseline Matters for Multilingual Apps

Different writing systems have unique baseline characteristics:

  • Latin scripts: Standard alphabetic baseline
  • Arabic/Hebrew: Different baseline due to right-to-left flow and character shapes
  • Thai: Characters extend significantly below baseline
  • Chinese/Japanese: Ideographic baseline differs from alphabetic
  • Hindi/Devanagari: Complex headline and baseline relationships

Basic Baseline Implementation

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedBaselineExample extends StatelessWidget {
  const LocalizedBaselineExample({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Row(
      crossAxisAlignment: CrossAxisAlignment.baseline,
      textBaseline: TextBaseline.alphabetic,
      children: [
        Text(
          l10n.priceLabel,
          style: Theme.of(context).textTheme.bodyMedium,
        ),
        const SizedBox(width: 8),
        Text(
          l10n.priceValue,
          style: Theme.of(context).textTheme.headlineLarge?.copyWith(
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(width: 4),
        Text(
          l10n.priceCurrency,
          style: Theme.of(context).textTheme.bodyMedium,
        ),
      ],
    );
  }
}

Price Display with Baseline Alignment

Localized Currency Display

class LocalizedPriceDisplay extends StatelessWidget {
  final String amount;
  final String currency;
  final String? originalAmount;

  const LocalizedPriceDisplay({
    super.key,
    required this.amount,
    required this.currency,
    this.originalAmount,
  });

  @override
  Widget build(BuildContext context) {
    final locale = Localizations.localeOf(context);
    final isRtl = Directionality.of(context) == TextDirection.rtl;

    return Row(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.baseline,
      textBaseline: TextBaseline.alphabetic,
      textDirection: Directionality.of(context),
      children: [
        if (originalAmount != null) ...[
          Text(
            originalAmount!,
            style: Theme.of(context).textTheme.bodyMedium?.copyWith(
              decoration: TextDecoration.lineThrough,
              color: Theme.of(context).colorScheme.outline,
            ),
          ),
          const SizedBox(width: 8),
        ],
        Text(
          _formatCurrency(amount, currency, locale, isRtl),
          style: Theme.of(context).textTheme.headlineMedium?.copyWith(
            fontWeight: FontWeight.bold,
            color: Theme.of(context).colorScheme.primary,
          ),
        ),
      ],
    );
  }

  String _formatCurrency(String amount, String currency, Locale locale, bool isRtl) {
    // Different locales format currency differently
    switch (locale.languageCode) {
      case 'ar':
        return '$amount $currency';
      case 'de':
      case 'fr':
        return '$amount $currency';
      default:
        return '$currency$amount';
    }
  }
}

Icon-Text Baseline Alignment

Localized List Items

class LocalizedBaselineListItem extends StatelessWidget {
  final IconData icon;
  final String text;
  final String? subtitle;

  const LocalizedBaselineListItem({
    super.key,
    required this.icon,
    required this.text,
    this.subtitle,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.baseline,
        textBaseline: TextBaseline.alphabetic,
        children: [
          Baseline(
            baseline: 20,
            baselineType: TextBaseline.alphabetic,
            child: Icon(
              icon,
              size: 20,
              color: Theme.of(context).colorScheme.primary,
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  text,
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
                if (subtitle != null) ...[
                  const SizedBox(height: 2),
                  Text(
                    subtitle!,
                    style: Theme.of(context).textTheme.bodySmall?.copyWith(
                      color: Theme.of(context).colorScheme.outline,
                    ),
                  ),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// Usage
class LocalizedFeatureList extends StatelessWidget {
  const LocalizedFeatureList({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Column(
      children: [
        LocalizedBaselineListItem(
          icon: Icons.speed,
          text: l10n.featureFastTitle,
          subtitle: l10n.featureFastDescription,
        ),
        LocalizedBaselineListItem(
          icon: Icons.security,
          text: l10n.featureSecureTitle,
          subtitle: l10n.featureSecureDescription,
        ),
        LocalizedBaselineListItem(
          icon: Icons.cloud_sync,
          text: l10n.featureSyncTitle,
          subtitle: l10n.featureSyncDescription,
        ),
      ],
    );
  }
}

Form Label Baseline Alignment

Aligned Form Layout

class LocalizedBaselineForm extends StatelessWidget {
  const LocalizedBaselineForm({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Column(
      children: [
        _BaselineFormRow(
          label: l10n.nameLabel,
          child: TextField(
            decoration: InputDecoration(
              hintText: l10n.nameHint,
              border: const OutlineInputBorder(),
            ),
          ),
        ),
        const SizedBox(height: 16),
        _BaselineFormRow(
          label: l10n.emailLabel,
          child: TextField(
            keyboardType: TextInputType.emailAddress,
            decoration: InputDecoration(
              hintText: l10n.emailHint,
              border: const OutlineInputBorder(),
            ),
          ),
        ),
        const SizedBox(height: 16),
        _BaselineFormRow(
          label: l10n.messageLabel,
          child: TextField(
            maxLines: 4,
            decoration: InputDecoration(
              hintText: l10n.messageHint,
              border: const OutlineInputBorder(),
            ),
          ),
        ),
      ],
    );
  }
}

class _BaselineFormRow extends StatelessWidget {
  final String label;
  final Widget child;

  const _BaselineFormRow({
    required this.label,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SizedBox(
          width: 100,
          child: Padding(
            padding: const EdgeInsets.only(top: 14),
            child: Text(
              label,
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.w500,
              ),
            ),
          ),
        ),
        const SizedBox(width: 16),
        Expanded(child: child),
      ],
    );
  }
}

Script-Aware Baseline Widget

Handling Different Writing Systems

class ScriptAwareBaseline extends StatelessWidget {
  final Widget child;
  final double alphabeticBaseline;
  final double ideographicBaseline;

  const ScriptAwareBaseline({
    super.key,
    required this.child,
    this.alphabeticBaseline = 0,
    this.ideographicBaseline = 0,
  });

  @override
  Widget build(BuildContext context) {
    final locale = Localizations.localeOf(context);
    final baselineType = _getBaselineType(locale);
    final baseline = baselineType == TextBaseline.ideographic
        ? ideographicBaseline
        : alphabeticBaseline;

    return Baseline(
      baseline: baseline,
      baselineType: baselineType,
      child: child,
    );
  }

  TextBaseline _getBaselineType(Locale locale) {
    switch (locale.languageCode) {
      case 'zh': // Chinese
      case 'ja': // Japanese
      case 'ko': // Korean
        return TextBaseline.ideographic;
      default:
        return TextBaseline.alphabetic;
    }
  }
}

// Usage
class LocalizedMixedContent extends StatelessWidget {
  const LocalizedMixedContent({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Row(
      crossAxisAlignment: CrossAxisAlignment.baseline,
      textBaseline: TextBaseline.alphabetic,
      children: [
        ScriptAwareBaseline(
          alphabeticBaseline: 24,
          ideographicBaseline: 28,
          child: Text(
            l10n.labelText,
            style: Theme.of(context).textTheme.bodyMedium,
          ),
        ),
        const SizedBox(width: 8),
        ScriptAwareBaseline(
          alphabeticBaseline: 24,
          ideographicBaseline: 28,
          child: Text(
            l10n.valueText,
            style: Theme.of(context).textTheme.titleLarge,
          ),
        ),
      ],
    );
  }
}

Baseline in Table Layouts

Localized Data Table

class LocalizedBaselineTable extends StatelessWidget {
  const LocalizedBaselineTable({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Table(
      columnWidths: const {
        0: FlexColumnWidth(2),
        1: FlexColumnWidth(1),
        2: FlexColumnWidth(1),
      },
      children: [
        _buildHeaderRow(context, l10n),
        _buildDataRow(
          context,
          l10n.productName1,
          l10n.productQuantity1,
          l10n.productPrice1,
        ),
        _buildDataRow(
          context,
          l10n.productName2,
          l10n.productQuantity2,
          l10n.productPrice2,
        ),
        _buildDataRow(
          context,
          l10n.productName3,
          l10n.productQuantity3,
          l10n.productPrice3,
        ),
      ],
    );
  }

  TableRow _buildHeaderRow(BuildContext context, AppLocalizations l10n) {
    return TableRow(
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(
            color: Theme.of(context).dividerColor,
            width: 2,
          ),
        ),
      ),
      children: [
        _BaselineCell(
          child: Text(
            l10n.tableHeaderProduct,
            style: Theme.of(context).textTheme.titleSmall?.copyWith(
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        _BaselineCell(
          child: Text(
            l10n.tableHeaderQuantity,
            style: Theme.of(context).textTheme.titleSmall?.copyWith(
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        _BaselineCell(
          child: Text(
            l10n.tableHeaderPrice,
            style: Theme.of(context).textTheme.titleSmall?.copyWith(
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ],
    );
  }

  TableRow _buildDataRow(
    BuildContext context,
    String name,
    String quantity,
    String price,
  ) {
    return TableRow(
      children: [
        _BaselineCell(child: Text(name)),
        _BaselineCell(child: Text(quantity)),
        _BaselineCell(
          child: Text(
            price,
            style: const TextStyle(fontWeight: FontWeight.w500),
          ),
        ),
      ],
    );
  }
}

class _BaselineCell extends StatelessWidget {
  final Widget child;

  const _BaselineCell({required this.child});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
      child: Baseline(
        baseline: 16,
        baselineType: TextBaseline.alphabetic,
        child: child,
      ),
    );
  }
}

Baseline with Superscript/Subscript

Localized Mathematical Notation

class LocalizedMathExpression extends StatelessWidget {
  const LocalizedMathExpression({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Row(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.baseline,
      textBaseline: TextBaseline.alphabetic,
      children: [
        Text(
          l10n.formulaLabel,
          style: Theme.of(context).textTheme.bodyMedium,
        ),
        const SizedBox(width: 8),
        _MathExpression(
          base: 'x',
          superscript: '2',
        ),
        Text(
          ' + ',
          style: Theme.of(context).textTheme.bodyLarge,
        ),
        _MathExpression(
          base: 'y',
          superscript: '2',
        ),
        Text(
          ' = ',
          style: Theme.of(context).textTheme.bodyLarge,
        ),
        _MathExpression(
          base: 'z',
          superscript: '2',
        ),
      ],
    );
  }
}

class _MathExpression extends StatelessWidget {
  final String base;
  final String? superscript;
  final String? subscript;

  const _MathExpression({
    required this.base,
    this.superscript,
    this.subscript,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          base,
          style: Theme.of(context).textTheme.titleLarge?.copyWith(
            fontStyle: FontStyle.italic,
          ),
        ),
        if (superscript != null || subscript != null)
          Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              if (superscript != null)
                Text(
                  superscript!,
                  style: Theme.of(context).textTheme.bodySmall,
                ),
              if (subscript != null)
                Text(
                  subscript!,
                  style: Theme.of(context).textTheme.bodySmall,
                ),
            ],
          ),
      ],
    );
  }
}

Baseline in Card Headers

Localized Info Card

class LocalizedInfoCard extends StatelessWidget {
  final IconData icon;
  final String title;
  final String value;
  final String? trend;
  final bool isPositive;

  const LocalizedInfoCard({
    super.key,
    required this.icon,
    required this.title,
    required this.value,
    this.trend,
    this.isPositive = true,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              crossAxisAlignment: CrossAxisAlignment.baseline,
              textBaseline: TextBaseline.alphabetic,
              children: [
                Baseline(
                  baseline: 18,
                  baselineType: TextBaseline.alphabetic,
                  child: Icon(
                    icon,
                    size: 20,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                ),
                const SizedBox(width: 8),
                Text(
                  title,
                  style: Theme.of(context).textTheme.titleSmall?.copyWith(
                    color: Theme.of(context).colorScheme.outline,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              crossAxisAlignment: CrossAxisAlignment.baseline,
              textBaseline: TextBaseline.alphabetic,
              children: [
                Text(
                  value,
                  style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
                if (trend != null) ...[
                  const SizedBox(width: 8),
                  Text(
                    trend!,
                    style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                      color: isPositive
                          ? Colors.green
                          : Theme.of(context).colorScheme.error,
                    ),
                  ),
                ],
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// Usage
class LocalizedDashboardCards extends StatelessWidget {
  const LocalizedDashboardCards({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Row(
      children: [
        Expanded(
          child: LocalizedInfoCard(
            icon: Icons.people,
            title: l10n.usersCardTitle,
            value: l10n.usersCardValue,
            trend: l10n.usersCardTrend,
            isPositive: true,
          ),
        ),
        const SizedBox(width: 16),
        Expanded(
          child: LocalizedInfoCard(
            icon: Icons.trending_up,
            title: l10n.revenueCardTitle,
            value: l10n.revenueCardValue,
            trend: l10n.revenueCardTrend,
            isPositive: true,
          ),
        ),
      ],
    );
  }
}

ARB File Structure

English (app_en.arb)

{
  "@@locale": "en",

  "priceLabel": "Price:",
  "@priceLabel": {
    "description": "Label before price display"
  },

  "priceValue": "99",
  "@priceValue": {
    "description": "Price amount"
  },

  "priceCurrency": "USD",
  "@priceCurrency": {
    "description": "Currency code"
  },

  "featureFastTitle": "Lightning Fast",
  "@featureFastTitle": {
    "description": "Title for speed feature"
  },

  "featureFastDescription": "Experience instant response times",
  "featureSecureTitle": "Bank-Grade Security",
  "featureSecureDescription": "Your data is protected 24/7",
  "featureSyncTitle": "Real-Time Sync",
  "featureSyncDescription": "Always stay up to date",

  "nameLabel": "Name",
  "nameHint": "Enter your full name",
  "emailLabel": "Email",
  "emailHint": "Enter your email address",
  "messageLabel": "Message",
  "messageHint": "Type your message here",

  "labelText": "Status:",
  "valueText": "Active",

  "tableHeaderProduct": "Product",
  "tableHeaderQuantity": "Qty",
  "tableHeaderPrice": "Price",
  "productName1": "Premium Widget",
  "productQuantity1": "5",
  "productPrice1": "$49.99",
  "productName2": "Standard Widget",
  "productQuantity2": "10",
  "productPrice2": "$29.99",
  "productName3": "Basic Widget",
  "productQuantity3": "25",
  "productPrice3": "$9.99",

  "formulaLabel": "Formula:",

  "usersCardTitle": "Total Users",
  "usersCardValue": "12,458",
  "usersCardTrend": "+12.5%",
  "revenueCardTitle": "Revenue",
  "revenueCardValue": "$84,230",
  "revenueCardTrend": "+8.2%"
}

German (app_de.arb)

{
  "@@locale": "de",

  "priceLabel": "Preis:",
  "priceValue": "99",
  "priceCurrency": "EUR",

  "featureFastTitle": "Blitzschnell",
  "featureFastDescription": "Erleben Sie sofortige Reaktionszeiten",
  "featureSecureTitle": "Banksicherheit",
  "featureSecureDescription": "Ihre Daten sind rund um die Uhr geschützt",
  "featureSyncTitle": "Echtzeit-Synchronisierung",
  "featureSyncDescription": "Immer auf dem neuesten Stand",

  "nameLabel": "Name",
  "nameHint": "Geben Sie Ihren vollständigen Namen ein",
  "emailLabel": "E-Mail",
  "emailHint": "Geben Sie Ihre E-Mail-Adresse ein",
  "messageLabel": "Nachricht",
  "messageHint": "Geben Sie hier Ihre Nachricht ein",

  "labelText": "Status:",
  "valueText": "Aktiv",

  "tableHeaderProduct": "Produkt",
  "tableHeaderQuantity": "Menge",
  "tableHeaderPrice": "Preis",
  "productName1": "Premium-Widget",
  "productQuantity1": "5",
  "productPrice1": "49,99 €",
  "productName2": "Standard-Widget",
  "productQuantity2": "10",
  "productPrice2": "29,99 €",
  "productName3": "Basis-Widget",
  "productQuantity3": "25",
  "productPrice3": "9,99 €",

  "formulaLabel": "Formel:",

  "usersCardTitle": "Gesamtbenutzer",
  "usersCardValue": "12.458",
  "usersCardTrend": "+12,5%",
  "revenueCardTitle": "Umsatz",
  "revenueCardValue": "84.230 €",
  "revenueCardTrend": "+8,2%"
}

Arabic (app_ar.arb)

{
  "@@locale": "ar",

  "priceLabel": "السعر:",
  "priceValue": "٩٩",
  "priceCurrency": "دولار",

  "featureFastTitle": "سريع للغاية",
  "featureFastDescription": "استمتع بأوقات استجابة فورية",
  "featureSecureTitle": "أمان بمستوى البنوك",
  "featureSecureDescription": "بياناتك محمية على مدار الساعة",
  "featureSyncTitle": "مزامنة في الوقت الفعلي",
  "featureSyncDescription": "ابق دائماً على اطلاع",

  "nameLabel": "الاسم",
  "nameHint": "أدخل اسمك الكامل",
  "emailLabel": "البريد الإلكتروني",
  "emailHint": "أدخل بريدك الإلكتروني",
  "messageLabel": "الرسالة",
  "messageHint": "اكتب رسالتك هنا",

  "labelText": "الحالة:",
  "valueText": "نشط",

  "tableHeaderProduct": "المنتج",
  "tableHeaderQuantity": "الكمية",
  "tableHeaderPrice": "السعر",
  "productName1": "ودجت متميز",
  "productQuantity1": "٥",
  "productPrice1": "٤٩٫٩٩ دولار",
  "productName2": "ودجت عادي",
  "productQuantity2": "١٠",
  "productPrice2": "٢٩٫٩٩ دولار",
  "productName3": "ودجت أساسي",
  "productQuantity3": "٢٥",
  "productPrice3": "٩٫٩٩ دولار",

  "formulaLabel": "الصيغة:",

  "usersCardTitle": "إجمالي المستخدمين",
  "usersCardValue": "١٢٬٤٥٨",
  "usersCardTrend": "+١٢٫٥٪",
  "revenueCardTitle": "الإيرادات",
  "revenueCardValue": "٨٤٬٢٣٠ دولار",
  "revenueCardTrend": "+٨٫٢٪"
}

Best Practices Summary

Do's

  1. Use TextBaseline.alphabetic for Latin, Cyrillic, and Arabic scripts
  2. Use TextBaseline.ideographic for Chinese, Japanese, and Korean
  3. Test with actual translated content to verify baseline alignment
  4. Combine with CrossAxisAlignment.baseline in Row widgets
  5. Consider script variations when mixing multiple languages

Don'ts

  1. Don't assume all scripts align the same way
  2. Don't hardcode baseline values without considering localization
  3. Don't ignore RTL text direction when aligning baselines
  4. Don't mix baseline types in a single row without careful consideration

Accessibility Considerations

class AccessibleBaselineLayout extends StatelessWidget {
  const AccessibleBaselineLayout({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final textScaleFactor = MediaQuery.textScaleFactorOf(context);

    // Adjust baseline for larger text scales
    final baselineOffset = 20 * textScaleFactor;

    return Semantics(
      label: '${l10n.priceLabel} ${l10n.priceValue} ${l10n.priceCurrency}',
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.baseline,
        textBaseline: TextBaseline.alphabetic,
        children: [
          Text(l10n.priceLabel),
          const SizedBox(width: 8),
          Text(
            l10n.priceValue,
            style: Theme.of(context).textTheme.headlineLarge,
          ),
        ],
      ),
    );
  }
}

Conclusion

Baseline alignment is a subtle but powerful tool for creating polished multilingual Flutter applications. By understanding how different scripts position their baselines and using the appropriate TextBaseline type, you can ensure text elements align beautifully regardless of the language being displayed. Always test with your target languages to verify alignment works correctly across different writing systems.

Further Reading