Flutter: التدويل والتوطين
تطبيق واحد للعالم — التدويل يجعل كودك يتحدث لغة المستخدم ويستخدم عملته.
📋 المتطلبات السابقة: يجب أن تكون ملمًا بـ
- الدرس 16: نظام التنسيق والأنماط
1. ما ستتعلمه
- flutter_localizations + حزمة intl: سير عمل ملفات ARB وتوليد الكود
- إعدادات localizationsDelegates و supportedLocales
- التبديل الديناميكي للغة: تبديل اللغة وقت التشغيل (مُدار بـ Riverpod)
- تنسيق ICU: الأرقام (فواصل الآلاف)، العملات (USD/EUR/CNY)، التواريخ، الجمع
- ShopApp: ثلاث لغات صينية/إنجليزية/يابانية + ثلاث عملات USD/CNY/JPY
2. قصة حقيقية عن فوضى التوسع العالمي
(1) المشكلة: القيم المشفرة ثابتة تربك المستخدمين العالميين
تطبيق ShopApp لبوب يدعم الإنجليزية والدولار فقط. أليس (مستخدمة صينية) ترى "$9,999.00" وتظنها 9999 دولار أمريكي، بينما يجب أن تكون 9999 يوان صيني. تشارلي (مستخدم ياباني) يرى "1,500" ولا يستطيع التمييز. الأسوأ أن وصف المنتجات كله بالإنجليزية، ومعدل ارتداد المستخدمين غير الناطقين بالإنجليزية 70%.
(2) حل ARB + تنسيق ICU
نظام l10n في Flutter يستخدم ملفات ARB لإدارة الترجمات، وتنسيق ICU يتعامل تلقائيًا مع الاختلافات الإقليمية في الأرقام/العملات/التواريخ.
import 'package:intl/intl.dart';
// ⚙️ تثبيت التبعيات: flutter pub add intl
// Locale-aware formatting
final price = NumberFormat.simpleCurrency(locale: 'zh_CN').format(9999);
// → ¥9,999.00 (CNY)
final price2 = NumberFormat.simpleCurrency(locale: 'en_US').format(1299.99);
// → $1,299.99 (USD)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
(3) الفائدة: انخفاض معدل الارتداد من 70% إلى 15%
بعد تطبيق ثلاث لغات + ثلاث عملات، انخفض معدل ارتداد المناطق غير الإنجليزية من 70% إلى 15%، وشهد السوق الياباني نموًا ثلاثيًا في الطلبات.
3. تدفق بيانات i18n
graph TD
ARB[ARB Files] --> |l10n| GEN[Generated Dart]
GEN --> MAT[MaterialApp.localizationsDelegates]
MAT --> L10N[AppLocalizations]
L10N --> EN[English: $1,299.99]
L10N --> ZH[中文: ¥9,999.00]
L10N --> JA[日本語: ¥150,000]
subgraph Users
Alice2[Alice: EN / USD]
Bob2[Bob: ZH / CNY]
Charlie[Charlie: JA / JPY]
end
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
(1) مصطلحات التدويل الرئيسية
| المصطلح | الوصف | مثال |
|---|---|---|
| i18n | التدويل (18 حرفًا محذوفًا) | دعم الإطار |
| l10n | التوطين (10 حروف محذوفة) | ترجمات محددة |
| Locale | مُعرِّف اللغة + المنطقة | en_US, zh_CN, ja_JP |
| ARB | حزمة موارد التطبيق | تنسيق ملف الترجمة |
| ICU | مكونات التدويل لـ Unicode | معيار التنسيق |
4. سير عمل ملفات ARB
(1) إعدادات المشروع
# l10n.yaml (project root)
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: S
nullable-getter: false
# pubspec.yaml
flutter:
generate: true
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: ملفات ترجمة ARB
// lib/l10n/app_en.arb (template)
{
"appTitle": "ShopApp",
"productCount": "{count, plural, =0{No products} =1{1 product} other{{count} products}}",
"priceWithCurrency": "{price, select, USD{${price}} CNY{¥{price}} JPY{¥{price}}}",
"welcomeMessage": "Welcome, {name}!",
"lastUpdated": "Last updated: {date}",
"@productCount": {
"placeholders": { "count": { "type": "int" } }
},
"@priceWithCurrency": {
"placeholders": { "price": { "type": "String" }, "currency": { "type": "String" } }
},
"@welcomeMessage": {
"placeholders": { "name": { "type": "String" } }
},
"@lastUpdated": {
"placeholders": { "date": { "type": "DateTime" } }
}
}
// lib/l10n/app_zh.arb
{
"appTitle": "ShopApp",
"productCount": "{count, plural, =0{没有商品} other{{count} 件商品}}",
"priceWithCurrency": "{price, select, USD{\\${price}} CNY{¥{price}} JPY{¥{price}}}",
"welcomeMessage": "欢迎,{name}!",
"lastUpdated": "最后更新:{date}"
}
// lib/l10n/app_ja.arb
{
"appTitle": "ShopApp",
"productCount": "{count, plural, other{{count}件の商品}}",
"priceWithCurrency": "{price, select, USD{\\${price}} CNY{¥{price}} JPY{¥{price}}}",
"welcomeMessage": "ようこそ、{name}さん!",
"lastUpdated": "最終更新: {date}"
}
5. إعدادات MaterialApp
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: إعدادات l10n
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ تثبيت التبعيات: flutter pub add flutter_riverpod
// ⚙️ إعدادات l10n: أنشئ l10n.yaml في جذر المشروع، أضف flutter: generate: true في pubspec.yaml
// Custom class definition source:
// - localeProvider: see Section 7 LocaleNotifier in this lesson
class ShopApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeProvider);
return MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'ShopApp',
home: const HomePage(),
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: استخدام النصوص المترجمة
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// ⚙️ إعدادات l10n: أنشئ l10n.yaml في جذر المشروع، شغّل flutter gen-l10n لتوليد الكود
// In any widget
final s = AppLocalizations.of(context)!;
Text(s.appTitle) // ShopApp
Text(s.productCount(42)) // 42 products / 42 件商品
Text(s.welcomeMessage('Alice')) // Welcome, Alice! / 欢迎,Alice!
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
6. تنسيق ICU
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تنسيق العملات
import 'package:intl/intl.dart';
import 'package:intl/number_symbols_data.dart';
// ⚙️ تثبيت التبعيات: flutter pub add intl
class CurrencyFormatter {
static String format(double amount, {String locale = 'en_US', String? currency}) {
final format = NumberFormat.simpleCurrency(locale: locale, name: currency);
return format.format(amount);
}
}
// Usage
CurrencyFormatter.format(1299.99, locale: 'en_US') // $1,299.99
CurrencyFormatter.format(9999.00, locale: 'zh_CN') // ¥9,999.00
CurrencyFormatter.format(150000, locale: 'ja_JP') // ¥150,000
CurrencyFormatter.format(99.99, locale: 'de_DE', currency: 'EUR') // 99,99 €
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
| Locale | تنسيق الأرقام | تنسيق العملات | تنسيق التاريخ |
|---|---|---|---|
| en_US | 1,299.99 | $1,299.99 | 07/13/2026 |
| zh_CN | 1,299.99 | ¥9,999.00 | 2026/07/13 |
| ja_JP | 1,299.99 | ¥150,000 | 2026/07/13 |
| de_DE | 1.299,99 | 1.299,99 € | 13.07.2026 |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تنسيق التواريخ
import 'package:intl/intl.dart';
// ⚙️ تثبيت التبعيات: flutter pub add intl
String formatDate(DateTime date, {String locale = 'en_US'}) {
return DateFormat.yMMMd(locale).format(date);
}
// en_US: Jul 13, 2026
// zh_CN: 2026年7月13日
// ja_JP: 2026年7月13日
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تنسيق الجمع
// In ARB file
"cartItemCount": "{count, plural, =0{Your cart is empty} =1{1 item in cart} other{{count} items in cart}}"
// Usage
Text(s.cartItemCount(0)) // Your cart is empty
Text(s.cartItemCount(1)) // 1 item in cart
Text(s.cartItemCount(5)) // 5 items in cart
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
7. التبديل الديناميكي للغة
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: إدارة اللغة بـ Riverpod
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';
// ⚙️ تثبيت التبعيات: flutter pub add flutter_riverpod riverpod_annotation shared_preferences intl
// ⚙️ تبعيات تطوير: flutter pub add --dev riverpod_generator build_runner
// Custom class definition source:
// - localeNotifierProvider/currencyProvider: see definitions below
@riverpod
class LocaleNotifier extends _$LocaleNotifier {
@override
Locale build() {
// Load saved preference
_loadSavedLocale();
return const Locale('en');
}
Future<void> _loadSavedLocale() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString('locale');
if (saved != null) {
state = Locale(saved);
}
}
Future<void> setLocale(Locale locale) async {
state = locale;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('locale', locale.languageCode);
}
}
// Settings page
class LanguageSettingsPage extends ConsumerWidget {
static const _locales = [
(Locale('en'), 'English', '🇺🇸'),
(Locale('zh'), '中文', '🇨🇳'),
(Locale('ja'), '日本語', '🇯🇵'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final current = ref.watch(localeNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text('Language')),
body: ListView(children: _locales.map((item) {
final (locale, name, flag) = item;
return ListTile(
leading: Text(flag, style: const TextStyle(fontSize: 24)),
title: Text(name),
trailing: current == locale ? const Icon(Icons.check, color: Colors.green) : null,
onTap: () => ref.read(localeNotifierProvider.notifier).setLocale(locale),
);
}).toList()),
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
8. مثال كامل: ويدجت السعر المُوطَّن لـ ShopApp
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';
// ⚙️ تثبيت التبعيات: flutter pub add flutter_riverpod riverpod_annotation shared_preferences intl
// ⚙️ تبعيات تطوير: flutter pub add --dev riverpod_generator build_runner
// Custom class definition source:
// - localeNotifierProvider: see Section 7 LocaleNotifier in this lesson
// - currencyProvider: see CurrencyNotifier below
class LocalizedPrice extends ConsumerWidget {
final double amount;
final TextStyle? style;
const LocalizedPrice({super.key, required this.amount, this.style});
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeNotifierProvider);
final currency = ref.watch(currencyProvider);
final formatted = _formatPrice(amount, locale.languageCode, currency);
return Text(formatted, style: style ?? const TextStyle(fontSize: 18, fontWeight: FontWeight.bold));
}
String _formatPrice(double amount, String languageCode, String currencyCode) {
final locale = switch (languageCode) {
'zh' => 'zh_CN',
'ja' => 'ja_JP',
_ => 'en_US',
};
return NumberFormat.simpleCurrency(locale: locale, name: currencyCode).format(amount);
}
}
// Currency provider
@riverpod
class CurrencyNotifier extends _$CurrencyNotifier {
@override
String build() {
_loadSaved();
return 'USD';
}
Future<void> _loadSaved() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString('currency');
if (saved != null) state = saved;
}
Future<void> setCurrency(String code) async {
state = code;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('currency', code);
}
}
// Usage in product card
LocalizedPrice(amount: product.price) // Auto-formats based on user's locale & currency
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
❓ أسئلة شائعة
flutter gen-l10n أو ببساطة flutter run (الذي يُطلق التوليد تلقائيًا).locale: Locale('ar')). تأكد من استخدام Directionality و start/end بدلاً من left/right.📖 ملخص
- ملفات ARB تُدير الترجمات؛ flutter gen-l10n يُولد تلقائيًا كود Dart آمن النوع
- MaterialApp يُهيئ localizationsDelegates + supportedLocales
- تنسيق ICU يتعامل تلقائيًا مع اختلافات اللغات في الأرقام/العملات/التواريخ/الجمع
- Riverpod + SharedPreferences يُديران تفضيلات اللغة والعملة
- NumberFormat.simpleCurrency يُمكّن تنسيق العملات المتعددة
📝 تمارين
- أساسي (الصعوبة ⭐): أعد l10n.yaml وملفات ARB، نفذ تبديل ثنائي اللغة صينية/إنجليزية مع 10 نصوص مترجمة على الأقل.
- متوسط (الصعوبة ⭐⭐): أضف دعم اليابانية، نفذ تنسيق ICU للجمع (عدد عناصر السلة)، وتكييف تلقائي لفواصل الآلاف.
- متقدم (الصعوبة ⭐⭐⭐): نفذ نظام متعدد اللغات كامل لـ ShopApp: ثلاث لغات صينية/إنجليزية/يابانية + ثلاث عملات USD/CNY/JPY + توطين تنسيق التاريخ + صفحة إعدادات التبديل + استمرار SharedPreferences.