← Back to Blog

Flutter CustomPaint Localization: Canvas Drawing for Multilingual Apps

fluttercustompaintcanvaschartslocalizationrtl

Flutter CustomPaint Localization: Canvas Drawing for Multilingual Apps

CustomPaint enables custom drawing on a canvas, allowing for charts, diagrams, signatures, custom shapes, and artistic effects. When combined with localization, CustomPaint creates RTL-aware graphics, culturally appropriate visualizations, and accessible custom elements that adapt to different languages and reading directions. This guide covers comprehensive strategies for localizing CustomPaint widgets in Flutter multilingual applications.

Understanding CustomPaint Localization

CustomPaint widgets require localization for:

  • RTL mirroring: Flipping charts and diagrams for right-to-left languages
  • Text rendering: Drawing localized text on canvas
  • Cultural symbols: Adapting visual elements for different cultures
  • Accessibility labels: Describing painted content for screen readers
  • Data labels: Localized values in charts and graphs
  • Direction-aware paths: Drawing paths that respect reading direction

Basic CustomPaint with RTL Support

Start with a simple RTL-aware progress bar:

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

class LocalizedProgressPainter extends CustomPainter {
  final double progress;
  final Color backgroundColor;
  final Color progressColor;
  final bool isRtl;

  LocalizedProgressPainter({
    required this.progress,
    required this.backgroundColor,
    required this.progressColor,
    required this.isRtl,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final backgroundPaint = Paint()
      ..color = backgroundColor
      ..style = PaintingStyle.fill;

    final progressPaint = Paint()
      ..color = progressColor
      ..style = PaintingStyle.fill;

    // Draw background
    final backgroundRect = RRect.fromRectAndRadius(
      Rect.fromLTWH(0, 0, size.width, size.height),
      const Radius.circular(10),
    );
    canvas.drawRRect(backgroundRect, backgroundPaint);

    // Draw progress - flip for RTL
    final progressWidth = size.width * progress;
    final progressRect = RRect.fromRectAndRadius(
      Rect.fromLTWH(
        isRtl ? size.width - progressWidth : 0,
        0,
        progressWidth,
        size.height,
      ),
      const Radius.circular(10),
    );
    canvas.drawRRect(progressRect, progressPaint);
  }

  @override
  bool shouldRepaint(covariant LocalizedProgressPainter oldDelegate) {
    return oldDelegate.progress != progress ||
        oldDelegate.isRtl != isRtl ||
        oldDelegate.progressColor != progressColor;
  }
}

class LocalizedProgressDemo extends StatefulWidget {
  const LocalizedProgressDemo({super.key});

  @override
  State<LocalizedProgressDemo> createState() => _LocalizedProgressDemoState();
}

class _LocalizedProgressDemoState extends State<LocalizedProgressDemo> {
  double _progress = 0.65;

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.customPaintDemoTitle)),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.progressLabel,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            Semantics(
              label: l10n.progressAccessibility((_progress * 100).round()),
              child: SizedBox(
                height: 20,
                child: CustomPaint(
                  painter: LocalizedProgressPainter(
                    progress: _progress,
                    backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
                    progressColor: Theme.of(context).colorScheme.primary,
                    isRtl: isRtl,
                  ),
                  size: const Size(double.infinity, 20),
                ),
              ),
            ),
            const SizedBox(height: 8),
            Text(
              l10n.progressPercentage((_progress * 100).round()),
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 24),
            Slider(
              value: _progress,
              onChanged: (value) => setState(() => _progress = value),
            ),
          ],
        ),
      ),
    );
  }
}

ARB File Structure for CustomPaint

{
  "customPaintDemoTitle": "Custom Paint",
  "@customPaintDemoTitle": {
    "description": "Title for custom paint demo page"
  },
  "progressLabel": "Download Progress",
  "progressAccessibility": "Progress: {percent} percent complete",
  "@progressAccessibility": {
    "placeholders": {
      "percent": {"type": "int"}
    }
  },
  "progressPercentage": "{percent}% complete",
  "@progressPercentage": {
    "placeholders": {
      "percent": {"type": "int"}
    }
  }
}

Localized Bar Chart

Create a bar chart with localized labels:

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

class LocalizedBarChartPainter extends CustomPainter {
  final List<double> values;
  final List<String> labels;
  final Color barColor;
  final Color labelColor;
  final bool isRtl;
  final TextStyle labelStyle;

