لغات مخصصة باستخدام الموارد المضمنة والخارجية في .NET Framework
مقدمة
في العمل، توصلنا إلى مشكلة عادية وأنا متأكد من أن الكثير من المطورين يجب عليهم التعامل معها، وهي اللغات واللغات المخصصة من العملاء الذين يقدمون خدماتنا لعملائهم.
على سبيل المثال، لنفترض أن اللغة الأساسية تستخدم لغة غير رسمية وأن العميل يريد لغة رسمية لأن عملائه هم من كبار السن.
لاحظ أيضًا أن هذه الحلول مخصصة لعدة لغات، وسنستخدم الإنجليزية والإسبانية.
لغتي الأم ليست الإنجليزية وأعتذر عن أي أخطاء في البرنامج التعليمي. إذا وجدت أخطاء وتريد إصلاحها، يمكنك فتح طلب سحب على هذا الريبو وسأوافق عليه بكل سرور!
الموارد
الموارد هي ملفات XML ذات الامتداد .resx والتي تحتوي على بنية مفتاح/قيمة وتبدو كما يلي:
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Action_cancel" xml:space="preserve">
<value>Finish</value>
</data>
<data name="Action_greeting" xml:space="preserve">
<value>Hello</value>
</data>
</root>
سنقوم بإنشاء موارد للغتين مختلفتين: الإنجليزية و الإسبانية، وبالتالي فإن تسميات الملفات ستكون: resources.ISOLANGUAGECODE.resx، على سبيل المثال: resource.es.resx للغة الإسبانية وresource.resx للغة الإنجليزية، وفي حالة أردنا إضافة اللغة الألمانية لاحقًا، فسيتم تسمية الملف resource.de.resx.
الموارد المضمنة
الموارد المضمنة، عند تجميع المشروع، ستتم إضافتها داخل dll.
فيما يلي صورة تحتوي على مورد مضمن، كما ترون، لا يمكن رؤية الملف Resource.resx في التجميع.
موارد خارجية
على الجانب الآخر، الموارد الخارجية أو غير المضمنة هي الموارد التي سيتم إضافتها إلى المجلد بعد التجميع.
ستكون ملفات الموارد (.resx) داخل المجلد Properties
بناء المشروع
لنبدأ بمشروع وحدة التحكم في .NET Framework.
إنشاء ملف المورد المضمن
أضف ملف مورد يحتوي على عدد قليل من المفاتيح وتأكد من أنه مورد مضمن، والذي سنستخدمه لاحقًا للتحديث باستخدام موارد خارجية.
إنشاء ملف الموارد الخارجية
من أجل فصل الموارد الخارجية عن الموارد المدمجة، سنقوم بإضافة الموارد الخارجية داخل مجلد باسم حتى نتمكن من الوصول إليها بسهولة لاحقًا.
نصيحة: لإضافة مجلد داخل مجلد الخصائص، قم بإنشائه بالخارج ونقله إلى الداخل، لا يسمح لك Visual Studio بإنشائه
افعل نفس ما فعلناه مع المورد المضمن، ثم انتقل إلى properties من الملف وداخل Advanced، Build action وقم بتغييره إلى: Content وقم بتغيير Copy to Output Dictionary إلى Copy if newer.
هذه هي الطريقة التي ينبغي أن تبدو وكأنها:
إضافة مفتاح إلى app.config
نظرًا لأننا مطورون رائعون ونحب إنجاز كل شيء بشكل جيد وعدم تغيير الكود لكل عميل، فلنضيف مفتاحًا إلى appconfig الذي سيكون له اسم المجلد الذي سنبحث فيه عن ملفات الموارد
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<appSettings>
<add key="CustomResources.Folder" value="John" />
</appSettings>
</configuration>
```لذلك، لاحقًا، عندما نبحث عن الموارد، سنبحث في `Properties.John` بدلاً من `Properties.Doe`.
وهذا أيضًا يجعل من السهل التغيير عندما يتم نشر التطبيق بالفعل حيث يمكنك تغيير app.config بسهولة.
## الوصول إلى ملفات الموارد المضمنة
يعد استخدام .NET Framework أمرًا سهلاً إلى حد ما، لذلك من أجل الوصول إلى الخصائص المضمنة، علينا فقط استخدام كائن `Properties`، الذي سيحتوي على ملفات الموارد كخاصية، وداخل هذا الكائن سيكون هناك كل المفتاح/القيمة الموجودة في ملف `resx`.
```csharp
static void Main(string[] args)
{
var hello = Properties.Resource.Action_greeting;
var bye = Properties.Resource.Action_cancel;
Console.WriteLine($"Action_greeting value: {hello}, Action_cancel value: {bye}");
Console.Read();
}
تشغيل ذلك يجب أن يظهر لنا شيئًا مثل هذا:
Action_greeting value: Hello, Action_cancel value: Cancel
الوصول إلى ملفات الموارد الخارجية
هذا الجزء هو نفسه إلى حد كبير، يمكنك الوصول إلى ملفات الموارد عن طريق كائن Properties.
var hello = Properties.Resource.Action_greeting;
var bye = Properties.Resource.Action_cancel;
var johnhello = Properties.John.Resource.Action_greeting;
var johnbye = Properties.John.Resource.Action_cancel;
var doehello = Properties.Doe.Resource.Action_greeting;
var doebye = Properties.Doe.Resource.Action_cancel;
مشكلة
المشكلة الرئيسية في استخدام هذه الطريقة هي أن كل مفتاح هو خاصية داخل الكائن، لذلك علينا أن نسميه كما رأينا من قبل. إذا كنت تريد استدعاء المفتاح Action_greeting من ملف الموارد الخاص بـ John فعلينا استخدام Properties.John التالي ثم Resource.Action_greeting.
هنا المشكلة.
وذلك لأنه إذا كنا نقوم بتطوير تطبيق لعدد كبير من العملاء، فمن السيئ أن نغير طريقة استدعاء ملفات الموارد لكل منهم.
هل يمكنك تخيل ذلك؟ تجميع التطبيق لكل عميل وتغيير John إلى Doe ثم إلى شيء آخر. هذا جنون!
الحل
فكر قائد فريقنا في طريقة جيدة جدًا، مثل نظام احتياطي، يجب أن يكون لدينا نموذج أساسي للموارد، وبعد ذلك يكون لكل عميل ملف مورد يقوم بتحديث الملف بموارده، وينتهي بنا الأمر بقائمة واحدة من الموارد.
إذا كان العميل لا يريد موارد مخصصة، فإننا نستخدم الموارد الأساسية، وإذا أرادها، نستخدم موارده.
لوضع هذا في قائمة التحقق، علينا القيام بما يلي:
- ابحث عن طريقة لتعيين جميع المفاتيح/القيم إلى قاموس الموارد المضمنة.
- ابحث عن طريقة لتعيين جميع المفاتيح/القيم إلى قاموس الموارد الخارجية
- امزج كلا الملفين واحصل على قاموس واحد لكل لغة
- قم بإنشاء طريقة للوصول إلى القاموس وإرجاع القيمة
رسم بياني
دعونا نرمز
أولاً، لنقم بإنشاء فئة منفصلة حيث سيكون لدينا كل منطقنا، بدءًا من الحصول على ملفات الموارد، وحتى مزجها وإرجاع القيمة. سيتم استدعاء هذا الفصل CustomResources.
هذا ما يبدو:
class CustomResources
{
private static Dictionary<string, string> _ResourcesEnglish;
private static Dictionary<string, string> ResourcesEnglish;
private static Dictionary<string, string> _ResourcesSpanish;
private static Dictionary<string, string> ResourcesSpanish;
private static Dictionary<string, string> OverwriteDictionary(Dictionary<string, string> currentDictionary, Dictionary<string, string> newDictionary, bool addIfDoesntExist = false)
{
...
}
private static Dictionary<string, string> GetDictionaryFromEmbedded(string embedded, string cultureInfoCode)
{
...
}
private static Dictionary<string, string> GetDictionaryFromFile(string file)
{
...
}
public static string GetText(string key)
{
...
}
public static string GetText(string key, string language)
{
...
}
}
لاحظ أننا نقوم بتطبيق التحميل البطيء للخصائص مما يساعد على زيادة الأداء ويجعل تحميل القاموس مرة واحدة.
GetDictionaryFromEmbedded: يُرجع قاموسًا من الموارد المضمنة.GetDictionaryFromFile: يُرجع قاموسًا من مصادر خارجية.OverwriteDictionary: خلط قاموسين وإرجاع قاموس واحد.GetText: تُرجع قيمة معينة بمفتاح
من المورد المضمن إلى القاموس
يتعين علينا الحصول على جميع الخصائص من ملف xml وإرجاع القاموس:
private static Dictionary<string, string> GetDictionaryFromEmbedded(string embedded, string cultureInfoCode)
{
Dictionary<string, string> res = new Dictionary<string, string>();
try
{
ResourceManager rm = new ResourceManager(embedded, Assembly.GetExecutingAssembly());
var resourceSet = rm.GetResourceSet(new CultureInfo(cultureInfoCode), true, true);
var resourceDictionary = resourceSet.Cast<DictionaryEntry>()
.ToDictionary(r => r.Key.ToString(),
r => r.Value.ToString());
res = resourceDictionary;
}
catch (Exception e)
{
string a = e.Message;
// Error getting resource file
}
return res;
}
شيئين:- لاحظ أنه يحتاج إلى معامل يسمى embedded، وهو اسم الملف الذي يمكنك رؤيته في المصمم، في حالتنا هو: resources-demo.Properties.Resource.
- لدينا أيضًا معلمة تسمى CultureInfoCode، وهي رمز اللغة التي سيتم اختيارها. لحسن الحظ بالنسبة لنا، يقوم .NET Framework بالمهمة نيابةً عنا وليس علينا القيام بأي شيء، فقط قم بتعيين ما نريده إما باللغة الإنجليزية أو الإسبانية، وسيختار بين
resource.es.resxأوresource.resx
من الموارد الخارجية إلى القاموس
يعد الحصول على الملف أمرًا صعبًا بعض الشيء ولكن ليس صعبًا، يتعين علينا الحصول على الموقع الحالي للملف القابل للتنفيذ، وربط موقع ملف المورد ثم تحليله إلى القاموس.
لكن عليك أولاً إضافة المرجع إلى System.Windows.Forms، من أجل الوصول إلى ResXResourceReader.
الآن إلى طريقة GetDictionaryFromFile الخاصة بنا:
private static Dictionary<string, string> GetDictionaryFromFile(string file)
{
Dictionary<string, string> res = new Dictionary<string, string>();
string currentPath = (System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase) + file).Replace("file:\\", "");
try
{
using (ResXResourceReader resxReader = new ResXResourceReader(currentPath))
{
foreach (DictionaryEntry entry in resxReader)
{
res.Add((string)entry.Key, (string)entry.Value);
}
}
}
catch (Exception e)
{
string a = e.Message;
}
return res;
}
يجب ملء معلمات الملف بالموقع من الملف القابل للتنفيذ إلى ملف المورد، في حالتنا هو: "\\Properties\\John\\Resource.resx".
خلط القواميس
لقد انتهينا تقريبًا، أولاً، تذكر إضافة System.Configuration إلى المراجع حتى تتمكن من الوصول إلى app.settings.
private static Dictionary<string, string> OverwriteDictionary(Dictionary<string, string> currentDictionary, Dictionary<string, string> newDictionary, bool addIfDoesntExist = false)
{
var identifier = ConfigurationManager.AppSettings["CustomResources.Folder"];
if (String.IsNullOrEmpty(identifier))
return currentDictionary;
foreach (var item in newDictionary)
{
try
{
currentDictionary[item.Key] = item.Value;
}
catch (Exception)
{
if(addIfDoesntExist)
currentDictionary.Add(item.Key, item.Value);
}
}
return currentDictionary;
}
الحصول على نص من القاموس
قم بإنشاء طريقة عامة تستدعي طريقة خاصة تحدد لغة:
private static string GetText(string key, string language)
{
try
{
switch (language)
{
case "es":
return ResourceSpanish[key];
case "en":
default:
return ResourceEnglish[key];
}
}
catch (Exception)
{
return $"No value with key: {key} and language: {language}";
}
}
public static string GetText(string key)
{
try
{
return GetText(key, Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName);
}
catch (Exception)
{
return $"No value with key: {key}";
}
}
لاحظ أنه يتعين عليك تعديل المفتاح إذا كنت تريد إضافة المزيد من اللغات.
إضافة التعليمات البرمجية إلى getter الخصائص
نظرًا لأن لدينا جميع الطرق في الوقت الحالي، فيمكننا تعديل مُحضر الخاصية العامة للحصول على القيم.
private static Dictionary<string, string> _ResourcesEnglish;
private static Dictionary<string, string> ResourcesEnglish
{
get
{
if (_ResourcesEnglish == null)
{
var folderIndentifier = ConfigurationManager.AppSettings["CustomResources.Folder"]; ;
var baseResources = GetDictionaryFromEmbedded("resources-demo.Properties.Resource");
var customResources = GetDictionaryFromFile($"\\Properties\\{folderIndentifier}\\Resource.resx", "en");
_ResourcesEnglish = OverwriteDictionary(baseResources, customResources);
}
return _ResourcesEnglish;
}
}
private static Dictionary<string, string> _ResourcesSpanish;
private static Dictionary<string, string> ResourcesSpanish
{
get
{
if (_ResourcesSpanish == null)
{
var folderIndentifier = ConfigurationManager.AppSettings["CustomResources.Folder"]; ;
var baseResources = GetDictionaryFromEmbedded("resources-demo.Properties.Resources");
var customResources = GetDictionaryFromFile($"\\Properties\\{folderIndentifier}\\Resources.es.resx", "es");
_ResourcesSpanish = OverwriteDictionary(baseResources, customResources);
}
return _ResourcesSpanish;
}
}
- أولاً نحصل على المُعرف من app.settings.
- ثم نحصل على الموارد الأساسية، تلك المدمجة.
- بعد ذلك نحصل على الموارد المخصصة ولهذا نحتاج إلى اسم المجلد (وهو المعرف).
- ثم نخلطهم ونعيد القيمة.
سيتم كل هذا مرة واحدة، وبالتالي التحميل البطيء.
الاختبار
تم الانتهاء من كل ما يتعلق بالكود، فلنختبره الآن، ومن أجل الحصول على قيمة من القاموس علينا استدعاء الأسلوب CustomResources.GetText(string key) الذي يُرجع القيمة.
تحديث ملفات الموارد بأكملها
هذا الاختبار هو حالة عندما نريد تحديث المفتاح/القيمة الكاملة لملفات الموارد، كما ترون في الصور لدينا نفس المفاتيح ولكن قيم مختلفة.
سنقوم باختبار John، ومن أجل ضبط ذلك، سيتم تعيين app.config على <add key="CustomResources.Folder" value="John" />.
الآن دعونا نتحقق من ملف المورد الأساسي الخاص بنا (Properties/Resource.es.resx):
ثم ملف الموارد الخارجية الخاص بنا (Properties/John/Resource.es.resx):
حسنًا، بعد أن تم ضبط كل شيء، نقوم بتشغيل تطبيق وحدة التحكم، فلنتوقف عند الجزء get من الخصائص ونتحقق من كل شيء
folderIdentifier له قيمة إعداد التطبيقات:
baseResources له قيمة الموارد الأساسية، تلك المضمنة:
customResources لديه قيم الموارد الخارجية داخل المجلد John:
وأخيرًا، _ResourcesSpanish له قيمة مختلطة من الموارد الأساسية إلى الموارد الخارجية.
تحديث واحد فقط
الآن دعونا نختبر نفس السيناريو ولكن مع وجود سيناريو مختلف، ما عليك سوى تحديث مفتاح واحد وترك الآخر كما هو.
كما ترون، الملفات لها نفس معنى القيمة لـ Action_greeting ولكن بقيمة مختلفة لـ Action_cancel، لذلك يجب تحديث Action_cancel فقط.
ملف المورد مفقود
إذا لم تقم بتوفير ملف المورد الخارجي، فلا يهم على الإطلاق لأنه كما نتوقع أن يكون لدينا ملف، إذا فشل فإنه سيعود قاموسًا فارغًا وعند خلط كلا القواميس سينتهي الأمر بالقاموس الأساسي.
زوج مفقود في المورد المضمن
إذا كان لديك زوج في ملف المورد الخارجي، فلن يضيفه افتراضيًا إلى القاموس النهائي، يمكنك تغيير ذلك عن طريق استدعاء الطريقة OverwriteDictionary() عند خلط كلا القواميس وتعيين المعلمة addIfDoesntExist على true.
لغات مختلفة
كما ترون لم نحدد أي لغة، لأن كل ذلك يتم بواسطة الوظيفة GetText(string key) التي تستدعي GetText(string key, string language)، ويتم ملء المعلمة language بواسطة TwoLetterISOLanguageName التي تُرجع اللغة الحالية لموضوعنا.
في حالتي، لدي اللغة الإسبانية كلغة افتراضية ولهذا السبب تظهر دائمًا باللغة الإسبانية، ولكن يمكننا تجربة استخدام اللغة الإنجليزية أيضًا.
دعونا نبرمج قليلاً ونبني شيئًا للتحقق من اللغتين الإسبانية والإنجليزية.
static void Main(string[] args)
{
// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
WriteText("Action_greeting");
WriteText("Action_cancel");
// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-ES");
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
WriteText("Action_greeting");
WriteText("Action_cancel");
Console.Read();
}
private static void WriteText(string key) {
Console.WriteLine($"[Culture: {System.Threading.Thread.CurrentThread.CurrentCulture}] Key: {key} -> value: {CustomResources.GetText(key)}");
}
عند تشغيل هذا، يجب أن يكون الإخراج شيء من هذا القبيل:
[Culture: en-US] Key: Action_greeting -> value: Hello
[Culture: en-US] Key: Action_cancel -> value: Finish
[Culture: es-ES] Key: Action_greeting -> value: Hola
[Culture: es-ES] Key: Action_cancel -> value: Terminar
أول قيمتين من resources.resx وهي باللغة الإنجليزية والأخيرة كلاهما باللغة الإسبانية ويتم استرداد القيم من resources.es.resx.
هذا كل شيء
وجدنا في هذا البرنامج التعليمي طريقة لدمج كل من الموارد المضمنة والخارجية للغات، وكان هذا حلاً لمشكلة واجهناها في الفريق، وهي تعمل منذ ذلك الحين دون أي مشاكل.
يمكنك التحقق من الكود المصدري هنا.
إذا كان لديك أي سؤال فلا تتردد في إرسال تغريدة لي على @emimontesdeocaa وسأعود إليك عندما يتوفر لدي الوقت.














