← Back to Blog

Flutter Table Localization: Grid Layouts for Multilingual Apps

fluttertablegridlayoutlocalizationrtl

Flutter Table Localization: Grid Layouts for Multilingual Apps

Table is a Flutter widget that displays children in a grid of rows and columns with configurable column widths. In multilingual applications, Table is essential for presenting translated data in structured grids, adapting column widths for translations of varying lengths, aligning content correctly in RTL layouts, and building comparison tables and specification sheets with localized labels.

Understanding Table in Localization Context

Table renders children in a fixed grid where each row has the same number of cells and column widths can be controlled independently. For multilingual apps, this enables:

  • Translated header and data cells in a structured grid layout
  • Adaptive column widths that accommodate different translation lengths
  • RTL-aware cell alignment that reverses column order automatically
  • Specification tables with localized labels and values

Why Table Matters for Multilingual Apps

Table provides:

  • Fixed grid layout: Consistent rows and columns for translated data
  • Column width control: FlexColumnWidth, FixedColumnWidth, and IntrinsicColumnWidth adapt to translation length
  • Cell alignment: Per-cell vertical alignment works across all text directions
  • Border customization: Consistent borders regardless of translation length

Basic Table Implementation

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

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.specificationTitle)),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Table(
          border: TableBorder.all(
            color: Theme.of(context).colorScheme.outlineVariant,
          ),
          columnWidths: const {
            0: FlexColumnWidth(2),
            1: FlexColumnWidth(3),
          },
          children: [
            TableRow(
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.primaryContainer,
              ),
              children: [
                _HeaderCell(text: l10n.propertyHeader),
                _HeaderCell(text: l10n.valueHeader),
              ],
            ),
            _buildRow(l10n.dimensionsLabel, l10n.dimensionsValue),
            _buildRow(l10n.weightLabel, l10n.weightValue),
            _buildRow(l10n.materialLabel, l10n.materialValue),
            _buildRow(l10n.colorLabel, l10n.colorValue),
            _buildRow(l10n.warrantyLabel, l10n.warrantyValue),
          ],
        ),
      ),
    );
  }

  TableRow _buildRow(String label, String value) {
    return TableRow(
      children: [
        _DataCell(text: label, isBold: true),
        _DataCell(text: value),
      ],
    );
  }
}

class _HeaderCell extends StatelessWidget {
  final String text;
  const _HeaderCell({required this.text});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(12),
      child: Text(
        text,
        style: Theme.of(context).textTheme.titleSmall?.copyWith(
          color: Theme.of(context).colorScheme.onPrimaryContainer,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}

class _DataCell extends StatelessWidget {
  final String text;
  final bool isBold;
  const _DataCell({required this.text, this.isBold = false});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(12),
      child: Text(
        text,
        style: Theme.of(context).textTheme.bodyMedium?.copyWith(
          fontWeight: isBold ? FontWeight.w600 : null,
        ),
      ),
    );
  }
}

Advanced Table Patterns for Localization

Comparison Table with Localized Features

Feature comparison tables with translated feature names and plan labels.

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

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

    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Table(
        border: TableBorder.all(
          color: Theme.of(context).colorScheme.outlineVariant,
        ),
        columnWidths: const {
          0: FlexColumnWidth(3),
          1: FlexColumnWidth(2),
          2: FlexColumnWidth(2),
          3: FlexColumnWidth(2),
        },
        children: [
          TableRow(
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.primaryContainer,
            ),
            children: [
              _HeaderCell(text: l10n.featureHeader),
              _HeaderCell(text: l10n.basicPlanLabel),
              _HeaderCell(text: l10n.proPlanLabel),
              _HeaderCell(text: l10n.enterprisePlanLabel),
            ],
          ),
          _featureRow(context, l10n.featureStorage, '5 GB', '50 GB',
              l10n.unlimitedLabel),
          _featureRow(context, l10n.featureUsers, '1', '10',
              l10n.unlimitedLabel),
          _featureRow(context, l10n.featureSupport,
              l10n.emailSupportLabel, l10n.prioritySupportLabel,
              l10n.dedicatedSupportLabel),
          _featureRow(context, l10n.featureAnalytics,
              l10n.basicLabel, l10n.advancedLabel, l10n.customLabel),
        ],
      ),
    );
  }

  TableRow _featureRow(BuildContext context, String feature,
      String basic, String pro, String enterprise) {
    return TableRow(
      children: [
        _DataCell(text: feature, isBold: true),
        _DataCell(text: basic),
        _DataCell(text: pro),
        _DataCell(text: enterprise),
      ],
    );
  }
}

Schedule Table with Localized Time Slots

