Flutter: التدويل والتوطين

تطبيق واحد للعالم — التدويل يجعل كودك يتحدث لغة المستخدم ويستخدم عملته.

📋 المتطلبات السابقة: يجب أن تكون ملمًا بـ

1. ما ستتعلمه


2. قصة حقيقية عن فوضى التوسع العالمي

(1) المشكلة: القيم المشفرة ثابتة تربك المستخدمين العالميين

تطبيق ShopApp لبوب يدعم الإنجليزية والدولار فقط. أليس (مستخدمة صينية) ترى "$9,999.00" وتظنها 9999 دولار أمريكي، بينما يجب أن تكون 9999 يوان صيني. تشارلي (مستخدم ياباني) يرى "1,500" ولا يستطيع التمييز. الأسوأ أن وصف المنتجات كله بالإنجليزية، ومعدل ارتداد المستخدمين غير الناطقين بالإنجليزية 70%.

(2) حل ARB + تنسيق ICU

نظام l10n في Flutter يستخدم ملفات ARB لإدارة الترجمات، وتنسيق ICU يتعامل تلقائيًا مع الاختلافات الإقليمية في الأرقام/العملات/التواريخ.

DART
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)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(3) الفائدة: انخفاض معدل الارتداد من 70% إلى 15%

بعد تطبيق ثلاث لغات + ثلاث عملات، انخفض معدل ارتداد المناطق غير الإنجليزية من 70% إلى 15%، وشهد السوق الياباني نموًا ثلاثيًا في الطلبات.


3. تدفق بيانات i18n

100%
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
TEXT
> الإخراج: شغّل محليًا باستخدام 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) إعدادات المشروع

YAML
# 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
YAML
# pubspec.yaml
flutter:
  generate: true

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: ملفات ترجمة ARB

JSON
// 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" } }
  }
}
JSON
// 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}"
}
JSON
// 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

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: إعدادات l10n

DART
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(),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: استخدام النصوص المترجمة

DART
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!
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

6. تنسيق ICU

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تنسيق العملات

DART
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 €
TEXT
> الإخراج: شغّل محليًا باستخدام 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

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تنسيق التواريخ

DART
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日
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تنسيق الجمع

DART
// 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
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

7. التبديل الديناميكي للغة

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: إدارة اللغة بـ Riverpod

DART
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()),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

8. مثال كامل: ويدجت السعر المُوطَّن لـ ShopApp

DART
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
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

❓ أسئلة شائعة

س كيف تصبح تغييرات ملف ARB سارية المفعول؟
ج شغّل flutter gen-l10n أو ببساطة flutter run (الذي يُطلق التوليد تلقائيًا).
س هل إعادة التحميل السريع تدعم تبديل اللغة؟
ج جزئيًا. تغييرات محتوى ARB تحتاج إعادة تشغيل سريعة (R)، لكن تبديل Locale يدعم إعادة التحميل السريع (r).
س هل أستخدم intl أم الدمج اليدوي لتنسيق العملات؟
ج استخدم NumberFormat.simpleCurrency من intl. الدمج اليدوي لا يستطيع التعامل مع اختلافات فواصل الآلاف والفاصلة العشرية بين اللغات.
س كيف أنسق "وان" الصينية؟
ج معيار ICU لا يحتوي وحدة "وان" — الأرقام الصينية تستخدم فاصلة الآلاف (9,999 وليس 0.9999). التنسيق المخصص مطلوب لعرض "وان".
س كيف أدعم لغات RTL (العربية)؟
ج Flutter يتعامل تلقائيًا مع تخطيط RTL (اضبط locale: Locale('ar')). تأكد من استخدام Directionality و start/end بدلاً من left/right.
س من يُ维护 ملفات الترجمة؟
ج المطورون يُحافظون على قالب ARB الإنجليزي؛ فرق الترجمة أو الذكاء الاصطناعي يترجمون ملفات ARB بلغات أخرى. يُنصح باستخدام منصات إدارة الترجمة مثل Lokalise/Crowdin.

📖 ملخص


📝 تمارين

  1. أساسي (الصعوبة ⭐): أعد l10n.yaml وملفات ARB، نفذ تبديل ثنائي اللغة صينية/إنجليزية مع 10 نصوص مترجمة على الأقل.
  2. متوسط (الصعوبة ⭐⭐): أضف دعم اليابانية، نفذ تنسيق ICU للجمع (عدد عناصر السلة)، وتكييف تلقائي لفواصل الآلاف.
  3. متقدم (الصعوبة ⭐⭐⭐): نفذ نظام متعدد اللغات كامل لـ ShopApp: ثلاث لغات صينية/إنجليزية/يابانية + ثلاث عملات USD/CNY/JPY + توطين تنسيق التاريخ + صفحة إعدادات التبديل + استمرار SharedPreferences.

← الدرس السابق | الدرس التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%