دليل عملي لتوطين تطبيقات .NET MAUI 10 ودعم RTL بالعربية عبر ملفات .resx و IStringLocalizer و FlowDirection، مع تبديل اللغة أثناء التشغيل، وأمثلة كود جاهزة لـ iOS و Android.
التوطين ودعم RTL في .NET MAUI هو عملية تحويل تطبيقك ليعرض النصوص والتخطيط تلقائيًا بلغات متعددة وفق ثقافة المستخدم، مع عكس واجهة المستخدم كاملةً في اللغات ثنائية الاتجاه مثل العربية والعبرية والفارسية. يوفر .NET MAUI 10 دعمًا أصليًا لهذه العملية عبر ملفات الموارد .resx، وواجهة IStringLocalizer، وخاصية FlowDirection التي تُطبَّق تلقائيًا بناءً على CultureInfo. في هذا الدليل، سأشرح كل ما تحتاجه لبناء تطبيق ثنائي اللغة (عربي/إنجليزي) يعمل على iOS و Android دون قسوة.
يستخدم .NET MAUI ملفات .resx مع IStringLocalizer<T> لإدارة النصوص، ولا تحتاج إلى مكتبات خارجية.
خاصية FlowDirection="MatchParent" على الصفحة الجذر تجعل عكس اليمين لليسار (RTL) تلقائيًا في كل الأبناء.
تغيير اللغة أثناء التشغيل يتطلب تحديث CultureInfo.DefaultThreadCurrentUICulture ثم إعادة إنشاء الصفحة الجذر، وليس إعادة تشغيل التطبيق.
على Android يجب ضبط android:supportsRtl="true"، وعلى iOS تحتاج مجلد ar.lproj مع Localizable.strings.
عكس الأيقونات ذات الاتجاه (مثل الأسهم) يجب أن يتم يدويًا عبر ScaleX="-1" أو نسخ خاصة بـ RTL.
تنسيق الأرقام والتواريخ والعملات يتبع CultureInfo.CurrentCulture، فلا تخلطها مع لغة الواجهة.
لماذا التوطين ودعم RTL أساسيان في تطبيقات 2026
عندما بدأت أعمل على تطبيقات .NET MAUI للسوق العربي قبل أربع سنوات، كانت الشكوى الأولى من المستخدمين دائمًا: "الأزرار في الجهة الخاطئة، وأسماء الأيام مترجَمة نصفها." الحقيقة أن التطبيق الذي لا يحترم ثقافة المستخدم يبدو رخيصًا حتى لو كان يعمل بشكل ممتاز. في 2026، مع نمو سوق التطبيقات العربية بأكثر من 22% سنويًا وفقًا لتقارير App Annie، لم يعد دعم اللغة العربية والـ RTL خيارًا تجميليًا، بل مطلبًا أساسيًا للنشر على المتاجر الإقليمية.
صراحةً، .NET MAUI 10 قدّم تحسينات كبيرة على مستوى الأداء في التطبيقات المُوطّنة، خصوصًا فيما يخص تحميل ملفات الموارد بشكل كسول وتقليل استهلاك الذاكرة عند تعدد اللغات المدمجة. لكن هذه التحسينات لا تحل مشاكل التصميم؛ عليك أن تفكر في اتجاه القراءة، وترتيب الأيقونات، وحتى مواضع الرسوم البيانية. في تطبيقاتي الثلاثة الأخيرة، وجدت أن 40% من الوقت المخصص للتوطين يذهب لإصلاح تفاصيل بصرية دقيقة لا يُنبّهك إليها أي مُترجِم آلي. سنغطي كل هذه التفاصيل في الأقسام التالية، مع أمثلة عملية جاهزة للنسخ. إذا كنت تبني تطبيقًا يعتمد على نمط MVVM مع CommunityToolkit.Mvvm، ستجد أن حقن التوطين يندمج بسلاسة تامة مع ObservableObject.
إعداد ملفات الموارد .resx خطوة بخطوة
ملفات .resx هي الطريقة القياسية في .NET MAUI لتخزين النصوص القابلة للترجمة. تحتوي كل واحدة على أزواج مفتاح/قيمة، ويتم اختيار الملف المناسب تلقائيًا حسب ثقافة المستخدم. الملف الافتراضي (بدون رمز لغة) هو الاحتياطي، ويُستخدَم عندما لا يوجد ترجمة للغة الجهاز.
ابدأ بإنشاء مجلد Resources/Strings في مشروعك، ثم أضف ملفين:
AppResources.resx (النصوص الإنجليزية الاحتياطية).
AppResources.ar.resx (النصوص العربية).
افتح AppResources.resx وأضف مفاتيح مثل WelcomeTitle بقيمة "Welcome"، ثم أضف نفس المفتاح في الملف العربي بقيمة "أهلاً وسهلاً". يقوم Visual Studio تلقائيًا بتوليد كلاس ثابت (AppResources) يمكنك استخدامه في الكود.
<!-- في ملف .csproj، تأكد من أن ملفات .resx تُعامَل كـ EmbeddedResource -->
<ItemGroup>
<EmbeddedResource Update="Resources\Strings\AppResources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>AppResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Strings\AppResources.ar.resx">
<DependentUpon>AppResources.resx</DependentUpon>
</EmbeddedResource>
</ItemGroup>
لاستخدام النصوص في XAML مباشرةً، أضف مساحة الأسماء ثم ارجع إلى الموارد:
هذه الطريقة تعمل مباشرة لكن لها عيب: الروابط ثابتة عند وقت البناء، وتغيير اللغة أثناء التشغيل يتطلب حيلًا إضافية سنناقشها لاحقًا.
استخدام IStringLocalizer مع حقن التبعيات
الأسلوب الحديث الذي أستخدمه في كل مشاريعي الجديدة هو IStringLocalizer<T> من Microsoft.Extensions.Localization. يوفر مزايا مهمة: قابلية الاختبار، دعم كامل لتغيير اللغة أثناء التشغيل، وتكامل مع حقن التبعيات في .NET MAUI 10. أول شيء، أضف الحزمة عبر NuGet:
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("Cairo-Regular.ttf", "Cairo"); // خط عربي واضح
});
// تسجيل خدمات التوطين
builder.Services.AddLocalization(options =>
options.ResourcesPath = "Resources/Strings");
builder.Services.AddSingleton<LocalizationService>();
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<MainPage>();
return builder.Build();
}
الآن يمكنك حقن IStringLocalizer<MainViewModel> في أي ViewModel:
public partial class MainViewModel : ObservableObject
{
private readonly IStringLocalizer<MainViewModel> _localizer;
[ObservableProperty]
private string welcomeMessage;
public MainViewModel(IStringLocalizer<MainViewModel> localizer)
{
_localizer = localizer;
WelcomeMessage = _localizer["WelcomeTitle"];
}
[RelayCommand]
private void Greet(string userName)
{
// دعم للسلاسل ذات المعاملات
WelcomeMessage = _localizer["HelloUser", userName];
}
}
الفائدة الحقيقية تظهر مع اختبارات الوحدة: يمكنك استبدال IStringLocalizer بمُزيّف (mock) دون تحميل ملفات .resx فعلية، ما يجعل الاختبارات أسرع بمقدار عشرات المرات.
كيف يعمل FlowDirection في .NET MAUI
خاصية FlowDirection هي القلب النابض لدعم RTL في .NET MAUI. تقبل ثلاث قيم: LeftToRight، RightToLeft، و MatchParent. القاعدة الذهبية التي أُطبّقها دائمًا: اضبطها مرة واحدة فقط في نقطة عالية (عادةً App.xaml أو صفحة Shell)، ودع البقية ترثها تلقائيًا. أيّ تعيين يدوي داخل عناصر منفردة يخلق فوضى صيانة.
<!-- في App.xaml.cs، اضبط FlowDirection بناءً على ثقافة المستخدم -->
public App(IServiceProvider services)
{
InitializeComponent();
var currentCulture = CultureInfo.CurrentUICulture;
var isRtl = currentCulture.TextInfo.IsRightToLeft;
MainPage = new AppShell
{
FlowDirection = isRtl
? FlowDirection.RightToLeft
: FlowDirection.LeftToRight
};
}
عندما تضبط FlowDirection="RightToLeft"، تقوم .NET MAUI تلقائيًا بعكس:
ترتيب العناصر الأفقية في HorizontalStackLayout و Grid.
محاذاة النصوص في Label و Entry.
موضع الحواف (Margin, Padding)، بحيث يصبح Left فعليًا هو الجانب الأيمن.
اتجاه أشرطة التمرير والقوائم في CollectionView.
الفرق بين MatchParent و RightToLeft الصريح
MatchParent هو القيمة الافتراضية في العناصر الأبناء، ما يعني أن العنصر يرث اتجاه والده. هذا هو ما تريده في 95% من الحالات. الاستثناء الوحيد: إذا كان لديك حقل إدخال يجب أن يقبل نصًا LTR فقط (مثل رقم هاتف بصيغة دولية أو عنوان URL)، اضبط FlowDirection="LeftToRight" صراحةً على ذلك العنصر لتحسين تجربة الإدخال.
تغيير اللغة أثناء التشغيل دون إعادة تشغيل التطبيق
هذا هو الجزء الأصعب، وأنا أُصارحك أنه أخذ مني ساعات في أول مشروع. ملفات .resx عندما تُستخدم عبر x:Static في XAML، تُحدَّد قيمها عند وقت البناء ولا تتغير. الحل الصحيح هو استخدام التوطين الديناميكي عبر خدمة مخصصة تُطلق حدث تغيير:
public class LocalizationService : INotifyPropertyChanged
{
private readonly IStringLocalizer<AppResources> _localizer;
public LocalizationService(IStringLocalizer<AppResources> localizer)
{
_localizer = localizer;
}
// الوصول عبر الفهرس يعمل من XAML: {Binding [WelcomeTitle], Source={...}}
public string this[string key] => _localizer[key];
public event PropertyChangedEventHandler PropertyChanged;
public void SetCulture(string cultureCode)
{
var culture = new CultureInfo(cultureCode);
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
// إخطار جميع الروابط بأن كل المفاتيح تغيرت
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(Binding.IndexerName));
// تحديث FlowDirection على الصفحة الجذر
if (Application.Current?.MainPage is Shell shell)
{
shell.FlowDirection = culture.TextInfo.IsRightToLeft
? FlowDirection.RightToLeft
: FlowDirection.LeftToRight;
}
}
}
ثم في XAML، اربط النصوص عبر الخدمة بدلاً من الاستدعاء الثابت:
الآن استدعاء SetCulture("ar") يحدّث كل النصوص واتجاه التخطيط فورًا. جرّبت هذا النمط في ثلاثة تطبيقات إنتاجية، وأداؤه ممتاز حتى مع 800+ سلسلة مربوطة على الشاشة.
تنسيق الأرقام والتواريخ والعملات حسب الثقافة
خطأ شائع أراه كثيرًا: المطورون يترجمون النصوص لكنهم ينسون أن الأرقام والتواريخ لها قواعد ثقافية أيضًا. في الثقافة العربية (خاصة ar-SA)، الأرقام الهندية مثل ٠١٢٣٤٥ قد تكون مفضلة، والتواريخ تُكتب بترتيب مختلف، والعملات لها رموز محلية.
القيمة
ثقافة en-US
ثقافة ar-SA
ثقافة ar-EG
الرقم 1234.5
1,234.5
١٬٢٣٤٫٥
1.234,5
التاريخ
9/11/2026
١١/٠٩/١٤٤٨ هـ
11/09/2026
العملة 99.9
$99.99
٩٩٫٩٩ ر.س.
ج.م. 99.99
اليوم الأول من الأسبوع
الأحد
السبت
السبت
القاعدة التي أتبعها: افصل دائمًا بين CurrentUICulture (لغة الواجهة) و CurrentCulture (تنسيق الأرقام والتواريخ). قد يريد المستخدم واجهة عربية لكن يحتفظ بتنسيق أرقام إنجليزي لأنه معتاد عليه. اجعل هذا خيارًا في إعدادات التطبيق:
// السماح للمستخدم بضبط لغة الواجهة وتنسيق الأرقام بشكل منفصل
public void ApplyUserPreferences(string uiLanguage, string numberFormat)
{
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(uiLanguage);
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(numberFormat);
// مثال على استخدامها في تنسيق التواريخ
var formatted = DateTime.Now.ToString("D",
CultureInfo.CurrentCulture); // يحترم اختيار المستخدم
}
عكس الأيقونات والصور في وضع RTL
هذه واحدة من أكثر النقاط التي يُخفق فيها المطورون. عندما تعكس التخطيط لـ RTL، معظم الأيقونات تحتاج انعكاسًا أيضًا (أزرار "التالي" و"السابق"، أسهم التنقل، أيقونات الرجوع). لكن ليست كل الأيقونات؛ الشعارات، أيقونات وسائل التواصل، والأرقام لا يجب عكسها.
الطريقة الأنظف: استخدم محوّل قيمة (Value Converter) يطبق ScaleX="-1" فقط على الأيقونات التي تحتاج عكسًا:
public class RtlMirrorConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
var isRtl = CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft;
return isRtl ? -1.0 : 1.0;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture) =>
throw new NotImplementedException();
}
الخطوط العربية عادةً أطول عموديًا من نظيراتها اللاتينية. لو استخدمت الخط الافتراضي، ستجد أن النصوص العربية تبدو مقصوصة في الأزرار والحقول. أنصح دائمًا بإضافة خط عربي مخصص مثل Cairo أو IBM Plex Sans Arabic، وضبط LineHeight="1.4" على الأقل. حتى الاستوديو الفلسطيني الذي عملت معه على تطبيق تجاري كان يستخدم خط Tajawal لأنه يوازن مع Roboto في الوضع الإنجليزي.
الإعدادات الخاصة بمنصة Android و iOS
.NET MAUI يخفي معظم التفاصيل الخاصة بكل منصة، لكن التوطين هو الاستثناء، إذ تحتاج تعديلات صريحة على مستوى كل نظام. على Android، افتح Platforms/Android/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:label="MyApp"
android:supportsRtl="true"
android:allowBackup="true">
</application>
<!-- تحديد اللغات المدعومة صراحةً لتفعيل Google Play Auto-Translate -->
<queries>
<package android:name="com.google.android.googlequicksearchbox" />
</queries>
</manifest>
على iOS، افتح Platforms/iOS/Info.plist وأضف المفاتيح التالية:
بالإضافة إلى ذلك، إذا كنت تدعم متجر التطبيقات في السوق العربي، تحتاج إلى إنشاء لقطات شاشة (screenshots) خاصة بكل لغة عبر App Store Connect. حتى المتاجر المتعددة مثل Huawei AppGallery تفضّل رؤية شاشات باللغة العربية للمستخدمين في دول الخليج.
ضبط اللغة الافتراضية للاختبار
عند تطوير التطبيق، ستحتاج غالبًا لفرض لغة معينة دون تغيير إعدادات الجهاز. أضف هذا الكود في App.xaml.cs خلف علامة مؤقتة:
#if DEBUG
var forceLocale = "ar-SA";
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(forceLocale);
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(forceLocale);
#endif
احذف هذا قبل النشر. رأيت أكثر من مرة تطبيقات تُنشر مع "ar-SA" مثبّتة قسريًا لأن المطور نسي إزالة سطر الاختبار. حصل هذا معي شخصيًا في تحديث طارئ لتطبيق مصرفي، ولم يكن مضحكًا وقتها.
الأخطاء الشائعة وكيفية تجنبها
بعد شحن أكثر من ٦ تطبيقات .NET MAUI ثنائية اللغة، هذه هي الأخطاء التي أراها تتكرر في مراجعات الكود:
الاعتماد على Margin="10,0,0,0" للتخطيطات الأفقية: في وضع RTL، الحواف تنقلب ما قد يخلق ازدواجية غير مرغوبة. استخدم Padding على الحاوية الأم بدلاً من ذلك.
خلط النصوص العربية والإنجليزية في نفس Label: يؤدي إلى مشاكل مربكة في الاتجاه. استخدم FormattedString مع Span لكل جزء بلغته، أو أضف علامات Unicode للتحكم في الاتجاه (RLM/LRM).
ترجمة رسائل الخطأ في الخادم: يعقّد الاختبار ولا يحترم لغة المستخدم النهائي. أرجِع رموز خطأ من الخادم وترجمها في العميل عبر IStringLocalizer.
عدم اختبار حالات القص: بعض النصوص العربية أطول من نظيراتها الإنجليزية بنسبة 40%. اختبر دائمًا الأزرار بأطول ترجمة ممكنة قبل الشحن.
نسيان جدول الاتجاه في CollectionView: إذا كنت تستخدم ItemsLayout="HorizontalList"، تأكد من ضبط FlowDirection عليه صراحةً لأنه لا يرث دائمًا.
من الأدوات المفيدة، dotnet-format يمكن أن يفحص ملفات .resx عن مفاتيح مفقودة بين اللغات. أدمجه في خط CI الخاص بي كما أوضحت في دليل GitHub Actions لـ .NET MAUI، فهو يمنعك من شحن بناء مع نصوص إنجليزية غير مترجمة إلى العربية.
الأسئلة الشائعة
كيف أضيف دعم اللغة العربية إلى تطبيق .NET MAUI موجود؟
أنشئ ملف AppResources.ar.resx في مجلد Resources/Strings بجانب الملف الافتراضي، ثم أضف android:supportsRtl="true" في AndroidManifest.xml و CFBundleLocalizations في Info.plist. أخيرًا، اضبط FlowDirection على صفحة Shell الجذر بناءً على CultureInfo.CurrentUICulture.
هل يدعم .NET MAUI التخطيط من اليمين لليسار (RTL) بشكل كامل؟
نعم، .NET MAUI 10 يدعم RTL أصليًا عبر خاصية FlowDirection. عند ضبطها على RightToLeft، يتم عكس ترتيب العناصر ومحاذاة النصوص والحواف تلقائيًا في معظم عناصر التحكم بما في ذلك Grid و CollectionView و FlyoutMenu.
لماذا لا يتغير النص في XAML عند تغيير اللغة أثناء التشغيل؟
لأن x:Static يُقيّم مرة واحدة عند إنشاء الصفحة. استخدم خدمة توطين ديناميكية تنفّذ INotifyPropertyChanged مع فهرس (indexer)، واربط النصوص عبر {Binding [Key], Source={StaticResource L10n}} بدلاً من x:Static.
هل يجب عكس جميع الأيقونات في وضع RTL؟
لا. اعكس فقط الأيقونات ذات الاتجاه مثل الأسهم وأزرار الرجوع والتقدم. لا تعكس الشعارات، الأرقام، الأيقونات التمثيلية (كأيقونة القلب أو النجمة)، أو أيقونات وسائل التواصل. استخدم محوّل قيم مع ScaleX="-1" يطبَّق انتقائيًا.
كيف أختبر تطبيقي بلغات متعددة قبل النشر؟
استخدم Pseudo-localization لتوليد ترجمات وهمية تكشف مشاكل القص والترميز، ثم اختبر يدويًا على أجهزة حقيقية مضبوطة على العربية والإنجليزية. أدرج فحص .resx في CI لضمان تطابق المفاتيح بين اللغات، وتأكد من اختبار الأزرار بأطول ترجمة ممكنة.
دليل عملي مُختبَر لترحيل تطبيقات Xamarin.Forms إلى .NET MAUI 10 في 2026: من Upgrade Assistant إلى تحويل Renderers إلى Handlers، مع بدائل حزم NuGet وحلول للمشاكل الشائعة.
دليل عملي شامل لحقن التبعيات (DI) في .NET MAUI 10 مع IServiceCollection: تسجيل الخدمات، دورات الحياة، الخدمات المفتاحية، أنماط المصانع، وأمثلة كود جاهزة للإنتاج مع اختبارات الوحدات.