A timetable layout with translated day names and event descriptions.

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

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

    final days = [
      l10n.mondayShort,
      l10n.tuesdayShort,
      l10n.wednesdayShort,
      l10n.thursdayShort,
      l10n.fridayShort,
    ];

    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Table(
          border: TableBorder.all(
            color: Theme.of(context).colorScheme.outlineVariant,
          ),
          defaultColumnWidth: const FixedColumnWidth(120),
          columnWidths: const {
            0: FixedColumnWidth(80),
          },
          children: [
            TableRow(
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.primaryContainer,
              ),
              children: [
                _HeaderCell(text: l10n.timeHeader),
                ...days.map((day) => _HeaderCell(text: day)),
              ],
            ),
            _timeRow(context, '09:00', [
              l10n.meetingEvent, '', l10n.workshopEvent, '', l10n.reviewEvent,
            ]),
            _timeRow(context, '11:00', [
              '', l10n.presentationEvent, '', l10n.trainingEvent, '',
            ]),
            _timeRow(context, '14:00', [
              l10n.lunchEvent, l10n.lunchEvent, l10n.lunchEvent,
              l10n.lunchEvent, l10n.lunchEvent,
            ]),
          ],
        ),
      ),
    );
  }

  TableRow _timeRow(BuildContext context, String time, List<String> events) {
    return TableRow(
      children: [
        _DataCell(text: time, isBold: true),
        ...events.map((event) => _DataCell(text: event)),
      ],
    );
  }
}

Locale-Aware Column Widths

Adjust column proportions based on locale to accommodate verbose translations.

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final locale = Localizations.localeOf(context);
    final isVerbose = ['de', 'fi', 'hu', 'nl'].contains(locale.languageCode);

    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Table(
        border: TableBorder.all(
          color: Theme.of(context).colorScheme.outlineVariant,
        ),
        columnWidths: {
          0: FlexColumnWidth(isVerbose ? 3 : 2),
          1: FlexColumnWidth(isVerbose ? 2 : 3),
        },
        children: [
          TableRow(
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.surfaceContainerHighest,
            ),
            children: [
              _HeaderCell(text: l10n.settingLabel),
              _HeaderCell(text: l10n.descriptionLabel),
            ],
          ),
          _buildRow(l10n.languageSettingLabel, l10n.languageSettingDescription),
          _buildRow(l10n.themeSettingLabel, l10n.themeSettingDescription),
          _buildRow(l10n.notificationSettingLabel,
              l10n.notificationSettingDescription),
        ],
      ),
    );
  }

  TableRow _buildRow(String label, String description) {
    return TableRow(
      children: [
        _DataCell(text: label, isBold: true),
        _DataCell(text: description),
      ],
    );
  }
}

RTL Support and Bidirectional Layouts

Table automatically reverses column order in RTL layouts. Cell content aligns according to the active directionality.

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

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

    return Padding(
      padding: const EdgeInsetsDirectional.all(16),
      child: Table(
        border: TableBorder.all(
          color: Theme.of(context).colorScheme.outlineVariant,
        ),
        children: [
          TableRow(
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.primaryContainer,
            ),
            children: [
              _HeaderCell(text: l10n.nameHeader),
              _HeaderCell(text: l10n.roleHeader),
              _HeaderCell(text: l10n.statusHeader),
            ],
          ),
          TableRow(children: [
            _DataCell(text: l10n.sampleUserName),
            _DataCell(text: l10n.adminRole),
            _DataCell(text: l10n.activeStatus),
          ]),
          TableRow(children: [
            _DataCell(text: l10n.sampleUserName2),
            _DataCell(text: l10n.editorRole),
            _DataCell(text: l10n.inactiveStatus),
          ]),
        ],
      ),
    );
  }
}

Testing Table Localization

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

void main() {
  Widget buildTestWidget({Locale locale = const Locale('en')}) {
    return MaterialApp(
      locale: locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const LocalizedTableExample(),
    );
  }

  testWidgets('Table renders localized cells', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(Table), findsOneWidget);
  });

  testWidgets('Table works in RTL', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });
}

Best Practices

  1. Use FlexColumnWidth for proportional columns that adapt to available space, adjusting ratios for verbose languages.

  2. Use IntrinsicColumnWidth when columns should size to their widest translated content automatically.

  3. Wrap horizontal tables in SingleChildScrollView with Axis.horizontal when translations may make the table wider than the screen.

  4. Style header rows with a distinct background color and bold translated text to visually separate them from data rows.

  5. Use EdgeInsetsDirectional for table cell padding so insets adapt correctly in RTL layouts.

  6. Test with verbose languages to verify column widths accommodate longer translations without clipping.

Conclusion

Table provides a fixed grid layout for structured data in Flutter. For multilingual apps, it handles translated headers and data cells with configurable column widths that can adapt to translation length. By using proportional column widths, locale-aware sizing, and RTL-aware alignment, you can build specification sheets, comparison tables, and schedules that display correctly across all supported languages.

Further Reading