← Back to Blog

Flutter Onboarding Localization: Multilingual Walkthrough and Tutorial Screens

flutteronboardingwalkthroughtutoriallocalizationux

Flutter Onboarding Localization: Multilingual Walkthrough and Tutorial Screens

Create onboarding experiences that welcome users in their language. This guide covers localizing welcome screens, feature tutorials, permission explanations, and app introductions in Flutter.

Onboarding Localization Needs

Onboarding flows require localization for:

  • Welcome messages - First impressions matter
  • Feature explanations - What the app does
  • Permission requests - Why you need access
  • Action buttons - Next, Skip, Get Started
  • Images/illustrations - Culture-appropriate visuals

Localized Onboarding Page Model

Data Structure

class OnboardingPage {
  final String titleKey;
  final String descriptionKey;
  final String imageAsset;
  final Map<String, String>? localizedImages;
  final Color? backgroundColor;
  final bool showSkip;

  const OnboardingPage({
    required this.titleKey,
    required this.descriptionKey,
    required this.imageAsset,
    this.localizedImages,
    this.backgroundColor,
    this.showSkip = true,
  });

  String getTitle(AppLocalizations l10n) {
    // Use reflection or a map to get localized string
    return _getLocalizedString(l10n, titleKey);
  }

  String getDescription(AppLocalizations l10n) {
    return _getLocalizedString(l10n, descriptionKey);
  }

  String getImageForLocale(String locale) {
    if (localizedImages != null && localizedImages!.containsKey(locale)) {
      return localizedImages![locale]!;
    }
    // Check language only
    final lang = locale.split('_').first;
    if (localizedImages != null && localizedImages!.containsKey(lang)) {
      return localizedImages![lang]!;
    }
    return imageAsset;
  }

  String _getLocalizedString(AppLocalizations l10n, String key) {
    // Map keys to localized strings
    final strings = {
      'onboarding_welcome_title': l10n.onboardingWelcomeTitle,
      'onboarding_welcome_description': l10n.onboardingWelcomeDescription,
      'onboarding_features_title': l10n.onboardingFeaturesTitle,
      'onboarding_features_description': l10n.onboardingFeaturesDescription,
      'onboarding_sync_title': l10n.onboardingSyncTitle,
      'onboarding_sync_description': l10n.onboardingSyncDescription,
      'onboarding_notifications_title': l10n.onboardingNotificationsTitle,
      'onboarding_notifications_description': l10n.onboardingNotificationsDescription,
      'onboarding_ready_title': l10n.onboardingReadyTitle,
      'onboarding_ready_description': l10n.onboardingReadyDescription,
    };
    return strings[key] ?? key;
  }
}

// Define onboarding pages
const onboardingPages = [
  OnboardingPage(
    titleKey: 'onboarding_welcome_title',
    descriptionKey: 'onboarding_welcome_description',
    imageAsset: 'assets/onboarding/welcome.png',
    localizedImages: {
      'ar': 'assets/onboarding/welcome_ar.png', // RTL version
      'ja': 'assets/onboarding/welcome_ja.png', // Japanese text in image
      'zh': 'assets/onboarding/welcome_zh.png', // Chinese text in image
    },
    backgroundColor: Color(0xFF6C63FF),
  ),
  OnboardingPage(
    titleKey: 'onboarding_features_title',
    descriptionKey: 'onboarding_features_description',
    imageAsset: 'assets/onboarding/features.png',
    backgroundColor: Color(0xFF00BFA5),
  ),
  OnboardingPage(
    titleKey: 'onboarding_sync_title',
    descriptionKey: 'onboarding_sync_description',
    imageAsset: 'assets/onboarding/sync.png',
    backgroundColor: Color(0xFFFF6B6B),
  ),
  OnboardingPage(
    titleKey: 'onboarding_notifications_title',
    descriptionKey: 'onboarding_notifications_description',
    imageAsset: 'assets/onboarding/notifications.png',
    backgroundColor: Color(0xFFFFB347),
    showSkip: false, // Don't skip permission explanation
  ),
  OnboardingPage(
    titleKey: 'onboarding_ready_title',
    descriptionKey: 'onboarding_ready_description',
    imageAsset: 'assets/onboarding/ready.png',
    backgroundColor: Color(0xFF4ECDC4),
    showSkip: false,
  ),
];

Localized Onboarding Screen

Complete Onboarding Widget

class LocalizedOnboarding extends StatefulWidget {
  final VoidCallback onComplete;

  const LocalizedOnboarding({required this.onComplete});

  @override
  State<LocalizedOnboarding> createState() => _LocalizedOnboardingState();
}

class _LocalizedOnboardingState extends State<LocalizedOnboarding> {
  final _pageController = PageController();
  int _currentPage = 0;

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