  LocalizedBarChartPainter({
    required this.values,
    required this.labels,
    required this.barColor,
    required this.labelColor,
    required this.isRtl,
    required this.labelStyle,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final barPaint = Paint()
      ..color = barColor
      ..style = PaintingStyle.fill;

    final maxValue = values.reduce((a, b) => a > b ? a : b);
    final barWidth = (size.width - 40) / values.length - 10;
    final maxBarHeight = size.height - 50;

    for (var i = 0; i < values.length; i++) {
      // Calculate position - flip for RTL
      final index = isRtl ? values.length - 1 - i : i;
      final barHeight = (values[index] / maxValue) * maxBarHeight;
      final x = 20 + i * (barWidth + 10);
      final y = size.height - 30 - barHeight;

      // Draw bar
      final barRect = RRect.fromRectAndRadius(
        Rect.fromLTWH(x, y, barWidth, barHeight),
        const Radius.circular(4),
      );
      canvas.drawRRect(barRect, barPaint);

      // Draw label
      final textPainter = TextPainter(
        text: TextSpan(
          text: labels[index],
          style: labelStyle.copyWith(color: labelColor),
        ),
        textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
      );
      textPainter.layout(maxWidth: barWidth);
      textPainter.paint(
        canvas,
        Offset(
          x + (barWidth - textPainter.width) / 2,
          size.height - 25,
        ),
      );

      // Draw value
      final valuePainter = TextPainter(
        text: TextSpan(
          text: values[index].toStringAsFixed(0),
          style: labelStyle.copyWith(
            color: labelColor,
            fontWeight: FontWeight.bold,
          ),
        ),
        textDirection: TextDirection.ltr,
      );
      valuePainter.layout();
      valuePainter.paint(
        canvas,
        Offset(
          x + (barWidth - valuePainter.width) / 2,
          y - 20,
        ),
      );
    }
  }

  @override
  bool shouldRepaint(covariant LocalizedBarChartPainter oldDelegate) {
    return oldDelegate.values != values ||
        oldDelegate.labels != labels ||
        oldDelegate.isRtl != isRtl;
  }
}

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

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

