Flutter PageView Localization: Onboarding, Tutorials, and Carousel Content
PageView is essential for onboarding flows, image carousels, tutorials, and swipeable content. Localizing PageView requires handling page indicators, navigation buttons, swipe instructions, and content across different languages. This guide covers everything you need to know about localizing PageView in Flutter.
Understanding PageView Localization
PageView requires localization for:
- Onboarding content: Titles, descriptions, and images
- Page indicators: Current page and total count
- Navigation buttons: Next, Skip, Get Started
- Carousel captions: Image descriptions and alt text
- Swipe instructions: Accessibility hints
- RTL support: Proper swipe direction for Arabic, Hebrew
Basic Onboarding PageView
Start with a localized onboarding flow:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedOnboarding extends StatefulWidget {
final VoidCallback onComplete;
const LocalizedOnboarding({
super.key,
required this.onComplete,
});
@override
State<LocalizedOnboarding> createState() => _LocalizedOnboardingState();
}
class _LocalizedOnboardingState extends State<LocalizedOnboarding> {
final PageController _pageController = PageController();
int _currentPage = 0;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final pages = [
OnboardingPage(
icon: Icons.translate,
title: l10n.onboardingTitle1,
description: l10n.onboardingDescription1,
color: Colors.blue,
),
OnboardingPage(
icon: Icons.speed,
title: l10n.onboardingTitle2,
description: l10n.onboardingDescription2,
color: Colors.green,
),
OnboardingPage(
icon: Icons.security,
title: l10n.onboardingTitle3,
description: l10n.onboardingDescription3,
color: Colors.purple,
),
];
return Scaffold(
body: SafeArea(
child: Column(
children: [
// Skip button
Align(
alignment: AlignmentDirectional.topEnd,
child: Padding(
padding: const EdgeInsets.all(16),
child: TextButton(
onPressed: widget.onComplete,
child: Text(l10n.skipButton),
),
),
),
// Page content
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: pages.length,
onPageChanged: (index) {
setState(() => _currentPage = index);
},
itemBuilder: (context, index) {
final page = pages[index];
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
page.icon,
size: 120,
color: page.color,
),
const SizedBox(height: 32),
Text(
page.title,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
page.description,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
);
},
),
),
// Page indicator
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
pages.length,
(index) => AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: _currentPage == index ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: _currentPage == index
? Theme.of(context).primaryColor
: Colors.grey[300],
borderRadius: BorderRadius.circular(4),
),
),
),
),
),
// Navigation buttons
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
if (_currentPage > 0)
TextButton(
onPressed: () {
_pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: Text(l10n.previousButton),
)
else
const SizedBox(width: 80),
const Spacer(),
Text(
l10n.pageIndicator(_currentPage + 1, pages.length),
style: TextStyle(color: Colors.grey[600]),
),
const Spacer(),
_currentPage < pages.length - 1
? ElevatedButton(
onPressed: () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: Text(l10n.nextButton),
)
: ElevatedButton(
onPressed: widget.onComplete,
child: Text(l10n.getStartedButton),
),
],
),
),
],
),
),
);
}
}
class OnboardingPage {
final IconData icon;
final String title;
final String description;
final Color color;
OnboardingPage({
required this.icon,
required this.title,
required this.description,
required this.color,
});
}
Image Carousel with Localized Captions
Create a carousel with localized image descriptions:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedImageCarousel extends StatefulWidget {
final List<CarouselItem> items;
const LocalizedImageCarousel({
super.key,
required this.items,
});
@override
State<LocalizedImageCarousel> createState() => _LocalizedImageCarouselState();
}
class _LocalizedImageCarouselState extends State<LocalizedImageCarousel> {
final PageController _pageController = PageController();
int _currentPage = 0;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
// Carousel
SizedBox(
height: 300,
child: PageView.builder(
controller: _pageController,
itemCount: widget.items.length,
onPageChanged: (index) {
setState(() => _currentPage = index);
},
itemBuilder: (context, index) {
final item = widget.items[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
fit: StackFit.expand,
children: [
// Image placeholder
Container(
color: Colors.grey[300],
child: Center(
child: Icon(
Icons.image,
size: 64,
color: Colors.grey[500],
),
),
),
// Gradient overlay for text
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.transparent,
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
item.description,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
],
),
),
),
],
),
),
);
},
),
),
const SizedBox(height: 16),
// Indicator dots
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: _currentPage > 0
? () {
_pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
: null,
tooltip: l10n.previousImageTooltip,
),
...List.generate(
widget.items.length,
(index) => GestureDetector(
onTap: () {
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentPage == index
? Theme.of(context).primaryColor
: Colors.grey[300],
),
),
),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage < widget.items.length - 1
? () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
: null,
tooltip: l10n.nextImageTooltip,
),
],
),
// Image counter
Text(
l10n.imageCounter(_currentPage + 1, widget.items.length),
style: TextStyle(color: Colors.grey[600]),
),
],
);
}
}
class CarouselItem {
final String imageUrl;
final String title;
final String description;
CarouselItem({
required this.imageUrl,
required this.title,
required this.description,
});
}
// Usage with localized content
class CarouselExample extends StatelessWidget {
const CarouselExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final items = [
CarouselItem(
imageUrl: 'assets/images/feature1.jpg',
title: l10n.carouselTitle1,
description: l10n.carouselDescription1,
),
CarouselItem(
imageUrl: 'assets/images/feature2.jpg',
title: l10n.carouselTitle2,
description: l10n.carouselDescription2,
),
CarouselItem(
imageUrl: 'assets/images/feature3.jpg',
title: l10n.carouselTitle3,
description: l10n.carouselDescription3,
),
];
return LocalizedImageCarousel(items: items);
}
}
Tutorial PageView with Steps
Create a step-by-step tutorial:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedTutorial extends StatefulWidget {
final VoidCallback onComplete;
const LocalizedTutorial({
super.key,
required this.onComplete,
});
@override
State<LocalizedTutorial> createState() => _LocalizedTutorialState();
}
class _LocalizedTutorialState extends State<LocalizedTutorial> {
final PageController _pageController = PageController();
int _currentStep = 0;
bool _stepCompleted = false;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final steps = [
TutorialStep(
number: 1,
title: l10n.tutorialStep1Title,
instruction: l10n.tutorialStep1Instruction,
hint: l10n.tutorialStep1Hint,
icon: Icons.account_circle,
),
TutorialStep(
number: 2,
title: l10n.tutorialStep2Title,
instruction: l10n.tutorialStep2Instruction,
hint: l10n.tutorialStep2Hint,
icon: Icons.settings,
),
TutorialStep(
number: 3,
title: l10n.tutorialStep3Title,
instruction: l10n.tutorialStep3Instruction,
hint: l10n.tutorialStep3Hint,
icon: Icons.notifications,
),
TutorialStep(
number: 4,
title: l10n.tutorialStep4Title,
instruction: l10n.tutorialStep4Instruction,
hint: l10n.tutorialStep4Hint,
icon: Icons.check_circle,
),
];
return Scaffold(
appBar: AppBar(
title: Text(l10n.tutorialTitle),
actions: [
TextButton(
onPressed: widget.onComplete,
child: Text(
l10n.skipTutorialButton,
style: const TextStyle(color: Colors.white),
),
),
],
),
body: Column(
children: [
// Progress indicator
LinearProgressIndicator(
value: (_currentStep + 1) / steps.length,
backgroundColor: Colors.grey[200],
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.stepProgress(_currentStep + 1, steps.length),
style: TextStyle(color: Colors.grey[600]),
),
),
// Tutorial content
Expanded(
child: PageView.builder(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
itemCount: steps.length,
onPageChanged: (index) {
setState(() {
_currentStep = index;
_stepCompleted = false;
});
},
itemBuilder: (context, index) {
final step = steps[index];
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Step icon
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
step.icon,
size: 50,
color: Theme.of(context).primaryColor,
),
),
const SizedBox(height: 24),
// Step number badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
l10n.stepNumber(step.number),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16),
// Title
Text(
step.title,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Instruction
Text(
step.instruction,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Hint card
Card(
color: Colors.amber[50],
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.lightbulb_outline,
color: Colors.amber[800],
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.tipLabel,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.amber[800],
),
),
Text(step.hint),
],
),
),
],
),
),
),
const SizedBox(height: 24),
// Complete step checkbox
CheckboxListTile(
title: Text(l10n.markStepCompleteLabel),
value: _stepCompleted,
onChanged: (value) {
setState(() => _stepCompleted = value ?? false);
},
),
],
),
);
},
),
),
// Navigation
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
child: Row(
children: [
if (_currentStep > 0)
OutlinedButton(
onPressed: () {
_pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: Text(l10n.previousStepButton),
)
else
const SizedBox(width: 100),
const Spacer(),
_currentStep < steps.length - 1
? ElevatedButton(
onPressed: _stepCompleted
? () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
: null,
child: Text(l10n.nextStepButton),
)
: ElevatedButton(
onPressed: _stepCompleted ? widget.onComplete : null,
child: Text(l10n.completeTutorialButton),
),
],
),
),
),
],
),
);
}
}
class TutorialStep {
final int number;
final String title;
final String instruction;
final String hint;
final IconData icon;
TutorialStep({
required this.number,
required this.title,
required this.instruction,
required this.hint,
required this.icon,
});
}
RTL Support for PageView
Handle right-to-left swipe direction:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class RTLPageView extends StatefulWidget {
const RTLPageView({super.key});
@override
State<RTLPageView> createState() => _RTLPageViewState();
}
class _RTLPageViewState extends State<RTLPageView> {
late PageController _pageController;
int _currentPage = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final isRTL = Directionality.of(context) == TextDirection.rtl;
// For RTL, PageView automatically reverses direction
// But we need to handle initial page correctly
_pageController = PageController(initialPage: 0);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRTL = Directionality.of(context) == TextDirection.rtl;
final pages = [
PageContent(
title: l10n.pageTitle1,
content: l10n.pageContent1,
color: Colors.blue,
),
PageContent(
title: l10n.pageTitle2,
content: l10n.pageContent2,
color: Colors.green,
),
PageContent(
title: l10n.pageTitle3,
content: l10n.pageContent3,
color: Colors.orange,
),
];
return Scaffold(
appBar: AppBar(
title: Text(l10n.galleryTitle),
),
body: Column(
children: [
// Swipe hint
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isRTL ? Icons.arrow_forward : Icons.arrow_back,
color: Colors.grey[400],
size: 16,
),
const SizedBox(width: 8),
Text(
l10n.swipeToNavigate,
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(width: 8),
Icon(
isRTL ? Icons.arrow_back : Icons.arrow_forward,
color: Colors.grey[400],
size: 16,
),
],
),
),
// PageView
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: pages.length,
onPageChanged: (index) {
setState(() => _currentPage = index);
},
itemBuilder: (context, index) {
final page = pages[index];
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: page.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: page.color.withOpacity(0.3),
),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
page.title,
style: Theme.of(context)
.textTheme
.headlineMedium
?.copyWith(color: page.color),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
page.content,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
),
);
},
),
),
// Navigation with RTL-aware arrows
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(isRTL ? Icons.arrow_forward : Icons.arrow_back),
onPressed: _currentPage > 0
? () {
_pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
: null,
tooltip: l10n.previousPageTooltip,
),
// Page dots
Row(
children: List.generate(
pages.length,
(index) => Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentPage == index
? Theme.of(context).primaryColor
: Colors.grey[300],
),
),
),
),
IconButton(
icon: Icon(isRTL ? Icons.arrow_back : Icons.arrow_forward),
onPressed: _currentPage < pages.length - 1
? () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
: null,
tooltip: l10n.nextPageTooltip,
),
],
),
),
],
),
);
}
}
class PageContent {
final String title;
final String content;
final Color color;
PageContent({
required this.title,
required this.content,
required this.color,
});
}
Accessibility for PageView
Ensure proper screen reader support:
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AccessiblePageView extends StatefulWidget {
const AccessiblePageView({super.key});
@override
State<AccessiblePageView> createState() => _AccessiblePageViewState();
}
class _AccessiblePageViewState extends State<AccessiblePageView> {
final PageController _pageController = PageController();
int _currentPage = 0;
final int _totalPages = 5;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _goToPage(int page) {
if (page >= 0 && page < _totalPages) {
_pageController.animateToPage(
page,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.presentationTitle),
),
body: Column(
children: [
// Accessibility controls
Semantics(
label: l10n.pageNavigationLabel,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Semantics(
button: true,
label: l10n.goToFirstPageLabel,
child: IconButton(
icon: const Icon(Icons.first_page),
onPressed: _currentPage > 0 ? () => _goToPage(0) : null,
tooltip: l10n.firstPageTooltip,
),
),
Semantics(
button: true,
label: l10n.previousPageLabel,
child: IconButton(
icon: const Icon(Icons.chevron_left),
onPressed:
_currentPage > 0 ? () => _goToPage(_currentPage - 1) : null,
tooltip: l10n.previousPageTooltip,
),
),
Semantics(
liveRegion: true,
label: l10n.currentPageAnnouncement(
_currentPage + 1,
_totalPages,
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
l10n.pageIndicator(_currentPage + 1, _totalPages),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
Semantics(
button: true,
label: l10n.nextPageLabel,
child: IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage < _totalPages - 1
? () => _goToPage(_currentPage + 1)
: null,
tooltip: l10n.nextPageTooltip,
),
),
Semantics(
button: true,
label: l10n.goToLastPageLabel,
child: IconButton(
icon: const Icon(Icons.last_page),
onPressed: _currentPage < _totalPages - 1
? () => _goToPage(_totalPages - 1)
: null,
tooltip: l10n.lastPageTooltip,
),
),
],
),
),
),
// PageView with accessibility
Expanded(
child: Semantics(
label: l10n.slideContentLabel,
hint: l10n.slideSwipeHint,
child: PageView.builder(
controller: _pageController,
itemCount: _totalPages,
onPageChanged: (index) {
setState(() => _currentPage = index);
// Announce page change
SemanticsService.announce(
l10n.pageChangedAnnouncement(index + 1, _totalPages),
TextDirection.ltr,
);
},
itemBuilder: (context, index) {
return Semantics(
label: l10n.slideAccessibilityLabel(index + 1),
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
),
],
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.slideTitle(index + 1),
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
Text(
l10n.slideDescription(index + 1),
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
),
),
);
},
),
),
),
// Quick navigation for accessibility
Padding(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 8,
children: List.generate(
_totalPages,
(index) => Semantics(
button: true,
label: l10n.goToSlideLabel(index + 1),
child: ChoiceChip(
label: Text('${index + 1}'),
selected: _currentPage == index,
onSelected: (selected) {
if (selected) _goToPage(index);
},
),
),
),
),
),
],
),
);
}
}
ARB Translations for PageView
Add these entries to your ARB files:
{
"onboardingTitle1": "Easy Translations",
"@onboardingTitle1": {
"description": "First onboarding page title"
},
"onboardingDescription1": "Translate your app into any language with just a few clicks.",
"onboardingTitle2": "Lightning Fast",
"onboardingDescription2": "Our AI-powered translations are ready in seconds, not days.",
"onboardingTitle3": "Secure & Private",
"onboardingDescription3": "Your content is encrypted and never shared with third parties.",
"skipButton": "Skip",
"previousButton": "Previous",
"nextButton": "Next",
"getStartedButton": "Get Started",
"pageIndicator": "{current} of {total}",
"@pageIndicator": {
"placeholders": {
"current": {"type": "int"},
"total": {"type": "int"}
}
},
"previousImageTooltip": "Previous image",
"nextImageTooltip": "Next image",
"imageCounter": "Image {current} of {total}",
"@imageCounter": {
"placeholders": {
"current": {"type": "int"},
"total": {"type": "int"}
}
},
"carouselTitle1": "Featured Product",
"carouselDescription1": "Check out our latest collection",
"carouselTitle2": "New Arrivals",
"carouselDescription2": "Fresh items just added",
"carouselTitle3": "Best Sellers",
"carouselDescription3": "Most popular this week",
"tutorialTitle": "Getting Started",
"skipTutorialButton": "Skip Tutorial",
"stepProgress": "Step {current} of {total}",
"@stepProgress": {
"placeholders": {
"current": {"type": "int"},
"total": {"type": "int"}
}
},
"stepNumber": "Step {number}",
"@stepNumber": {
"placeholders": {
"number": {"type": "int"}
}
},
"tutorialStep1Title": "Create Your Profile",
"tutorialStep1Instruction": "Start by setting up your profile with your name and preferences.",
"tutorialStep1Hint": "You can always update your profile later in Settings.",
"tutorialStep2Title": "Configure Settings",
"tutorialStep2Instruction": "Customize the app to match your workflow and preferences.",
"tutorialStep2Hint": "Enable dark mode for comfortable night-time use.",
"tutorialStep3Title": "Enable Notifications",
"tutorialStep3Instruction": "Stay informed with important updates and reminders.",
"tutorialStep3Hint": "You can customize which notifications you receive.",
"tutorialStep4Title": "You're All Set!",
"tutorialStep4Instruction": "You're ready to start using the app. Explore all features!",
"tutorialStep4Hint": "Check the Help section anytime for more tips.",
"tipLabel": "Tip",
"markStepCompleteLabel": "I've completed this step",
"previousStepButton": "Previous",
"nextStepButton": "Next Step",
"completeTutorialButton": "Finish Tutorial",
"galleryTitle": "Gallery",
"swipeToNavigate": "Swipe to navigate",
"pageTitle1": "Welcome",
"pageContent1": "This is the first page of our gallery.",
"pageTitle2": "Features",
"pageContent2": "Discover all the amazing features available.",
"pageTitle3": "Get Started",
"pageContent3": "Begin your journey with us today.",
"previousPageTooltip": "Previous page",
"nextPageTooltip": "Next page",
"presentationTitle": "Presentation",
"pageNavigationLabel": "Page navigation controls",
"goToFirstPageLabel": "Go to first page",
"firstPageTooltip": "First page",
"previousPageLabel": "Go to previous page",
"nextPageLabel": "Go to next page",
"goToLastPageLabel": "Go to last page",
"lastPageTooltip": "Last page",
"currentPageAnnouncement": "Currently on page {current} of {total}",
"@currentPageAnnouncement": {
"placeholders": {
"current": {"type": "int"},
"total": {"type": "int"}
}
},
"slideContentLabel": "Presentation slides",
"slideSwipeHint": "Swipe left or right to navigate between slides",
"pageChangedAnnouncement": "Now showing page {current} of {total}",
"@pageChangedAnnouncement": {
"placeholders": {
"current": {"type": "int"},
"total": {"type": "int"}
}
},
"slideAccessibilityLabel": "Slide {number}",
"@slideAccessibilityLabel": {
"placeholders": {
"number": {"type": "int"}
}
},
"slideTitle": "Slide {number}",
"@slideTitle": {
"placeholders": {
"number": {"type": "int"}
}
},
"slideDescription": "Content for slide number {number}",
"@slideDescription": {
"placeholders": {
"number": {"type": "int"}
}
},
"goToSlideLabel": "Go to slide {number}",
"@goToSlideLabel": {
"placeholders": {
"number": {"type": "int"}
}
}
}
Testing PageView Localization
Write tests for your localized PageView:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
group('LocalizedOnboarding', () {
testWidgets('displays localized content in English', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: LocalizedOnboarding(onComplete: () {}),
),
);
expect(find.text('Easy Translations'), findsOneWidget);
expect(find.text('Skip'), findsOneWidget);
expect(find.text('Next'), findsOneWidget);
});
testWidgets('displays localized content in Spanish', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('es'),
home: LocalizedOnboarding(onComplete: () {}),
),
);
expect(find.text('Traducciones Fáciles'), findsOneWidget);
expect(find.text('Omitir'), findsOneWidget);
expect(find.text('Siguiente'), findsOneWidget);
});
testWidgets('navigates to next page and updates indicator', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: LocalizedOnboarding(onComplete: () {}),
),
);
// Initial page indicator
expect(find.text('1 of 3'), findsOneWidget);
// Tap next button
await tester.tap(find.text('Next'));
await tester.pumpAndSettle();
// Updated page indicator
expect(find.text('2 of 3'), findsOneWidget);
expect(find.text('Lightning Fast'), findsOneWidget);
});
testWidgets('shows Get Started on last page', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: LocalizedOnboarding(onComplete: () {}),
),
);
// Navigate to last page
await tester.tap(find.text('Next'));
await tester.pumpAndSettle();
await tester.tap(find.text('Next'));
await tester.pumpAndSettle();
// Last page shows Get Started button
expect(find.text('Get Started'), findsOneWidget);
expect(find.text('Secure & Private'), findsOneWidget);
});
testWidgets('handles RTL layout correctly', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('ar'),
home: const Directionality(
textDirection: TextDirection.rtl,
child: RTLPageView(),
),
),
);
// Verify RTL content is displayed
expect(find.text('المعرض'), findsOneWidget);
});
});
}
Summary
Localizing PageView in Flutter requires:
- Onboarding content with localized titles and descriptions
- Page indicators showing current position
- Navigation buttons in the user's language
- Carousel captions for image descriptions
- Tutorial steps with localized instructions
- RTL support with proper swipe direction hints
- Accessibility announcements for page changes
- Comprehensive testing across different locales
PageView is essential for onboarding and content presentation, and proper localization ensures your app makes a great first impression for users in any language.