    return Scaffold(
      body: Stack(
        children: [
          // Page view
          PageView.builder(
            controller: _pageController,
            onPageChanged: (index) => setState(() => _currentPage = index),
            itemCount: onboardingPages.length,
            reverse: isRtl, // Reverse for RTL
            itemBuilder: (context, index) {
              return _buildPage(onboardingPages[index], l10n, locale);
            },
          ),

          // Skip button
          if (onboardingPages[_currentPage].showSkip)
            Positioned(
              top: MediaQuery.of(context).padding.top + 16,
              right: isRtl ? null : 16,
              left: isRtl ? 16 : null,
              child: TextButton(
                onPressed: _skipOnboarding,
                child: Text(
                  l10n.skip,
                  style: TextStyle(color: Colors.white70),
                ),
              ),
            ),

          // Bottom controls
          Positioned(
            bottom: 0,
            left: 0,
            right: 0,
            child: SafeArea(
              child: Padding(
                padding: EdgeInsets.all(24),
                child: _buildBottomControls(l10n, isRtl),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildPage(OnboardingPage page, AppLocalizations l10n, String locale) {
    return Container(
      color: page.backgroundColor ?? Theme.of(context).primaryColor,
      child: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(24),
          child: Column(
            children: [
              Spacer(),

              // Image
              Semantics(
                label: l10n.illustrationFor(page.getTitle(l10n)),
                child: Image.asset(
                  page.getImageForLocale(locale),
                  height: 280,
                  fit: BoxFit.contain,
                ),
              ),
              SizedBox(height: 48),

              // Title
              Text(
                page.getTitle(l10n),
                style: TextStyle(
                  fontSize: 28,
                  fontWeight: FontWeight.bold,
                  color: Colors.white,
                ),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 16),

              // Description
              Text(
                page.getDescription(l10n),
                style: TextStyle(
                  fontSize: 16,
                  color: Colors.white70,
                  height: 1.5,
                ),
                textAlign: TextAlign.center,
              ),

              Spacer(flex: 2),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildBottomControls(AppLocalizations l10n, bool isRtl) {
    final isLastPage = _currentPage == onboardingPages.length - 1;

    return Row(
      children: [
        // Page indicators
        Expanded(
          child: Row(
            mainAxisAlignment: isRtl
                ? MainAxisAlignment.end
                : MainAxisAlignment.start,
            children: List.generate(onboardingPages.length, (index) {
              return AnimatedContainer(
                duration: Duration(milliseconds: 300),
                margin: EdgeInsets.symmetric(horizontal: 4),
                width: _currentPage == index ? 24 : 8,
                height: 8,
                decoration: BoxDecoration(
                  color: _currentPage == index
                      ? Colors.white
                      : Colors.white38,
                  borderRadius: BorderRadius.circular(4),
                ),
              );
            }),
          ),
        ),

        // Next/Get Started button
        ElevatedButton(
          onPressed: isLastPage ? widget.onComplete : _nextPage,
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.white,
            foregroundColor: onboardingPages[_currentPage].backgroundColor,
            padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(30),
            ),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                isLastPage ? l10n.getStarted : l10n.next,
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              if (!isLastPage) ...[
                SizedBox(width: 8),
                Icon(
                  isRtl ? Icons.arrow_back : Icons.arrow_forward,
                  size: 18,
                ),
              ],
            ],
          ),
        ),
      ],
    );
  }

  void _nextPage() {
    _pageController.nextPage(
      duration: Duration(milliseconds: 300),
      curve: Curves.easeInOut,
    );
  }

  void _skipOnboarding() {
    // Track that user skipped
    Analytics.track('onboarding_skipped', {
      'page': _currentPage,
    });
    widget.onComplete();
  }
}

Permission Request Screens

Localized Permission Explanation

class LocalizedPermissionRequest extends StatelessWidget {
  final PermissionType permission;
  final VoidCallback onAllow;
  final VoidCallback onDeny;

  const LocalizedPermissionRequest({
    required this.permission,
    required this.onAllow,
    required this.onDeny,
  });

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

    return Container(
      padding: EdgeInsets.all(24),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // Icon
          Container(
            width: 120,
            height: 120,
            decoration: BoxDecoration(
              color: _getPermissionColor().withOpacity(0.1),
              shape: BoxShape.circle,
            ),
            child: Icon(
              _getPermissionIcon(),
              size: 60,
              color: _getPermissionColor(),
            ),
          ),
          SizedBox(height: 32),

          // Title
          Text(
            _getTitle(l10n),
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
            textAlign: TextAlign.center,
          ),
          SizedBox(height: 16),

          // Explanation
          Text(
            _getExplanation(l10n),
            style: TextStyle(
              fontSize: 16,
              color: Colors.grey[600],
              height: 1.5,
            ),
            textAlign: TextAlign.center,
          ),
          SizedBox(height: 16),

          // Benefits list
          ..._getBenefits(l10n).map((benefit) => _buildBenefitRow(benefit)),
          SizedBox(height: 32),

          // Buttons
          SizedBox(
            width: double.infinity,
            child: ElevatedButton(
              onPressed: onAllow,
              child: Text(_getAllowButtonText(l10n)),
              style: ElevatedButton.styleFrom(
                padding: EdgeInsets.symmetric(vertical: 16),
              ),
            ),
          ),
          SizedBox(height: 12),
          TextButton(
            onPressed: onDeny,
            child: Text(l10n.notNow),
          ),
        ],
      ),
    );
  }

  String _getTitle(AppLocalizations l10n) {
    switch (permission) {
      case PermissionType.notifications:
        return l10n.permissionNotificationsTitle;
      case PermissionType.location:
        return l10n.permissionLocationTitle;
      case PermissionType.camera:
        return l10n.permissionCameraTitle;
      case PermissionType.photos:
        return l10n.permissionPhotosTitle;
      case PermissionType.microphone:
        return l10n.permissionMicrophoneTitle;
      case PermissionType.contacts:
        return l10n.permissionContactsTitle;
    }
  }

  String _getExplanation(AppLocalizations l10n) {
    switch (permission) {
      case PermissionType.notifications:
        return l10n.permissionNotificationsExplanation;
      case PermissionType.location:
        return l10n.permissionLocationExplanation;
      case PermissionType.camera:
        return l10n.permissionCameraExplanation;
      case PermissionType.photos:
        return l10n.permissionPhotosExplanation;
      case PermissionType.microphone:
        return l10n.permissionMicrophoneExplanation;
      case PermissionType.contacts:
        return l10n.permissionContactsExplanation;
    }
  }

  List<String> _getBenefits(AppLocalizations l10n) {
    switch (permission) {
      case PermissionType.notifications:
        return [
          l10n.benefitNotifications1,
          l10n.benefitNotifications2,
          l10n.benefitNotifications3,
        ];
      case PermissionType.location:
        return [
          l10n.benefitLocation1,
          l10n.benefitLocation2,
          l10n.benefitLocation3,
        ];
      default:
        return [];
    }
  }

  String _getAllowButtonText(AppLocalizations l10n) {
    switch (permission) {
      case PermissionType.notifications:
        return l10n.enableNotifications;
      case PermissionType.location:
        return l10n.allowLocation;
      case PermissionType.camera:
        return l10n.allowCamera;
      case PermissionType.photos:
        return l10n.allowPhotos;
      case PermissionType.microphone:
        return l10n.allowMicrophone;
      case PermissionType.contacts:
        return l10n.allowContacts;
    }
  }

  Widget _buildBenefitRow(String benefit) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          Icon(Icons.check_circle, color: Colors.green, size: 20),
          SizedBox(width: 12),
          Expanded(child: Text(benefit)),
        ],
      ),
    );
  }