    final labels = [
      l10n.monthJan,
      l10n.monthFeb,
      l10n.monthMar,
      l10n.monthApr,
      l10n.monthMay,
    ];
    final values = [45.0, 72.0, 58.0, 89.0, 63.0];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.barChartTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.monthlySalesLabel,
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            Text(
              l10n.chartDescription,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 24),
            Expanded(
              child: Semantics(
                label: l10n.barChartAccessibility(
                  labels.join(', '),
                ),
                child: CustomPaint(
                  painter: LocalizedBarChartPainter(
                    values: values,
                    labels: labels,
                    barColor: Theme.of(context).colorScheme.primary,
                    labelColor: Theme.of(context).colorScheme.onSurface,
                    isRtl: isRtl,
                    labelStyle: Theme.of(context).textTheme.bodySmall!,
                  ),
                  size: Size.infinite,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

RTL-Aware Line Chart

Create a line chart that adapts to reading direction:

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

class LocalizedLineChartPainter extends CustomPainter {
  final List<double> values;
  final Color lineColor;
  final Color fillColor;
  final Color gridColor;
  final bool isRtl;
  final bool showGrid;
  final bool showArea;

  LocalizedLineChartPainter({
    required this.values,
    required this.lineColor,
    required this.fillColor,
    required this.gridColor,
    required this.isRtl,
    this.showGrid = true,
    this.showArea = true,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final padding = 40.0;
    final chartWidth = size.width - padding * 2;
    final chartHeight = size.height - padding * 2;

    // Draw grid
    if (showGrid) {
      _drawGrid(canvas, size, padding, chartWidth, chartHeight);
    }

    // Calculate points
    final maxValue = values.reduce((a, b) => a > b ? a : b);
    final minValue = values.reduce((a, b) => a < b ? a : b);
    final valueRange = maxValue - minValue;

    final points = <Offset>[];
    for (var i = 0; i < values.length; i++) {
      // Flip x position for RTL
      final xIndex = isRtl ? values.length - 1 - i : i;
      final x = padding + (chartWidth / (values.length - 1)) * i;
      final normalizedValue = (values[xIndex] - minValue) / valueRange;
      final y = padding + chartHeight * (1 - normalizedValue);
      points.add(Offset(x, y));
    }

    // Draw filled area
    if (showArea) {
      final areaPath = Path();
      areaPath.moveTo(points.first.dx, size.height - padding);
      for (final point in points) {
        areaPath.lineTo(point.dx, point.dy);
      }
      areaPath.lineTo(points.last.dx, size.height - padding);
      areaPath.close();

      final areaPaint = Paint()
        ..shader = ui.Gradient.linear(
          Offset(0, padding),
          Offset(0, size.height - padding),
          [fillColor.withOpacity(0.5), fillColor.withOpacity(0.0)],
        );
      canvas.drawPath(areaPath, areaPaint);
    }

    // Draw line
    final linePath = Path();
    linePath.moveTo(points.first.dx, points.first.dy);
    for (var i = 1; i < points.length; i++) {
      linePath.lineTo(points[i].dx, points[i].dy);
    }

    final linePaint = Paint()
      ..color = lineColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3
      ..strokeCap = StrokeCap.round
      ..strokeJoin = StrokeJoin.round;
    canvas.drawPath(linePath, linePaint);

    // Draw points
    final pointPaint = Paint()
      ..color = lineColor
      ..style = PaintingStyle.fill;
    for (final point in points) {
      canvas.drawCircle(point, 5, pointPaint);
      canvas.drawCircle(
        point,
        3,
        Paint()..color = Colors.white,
      );
    }
  }

  void _drawGrid(
    Canvas canvas,
    Size size,
    double padding,
    double chartWidth,
    double chartHeight,
  ) {
    final gridPaint = Paint()
      ..color = gridColor.withOpacity(0.3)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1;

    // Horizontal lines
    for (var i = 0; i <= 4; i++) {
      final y = padding + (chartHeight / 4) * i;
      canvas.drawLine(
        Offset(padding, y),
        Offset(size.width - padding, y),
        gridPaint,
      );
    }

    // Vertical lines
    for (var i = 0; i < values.length; i++) {
      final x = padding + (chartWidth / (values.length - 1)) * i;
      canvas.drawLine(
        Offset(x, padding),
        Offset(x, size.height - padding),
        gridPaint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant LocalizedLineChartPainter oldDelegate) {
    return oldDelegate.values != values || oldDelegate.isRtl != isRtl;
  }
}

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

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

    final values = [23.0, 45.0, 32.0, 67.0, 54.0, 78.0, 61.0];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.lineChartTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.weeklyActivityLabel,
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 24),
            Expanded(
              child: Semantics(
                label: l10n.lineChartAccessibility(values.length),
                child: CustomPaint(
                  painter: LocalizedLineChartPainter(
                    values: values,
                    lineColor: Theme.of(context).colorScheme.primary,
                    fillColor: Theme.of(context).colorScheme.primary,
                    gridColor: Theme.of(context).colorScheme.outline,
                    isRtl: isRtl,
                  ),
                  size: Size.infinite,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Localized Signature Pad

Create a signature capture with RTL support:

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

class LocalizedSignaturePainter extends CustomPainter {
  final List<List<Offset>> strokes;
  final Color penColor;
  final double penWidth;

  LocalizedSignaturePainter({
    required this.strokes,
    required this.penColor,
    required this.penWidth,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = penColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = penWidth
      ..strokeCap = StrokeCap.round
      ..strokeJoin = StrokeJoin.round;

    for (final stroke in strokes) {
      if (stroke.length < 2) continue;

      final path = Path();
      path.moveTo(stroke.first.dx, stroke.first.dy);

      for (var i = 1; i < stroke.length; i++) {
        path.lineTo(stroke[i].dx, stroke[i].dy);
      }

      canvas.drawPath(path, paint);
    }
  }

  @override
  bool shouldRepaint(covariant LocalizedSignaturePainter oldDelegate) {
    return oldDelegate.strokes != strokes;
  }
}

class SignaturePadDemo extends StatefulWidget {
  const SignaturePadDemo({super.key});

  @override
  State<SignaturePadDemo> createState() => _SignaturePadDemoState();
}

class _SignaturePadDemoState extends State<SignaturePadDemo> {
  final List<List<Offset>> _strokes = [];
  List<Offset> _currentStroke = [];

  void _clear() {
    setState(() {
      _strokes.clear();
      _currentStroke = [];
    });
  }

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

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.signaturePadTitle),
        actions: [
          IconButton(
            onPressed: _clear,
            icon: const Icon(Icons.clear),
            tooltip: l10n.clearSignatureTooltip,
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.signatureInstructions,
              style: Theme.of(context).textTheme.bodyLarge,
            ),
            const SizedBox(height: 16),
            Expanded(
              child: Semantics(
                label: l10n.signatureAreaAccessibility,
                child: Container(
                  decoration: BoxDecoration(
                    border: Border.all(
                      color: Theme.of(context).colorScheme.outline,
                    ),
                    borderRadius: BorderRadius.circular(12),
                    color: Theme.of(context).colorScheme.surface,
                  ),
                  child: Stack(
                    children: [
                      // Signature line
                      Positioned(
                        bottom: 50,
                        left: isRtl ? null : 20,
                        right: isRtl ? 20 : null,
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Container(
                              width: 200,
                              height: 1,
                              color: Theme.of(context).colorScheme.outline,
                            ),
                            const SizedBox(width: 8),
                            Text(
                              'X',
                              style: TextStyle(
                                color: Theme.of(context).colorScheme.outline,
                              ),
                            ),
                          ],
                        ),
                      ),
                      // Drawing area
                      GestureDetector(
                        onPanStart: (details) {
                          setState(() {
                            _currentStroke = [details.localPosition];
                          });
                        },
                        onPanUpdate: (details) {
                          setState(() {
                            _currentStroke.add(details.localPosition);
                          });
                        },
                        onPanEnd: (details) {
                          setState(() {
                            _strokes.add(List.from(_currentStroke));
                            _currentStroke = [];
                          });
                        },
                        child: CustomPaint(
                          painter: LocalizedSignaturePainter(
                            strokes: [..._strokes, _currentStroke],
                            penColor: Theme.of(context).colorScheme.onSurface,
                            penWidth: 2,
                          ),
                          size: Size.infinite,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: OutlinedButton(
                    onPressed: _clear,
                    child: Text(l10n.clearButton),
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: ElevatedButton(
                    onPressed: _strokes.isNotEmpty ? () {} : null,
                    child: Text(l10n.saveSignatureButton),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Localized Pie Chart

Create a pie chart with localized labels:

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

class PieChartData {
  final String label;
  final double value;
  final Color color;

  PieChartData({
    required this.label,
    required this.value,
    required this.color,
  });
}

class LocalizedPieChartPainter extends CustomPainter {
  final List<PieChartData> data;
  final bool isRtl;
  final TextStyle labelStyle;

  LocalizedPieChartPainter({
    required this.data,
    required this.isRtl,
    required this.labelStyle,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = math.min(size.width, size.height) / 2 - 40;

    final total = data.fold<double>(0, (sum, item) => sum + item.value);

    // Start from top for LTR, or top-right for RTL
    double startAngle = -math.pi / 2;
    if (isRtl) {
      startAngle = math.pi / 2;
    }

    for (final item in data) {
      final sweepAngle = (item.value / total) * 2 * math.pi;
      final actualSweep = isRtl ? -sweepAngle : sweepAngle;

      // Draw slice
      final paint = Paint()
        ..color = item.color
        ..style = PaintingStyle.fill;

      canvas.drawArc(
        Rect.fromCircle(center: center, radius: radius),
        startAngle,
        actualSweep,
        true,
        paint,
      );

      // Draw border
      canvas.drawArc(
        Rect.fromCircle(center: center, radius: radius),
        startAngle,
        actualSweep,
        true,
        Paint()
          ..color = Colors.white
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );

      // Draw label
      final midAngle = startAngle + actualSweep / 2;
      final labelRadius = radius * 0.7;
      final labelX = center.dx + labelRadius * math.cos(midAngle);
      final labelY = center.dy + labelRadius * math.sin(midAngle);

      final percentage = (item.value / total * 100).toStringAsFixed(0);
      final textPainter = TextPainter(
        text: TextSpan(
          text: '$percentage%',
          style: labelStyle.copyWith(
            color: Colors.white,
            fontWeight: FontWeight.bold,
          ),
        ),
        textDirection: TextDirection.ltr,
      );
      textPainter.layout();
      textPainter.paint(
        canvas,
        Offset(labelX - textPainter.width / 2, labelY - textPainter.height / 2),
      );

      startAngle += actualSweep;
    }
  }

  @override
  bool shouldRepaint(covariant LocalizedPieChartPainter oldDelegate) {
    return oldDelegate.data != data || oldDelegate.isRtl != isRtl;
  }
}

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

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

    final data = [
      PieChartData(label: l10n.categoryFood, value: 35, color: Colors.blue),
      PieChartData(label: l10n.categoryTransport, value: 25, color: Colors.green),
      PieChartData(label: l10n.categoryEntertainment, value: 20, color: Colors.orange),
      PieChartData(label: l10n.categoryOther, value: 20, color: Colors.purple),
    ];

    return Scaffold(
      appBar: AppBar(title: Text(l10n.pieChartTitle)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.expenseBreakdownLabel,
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 24),
            Expanded(
              child: Semantics(
                label: l10n.pieChartAccessibility,
                child: CustomPaint(
                  painter: LocalizedPieChartPainter(
                    data: data,
                    isRtl: isRtl,
                    labelStyle: Theme.of(context).textTheme.bodyMedium!,
                  ),
                  size: Size.infinite,
                ),
              ),
            ),
            const SizedBox(height: 24),
            // Legend
            Wrap(
              spacing: 16,
              runSpacing: 8,
              children: data.map((item) {
                return Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Container(
                      width: 16,
                      height: 16,
                      decoration: BoxDecoration(
                        color: item.color,
                        borderRadius: BorderRadius.circular(4),
                      ),
                    ),
                    const SizedBox(width: 8),
                    Text(item.label),
                  ],
                );
              }).toList(),
            ),
          ],
        ),
      ),
    );
  }
}

Complete ARB File for CustomPaint

{
  "@@locale": "en",

  "customPaintDemoTitle": "Custom Paint",
  "progressLabel": "Download Progress",
  "progressAccessibility": "Progress: {percent} percent complete",
  "@progressAccessibility": {
    "placeholders": {"percent": {"type": "int"}}
  },
  "progressPercentage": "{percent}% complete",
  "@progressPercentage": {
    "placeholders": {"percent": {"type": "int"}}
  },

  "barChartTitle": "Sales Chart",
  "monthlySalesLabel": "Monthly Sales Performance",
  "chartDescription": "Sales figures for the past 5 months",
  "barChartAccessibility": "Bar chart showing sales data for {labels}",
  "@barChartAccessibility": {
    "placeholders": {"labels": {"type": "String"}}
  },
  "monthJan": "Jan",
  "monthFeb": "Feb",
  "monthMar": "Mar",
  "monthApr": "Apr",
  "monthMay": "May",

  "lineChartTitle": "Activity Trends",
  "weeklyActivityLabel": "Weekly Activity",
  "lineChartAccessibility": "Line chart showing {count} data points",
  "@lineChartAccessibility": {
    "placeholders": {"count": {"type": "int"}}
  },

  "signaturePadTitle": "Signature",
  "signatureInstructions": "Please sign in the area below",
  "signatureAreaAccessibility": "Signature drawing area. Use touch or stylus to sign.",
  "clearSignatureTooltip": "Clear signature",
  "clearButton": "Clear",
  "saveSignatureButton": "Save Signature",

  "pieChartTitle": "Expense Analysis",
  "expenseBreakdownLabel": "Monthly Expense Breakdown",
  "pieChartAccessibility": "Pie chart showing expense distribution by category",
  "categoryFood": "Food",
  "categoryTransport": "Transport",
  "categoryEntertainment": "Entertainment",
  "categoryOther": "Other"
}

Best Practices Summary

  1. Mirror for RTL: Flip chart axes and reading direction for RTL languages
  2. Localize all text: Translate labels, legends, and tooltips
  3. Provide accessibility: Add semantic descriptions for screen readers
  4. Use localized number formats: Format values according to locale
  5. Handle text direction: Use TextPainter with correct textDirection
  6. Test with long text: Ensure labels fit with different language lengths
  7. Consider cultural colors: Some colors have different meanings across cultures
  8. Cache painters: Implement shouldRepaint efficiently
  9. Responsive sizing: Adapt chart sizes to available space
  10. Announce changes: Notify assistive technologies of data updates

Conclusion

CustomPaint enables powerful custom graphics like charts, signatures, and diagrams that can adapt to different languages and reading directions. By respecting RTL layouts, localizing labels, and providing proper accessibility descriptions, you can build visually rich applications that work seamlessly for users worldwide. The key is ensuring that all painted content respects the user's language direction and that visual elements are properly described for accessibility.

Remember to test your custom painted elements across different locales and with assistive technologies to ensure they provide a consistent experience for all users.