  IconData _getPermissionIcon() {
    switch (permission) {
      case PermissionType.notifications:
        return Icons.notifications_active;
      case PermissionType.location:
        return Icons.location_on;
      case PermissionType.camera:
        return Icons.camera_alt;
      case PermissionType.photos:
        return Icons.photo_library;
      case PermissionType.microphone:
        return Icons.mic;
      case PermissionType.contacts:
        return Icons.contacts;
    }
  }

  Color _getPermissionColor() {
    switch (permission) {
      case PermissionType.notifications:
        return Colors.orange;
      case PermissionType.location:
        return Colors.blue;
      case PermissionType.camera:
        return Colors.purple;
      case PermissionType.photos:
        return Colors.green;
      case PermissionType.microphone:
        return Colors.red;
      case PermissionType.contacts:
        return Colors.teal;
    }
  }
}

enum PermissionType {
  notifications,
  location,
  camera,
  photos,
  microphone,
  contacts,
}

Feature Highlights

Animated Feature Showcase

class LocalizedFeatureHighlight extends StatelessWidget {
  final List<Feature> features;

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

    return ListView.builder(
      padding: EdgeInsets.all(24),
      itemCount: features.length,
      itemBuilder: (context, index) {
        return _buildFeatureCard(features[index], l10n, index);
      },
    );
  }

  Widget _buildFeatureCard(Feature feature, AppLocalizations l10n, int index) {
    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: 1),
      duration: Duration(milliseconds: 500 + (index * 100)),
      builder: (context, value, child) {
        return Opacity(
          opacity: value,
          child: Transform.translate(
            offset: Offset(0, 20 * (1 - value)),
            child: child,
          ),
        );
      },
      child: Card(
        margin: EdgeInsets.only(bottom: 16),
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Row(
            children: [
              Container(
                width: 60,
                height: 60,
                decoration: BoxDecoration(
                  color: feature.color.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Icon(
                  feature.icon,
                  color: feature.color,
                  size: 30,
                ),
              ),
              SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      feature.getTitle(l10n),
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                    ),
                    SizedBox(height: 4),
                    Text(
                      feature.getDescription(l10n),
                      style: TextStyle(
                        color: Colors.grey[600],
                        fontSize: 14,
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class Feature {
  final String titleKey;
  final String descriptionKey;
  final IconData icon;
  final Color color;

  const Feature({
    required this.titleKey,
    required this.descriptionKey,
    required this.icon,
    required this.color,
  });

  String getTitle(AppLocalizations l10n) {
    final titles = {
      'feature_sync': l10n.featureSync,
      'feature_offline': l10n.featureOffline,
      'feature_share': l10n.featureShare,
      'feature_secure': l10n.featureSecure,
    };
    return titles[titleKey] ?? titleKey;
  }

  String getDescription(AppLocalizations l10n) {
    final descriptions = {
      'feature_sync_desc': l10n.featureSyncDesc,
      'feature_offline_desc': l10n.featureOfflineDesc,
      'feature_share_desc': l10n.featureShareDesc,
      'feature_secure_desc': l10n.featureSecureDesc,
    };
    return descriptions[descriptionKey] ?? descriptionKey;
  }
}

ARB File Structure

{
  "@@locale": "en",

  "skip": "Skip",
  "next": "Next",
  "back": "Back",
  "getStarted": "Get Started",
  "done": "Done",
  "notNow": "Not Now",

  "onboardingWelcomeTitle": "Welcome to AppName",
  "onboardingWelcomeDescription": "Your personal assistant for managing tasks and staying productive.",

  "onboardingFeaturesTitle": "Powerful Features",
  "onboardingFeaturesDescription": "Everything you need to organize your work and life in one place.",

  "onboardingSyncTitle": "Sync Everywhere",
  "onboardingSyncDescription": "Access your data on any device. Changes sync instantly across all your devices.",

  "onboardingNotificationsTitle": "Stay Updated",
  "onboardingNotificationsDescription": "Get timely reminders and never miss important tasks or deadlines.",

  "onboardingReadyTitle": "You're All Set!",
  "onboardingReadyDescription": "Start organizing your life today. We're here to help you succeed.",

  "illustrationFor": "Illustration for {title}",
  "@illustrationFor": {
    "placeholders": {"title": {"type": "String"}}
  },

  "permissionNotificationsTitle": "Enable Notifications",
  "permissionNotificationsExplanation": "Get reminded about important tasks and updates. You can customize notification preferences anytime.",
  "benefitNotifications1": "Never miss a deadline",
  "benefitNotifications2": "Get updates on shared tasks",
  "benefitNotifications3": "Receive daily summaries",
  "enableNotifications": "Enable Notifications",

  "permissionLocationTitle": "Enable Location",
  "permissionLocationExplanation": "Help us show relevant content and enable location-based reminders.",
  "benefitLocation1": "Location-based reminders",
  "benefitLocation2": "Find nearby places",
  "benefitLocation3": "Weather-aware suggestions",
  "allowLocation": "Allow Location Access",

  "permissionCameraTitle": "Camera Access",
  "permissionCameraExplanation": "Take photos of documents, receipts, or notes to add to your tasks.",
  "allowCamera": "Allow Camera",

  "permissionPhotosTitle": "Photo Library Access",
  "permissionPhotosExplanation": "Attach photos from your library to tasks and notes.",
  "allowPhotos": "Allow Photos",

  "permissionMicrophoneTitle": "Microphone Access",
  "permissionMicrophoneExplanation": "Create voice notes and hands-free task entry.",
  "allowMicrophone": "Allow Microphone",

  "permissionContactsTitle": "Contacts Access",
  "permissionContactsExplanation": "Easily share tasks and collaborate with your contacts.",
  "allowContacts": "Allow Contacts",

  "featureSync": "Cloud Sync",
  "featureSyncDesc": "Your data syncs automatically across all devices",
  "featureOffline": "Offline Mode",
  "featureOfflineDesc": "Works without internet, syncs when connected",
  "featureShare": "Easy Sharing",
  "featureShareDesc": "Share tasks and lists with friends and family",
  "featureSecure": "Bank-Level Security",
  "featureSecureDesc": "Your data is encrypted and protected"
}

Conclusion

Onboarding localization requires:

  1. Culturally appropriate visuals per locale
  2. Clear permission explanations in native language
  3. RTL support for bidirectional layouts
  4. Benefit-focused copy that resonates locally
  5. Skip and progress options properly labeled

With proper localization, your onboarding will convert users worldwide into engaged customers.

Related Resources