Skip to content

Instantly share code, notes, and snippets.

@YaserAlOsh
Last active May 20, 2019 14:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save YaserAlOsh/1f6d748139418bbcf97cb01472d4663a to your computer and use it in GitHub Desktop.
Save YaserAlOsh/1f6d748139418bbcf97cb01472d4663a to your computer and use it in GitHub Desktop.
Texts Localization Solution for the Unity Engine - Unity نظام لتعدد اللغات وتوطين النصوص في محرك الألعاب
//تمت كتابة الملف من قبل الموقع مراحل - لمطوري الألعاب
//http://www.devjourney.epizy.com/عرض-النصوص-بلغات-متعددة-في-untiy/
//يمكن استخدامه في أي مشروع من دون ذكر المرجع
//ولكن لا يسمح بيعه على حدة
//فهو متوفر بشكل مجاني للجميع
namespace Localization
{
public class LocalizationsMaster : MonoBehaviour
{
[SerializeField]
SectionLocalizedTexts[] sectionsLocalizedText;
//صنف لكل قسم، يحتوي تعريف Enum للقسم
//ونسخة ال ScriptableObject التي تحتوي على النصوص والترجمات
[System.Serializable]
public class SectionLocalizedTexts
{
public TextSections textSection;
public LocalizationSO localizationScriptableObject;
}
//خاصية اللغة المختارة حاليًا
//لاحظ أنه لا يمكن تغييرها من خارج الملف البرمجي
public Language CurrentLanguage { get; private set; }
//تحديد اللغة بناءً على اللغة الافتراضية للجهاز؟
public bool useSystemLangauge;
//النسخة المشتركة للملف البرمجي
static LocalizationsMaster instance;
//دالة لاسترجاع هذه النسخة من أي مكان في المشروع
public static LocalizationsMaster getInstance()
{
return instance;
}
void Awake()
{
//إذا لم تكن النسخة معرفة بعد، فهذا يعني أن هذا الملف
//الوحيد الموجود في المشروع
if(instance == null)
{
instance = this;
}
else
{
//وجود أكثر من نسخة يعني أنه تم قراءة عنصر من الملف
//سابقًا، لذا دمر هذا العنصر الجديد
Destroy(this);
}
//جعل الملف يعيش طوال حياة المشروع
DontDestroyOnLoad(gameObject);
if (useSystemLangauge)
{
//قم بتعيين اللغة المختارة حسب اللغة الافتراضية
//..لكن فقط إذا كانت اللغة متوفرة لدينا.
if (Application.systemLanguage == SystemLanguage.English)
CurrentLanguage = Localization.Language.English;
else if(Application.systemLanguage ==
SystemLanguage.Arabic)
CurrentLanguage = Localization.Language.Arabic;
else
CurrentLanguage = Localization.Language.English;
}
}
//ستفيدنا هذه لتعيين اللغة من عناصرالواجهة
public void SetLanguageString(string language)
{
if (language.Contains("Arabic"))
CurrentLanguage = Localization.Language.Arabic;
else
{
CurrentLanguage = Localization.Language.English;
}
LocalizationEvents.CallChangeLanguage(CurrentLanguage);
}
public void SetLanguageEnum(Language language)
{
CurrentLanguage = language;
}
public string GetText(string key, TextSections textSection = TextSections.MainUI)
{
for(int s =0; s < sectionsLocalizedText.Length; s++)
{
if (sectionsLocalizedText[s].textSection == textSection)
return sectionsLocalizedText[s].localizationScriptableObject.GetString(key, CurrentLanguage);
}
return sectionsLocalizedText[0].localizationScriptableObject.GetString(key, CurrentLanguage);
}
//نضيف هنا جميع الأقسام التي نريدها في اللعبة
//أو نستخدم فسم واحد فقط، إذا لم تعجبنا فكرة الأقسام.
public enum TextSections
{
MainUI
}
}
//اللفات التي سندعمها في هذا المقال، يمكنك إضافة قدر ما تريد
public enum Language
{
English,
Arabic
}
//عرفته كمشترك لكي تبقى نسخة واحد فقط في المشروع
public static class LocalizationEvents
{
//Delegate تقوم بتوظيف دالة مكان الحدث، أي أنه عندما يُستدعى الحدث، يمكن سماعه باستخدام دالة.
public delegate void LanguageEvent(Language language);
//الحدث المشترك الذي أستدعيه عند تغيير اللغة
//لاحظ أن نوعه LanguageEvent
//مما يعني أنه سيكسب الخصائص المعرفة في الأعلى لها.
public static event LanguageEvent LanguageChanged;
//نمرر اللغة الجديدة لهذه الدالة عند تغيير اللغة
public static void CallChangeLanguage(Language newLanguage)
{
//إذا كان هناك أحد ما مشترك بالحدث، قم باستدعائه
LanguageChanged?.Invoke(newLanguage);
}
}
}
//تمت كتابة الملف من قبل الموقع مراحل - لمطوري الألعاب
//http://www.devjourney.epizy.com/عرض-النصوص-بلغات-متعددة-في-untiy/
//يمكن استخدامه في أي مشروع من دون ذكر المرجع
//ولكن لا يسمح بيعه على حدة
//فهو متوفر بشكل مجاني للجميع
using UnityEditor;
using UnityEngine;
using Localization;
//هذه التعليمة تساعدنا في إنشاء نسخ من هذا الملف
//حيث تعرّف عنصر جددي في قائمة Create
//كما سنشاهد في الخطوة التالية
[CreateAssetMenu(fileName = "Localized Strings",menuName = "Localization")]
//يجب أن يرث الملف البرمجي من ScriptableObject بدلاً من MonoBehaviour
public class LocalizationSO : ScriptableObject
{
[SerializeField]
TextAsset XmlOfStrings;
//مصفوفة معلومات النصوص
[SerializeField] //هذه التعليمة تجعل unity قادر على تحميل وعرض المتغير في المحرر inspector
KeyString[] strings;
//صنف لمعلومات النص، يحتوى على المفتاح والترجمات
[System.Serializable]//هذه التعليمة تجعل Unity قادر على التعامل مع الصنف المخصص وحفظه في ال metaData
public class KeyString
{
public string key;
public LocalizedText[] localizations;
}
//صنف لكل ترجمة للنص
// يحتوي على النص وتعريف بلغته
[System.Serializable]
public class LocalizedText
{
//لكي نكتب نصوص بأكثر من سطر (3 كحد أقصى)
[Multiline(3)]
public string localizedString;
public Language language;
}
//دالة لاسترجاع النص حسب المفتاح واللغة الحالية
//لاحظ أن صنف ال ScriptableObject يعمل كأي ملف برمجي
//حيث الدوال والمتغيرات المعرّفة يمكن استعمالها لأي نسخة منشئة منه
public string GetString(string key,Localization.Language language)
{
for (int s = 0; s < strings.Length; s++)
{
//نبحث في المصفوفة الأساسية عن المفتاح المطلوب
if (strings[s].key == key)
{
//للترجمات المعرفة لهذا المفتاح، نحصل على التي تناسب اللغة الحالية
for (int l = 0; l < strings[s].localizations.Length; l++) {
if (strings[s].localizations[l].language == language)
{
return strings[s].localizations[l].localizedString;
}
}
//بحال لم نجد ترجمة لهذا النص باللغة الجالية
return "Language: " + language + "Not Found, For Key: " + key;
}
}
//وبحال لم نجد المفتاح من الأساس
return "Key: " + key + " Not Found";
}
[ExecuteInEditMode] //يجب إضافة هذه التعليمة لكي تعمل الدالة من دون تشغيل اللعبة
public void GetStringsFromXML()
{
//إذا لم يتم تعيين أي ملف في ال inspector
if (XmlOfStrings == null)
return;
//ننشئ نسخة من XmlDocument ونعطيها نص الملف
XmlDocument xMLDocument = new XmlDocument();
xMLDocument.LoadXml(XmlOfStrings.text);
if(xMLDocument == null)
{//إذا حدث خطأ ما لسبب ما (قد يكون الملف غير مخصص لل xml)
Debug.LogError("Not Found: " + XmlOfStrings.name);
return;
}
//هذه العقدة تقع بعد وصف ال Localization، وهي التي تحتوي على عناصر
//النصوص
XmlNode MainXmlNode = xMLDocument.ChildNodes.Item(1);
//نعرف مصفوفة جديدة بعدد العناصر الموجودة في الملف
KeyString[] newStrings = new KeyString[MainXmlNode.ChildNodes.Count];
int currCount = 0;
//لكل عنصر نص في المستند
foreach (XmlNode xmlNode in MainXmlNode.ChildNodes)
{
//نتأكد أنه يحتوي على المفتاح
if(xmlNode.LocalName == "Key")
{
KewStrings[currCount] = new KeyString();
//نحصل على المفتاح
newStrings[currCount].key = xmlNode.Attributes.GetNamedItem("name").Value;
int index = 0;
//نبني مصفوفة للترجمات بعدد الأبناء المتوفرة
newStrings[currCount].localizations = new LocalizedText[xmlNode.ChildNodes.Count];
//لكل ابن موجود
foreach (XmlNode localizedVersions in xmlNode)
{
Language language;
//نحاول تحويل اللغة إلى النوع Enum المخصص للغات
//إن لم تنجح هذه العملية فهذا يعني أن اللغة غير مدعومة
//ويجب إضافتها لل Enum
if(!Enum.TryParse( localizedVersions.LocalName, out language))
{
return;
}
newStrings[currCount].localizations[index] = new LocalizedText();
//نحضر النص ونعينه
newStrings[currCount].localizations[index].localizedString = localizedVersions.InnerText;
//نعين اللغة
newStrings[currCount].localizations[index].language = language;
index++;
}
currCount++;
}
}
//نبدل المصفوفة الموجودة بالجديدة التي بنيناها
strings = newStrings;
}
}
[CustomEditor(typeof(LocalizationSO))]
public class LocalizationSOEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
//ال target هي نسخة الملف البرمجي المرتبط بهذا المحرر
LocalizationSO localizationSO = (LocalizationSO)target;
//ننشئ زرًا جديدًأ
if (GUILayout.Button("Get Items From XML Document"))
{
//نستدعي الدالة التي كتبناها للتو
//(تأكد أن هذه التعليمة موجودة في أعلاها [ExecuteInEditMode])
localizationSO.GetStringsFromXML();
}
base.OnInspectorGUI();
serializedObject.ApplyModifiedProperties();
}
}
//هذه التعليمة تخبر المحرك بأن هذا الكلاس مخصص للصنف
//LocalizationSO.LocalizedText
[CustomPropertyDrawer(typeof(LocalizationSO.LocalizedText))]
//يجب أن نرث من PropertyDrawer لنعدل على طريقة العرض
public class LocalizedTextEditor : PropertyDrawer
{
//متغير لحفظ قيمة الارتفاع الافتراضية
float height = EditorGUIUtility.singleLineHeight;
//هذه الدالة سيتم استدعائها لكل عنصر في المصفوفة، نقوم هنا بإعادة
///كتابتها لنغير الارتفاع حسب ما نحتاج
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
// نحضر القيم المطلوبة : النص واللغة
SerializedProperty localizedText =
property.FindPropertyRelative("localizedString");
SerializedProperty language =
property.FindPropertyRelative("language");
//نحفظ النص كمتغير نصي (أجل :3
string correctedArabicText = localizedText.stringValue;
if (language.enumValueIndex == (int)Language.Arabic)
{
//نقوم بإصلاح النص إذا كان للغة العربية
//انبته هنا أننا لم نعين قيمته بعد التصحيح لمحرر النص بعد
correctedArabicText =
ArabicSupport.ArabicFixer.Fix(localizedText.stringValue, true,
false);
}
//للعنوان، نختصره إذا كان طويلاً
title.text = correctedArabicText.Length > 10 ? correctedArabicText.Substring(0, 10) + "..." : correctedArabicText;
//نبدأ بالخاصية
//تسمح لنا هذه الدالة بعرض الخواص الموجودة في العناصر في
//الواجهة بكل سهولة
label = EditorGUI.BeginProperty(position, title, property);
//مبدئيًا، نحدد الارتفاع بالقيمة الافتراضية
position.height = EditorGUIUtility.singleLineHeight;
//نضيف القدرة على إظهار وإخفاء العناصر
//القيمة الأخيرة تحدد ما إذا كنا نستطيع الضغظ على الاسم للإخفاء
//والإظهار
property.isExpanded = EditorGUI.Foldout(position,
property.isExpanded, label,true);
//فقط إذا كانت العناصر ظاهرة
if (property.isExpanded)
{
//نحرك كل عنصر في المصفوفة إلى اليمين قليلاً
EditorGUI.indentLevel = 10;
//الصنف Rect هو المسؤول عن أبعاد العناصر
//هنا نقوم فقط بتحريكه للأسفل، ونعين الارتفاع كثلاثة أضعاف
//الارتفاع الافتراضي (أي ثلاث سطور
//تذكر أن الزيادة الموجبة الرأسية تحرك العنصر للأسفل
//أجل، هذه أحد غرائب برمجة الواجهات
Rect localizedRect = new Rect(position.x, position.y + height * 1f + 4f, position.width, height * 3f);
//نقوم بعرض خاصية النص بالأبعاد المحددة
//هذه هي فائدة استخدام دالة EditorGUI.BeginProperty
EditorGUI.PropertyField(localizedRect, localizedText, new GUIContent("Localized String"));
//نقوم بالعملية ذاتها لعنصر اختيار اللغة
//لاحظ الفرق في موقع الإزاحة الرأسية
Rect languageRect = new Rect(position.x, position.y + height * 4f + 6f, position.width, height);
EditorGUI.PropertyField(languageRect, language, new GUIContent("Language"));
//والآن الخطوة الأهم، للغة العربية:
if (language.enumValueIndex == (int)Language.Arabic)
{
//أبعاد جديدة لعنصر عرض النص المصحح
//نفس الموقع الأفقي
//وإنزاله للأسفل أكثر
Rect arabicCorrectedLabel = new Rect(position.x , position.y + height * 5f + 8f, localizedRect.width, height * 3f);
//بعض خواص التنسيق البسيطة
GUIStyle RightAlign = EditorStyles.textArea; RightAlign.alignment = TextAnchor.MiddleCenter;
//إنشاء خاصية لعرض النص
//Selectable تعني أنه يمكن نسخ النص، ولكن لا يمكن التعديل عليه
//لاحظ هنا أنني الآن مرّرت النص المصحح
EditorGUI.SelectableLabel(arabicCorrectedLabel, correctedArabicText, RightAlign);
}
}
EditorGUI.EndProperty();
//نقوم بتطبيق التغييرات على الواجهة
property.serializedObject.ApplyModifiedProperties();
}
//نسخة مسؤولة عن تحديد خواص رأسية الخصائص
GUIContent title = new GUIContent();
//هذه الدالة هي ما تقوم بالعرض، وبداخلها سنقوم بتكوين العناصر حسب
//ما نرغب
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
//إذا لم يتم الضغظ على عنصر المصفوفة لعرض محتواه
if (!property.isExpanded)
return EditorGUIUtility.singleLineHeight;
//نبحث عن خاصية اللغة لعنصر المصفوفة الحالي
SerializedProperty language = property.FindPropertyRelative("language");
//إذا كانت اللغة العربية، نقوم بزيادة الارتفاع بما يعادل 9 أضعاف القيمة الافتراضية
//وهذا لأن طول محرر النص 3، مع طول اختيار اللغة 1، ومحرر النص الآخر 3، و1 للعنوان يصبحوا 8
if (language.enumValueIndex == (int)Language.Arabic)
return EditorGUIUtility.singleLineHeight * 8f + 6f;
//وإلا، فلن نعرض محرر النص الثاني، لذا ننقص 3 قيم للارتفاع، مع مسافة قليلة بين القيم
else
return EditorGUIUtility.singleLineHeight * 5f + 4f;
}
}
//تمت كتابة الملف من قبل الموقع مراحل - لمطوري الألعاب
//http://www.devjourney.epizy.com/عرض-النصوص-بلغات-متعددة-في-untiy/
//يمكن استخدامه في أي مشروع من دون ذكر المرجع
//ولكن لا يسمح بيعه على حدة
//فهو متوفر بشكل مجاني للجميع
using UnityEngine;
using Localization;
using TMPro;
//فقط لأنني لا أحب كتابة أسماء طويلة، اختصرت الاسم هنا ^-^
using LM = Localization.LocalizationsMaster;
//يجب أن يحتوى الكائن على عنصر TextMeshProUGUI
[RequireComponent(typeof(TextMeshProUGUI))]
public class LocalizeTextMPro : MonoBehaviour
{
//حقول للمفتاح والقسم الخاص بالنص
[SerializeField]
string StringKey;
[SerializeField]
Localization.TextSections textSection;
private string requiredText;
TextMeshProUGUI tmproUGUI;
void Start()
{
tmproUGUI = GetComponent<TextMeshProUGUI>();
//نحصل على النص المطلوب من مدير التوطين، حسب المفتاح والقسم
//المحددين.
requiredText = LM.getInstance().GetText(StringKey, textSection);
//نعرض النص في العنصر tmproUGUI
tmproUGUI.text = requiredText;
CheckArabicLanguage();
Localization.LocalizationEvents.LanguageChanged += LanguageChanged;
}
private void LanguageChanged(Language language)
{
requiredText = LM.getInstance().GetText(StringKey, textSection);
tmproUGUI.text = requiredText;
CheckArabicLanguage();
}
//الحالة الخاصة التي تكون اللغة فيها عربية - أو حتى فارسية، إذا أردنا.
private void CheckArabicLanguage()
{
if (LM.getInstance().CurrentLanguage == Language.Arabic)
{
//هذا هو الملف البرمجي الذي كتبناه في المقال السابق
//تأكد من وجوده في مشروعك.
//هنا نتأكد من وجوده في الكائن
if (!GetComponent<FixArabicTMProUGUI>())
{
gameObject.AddComponent<FixArabicTMProUGUI>();
}
//هذه الدالة هي المسؤولة عن إعدادالنص المراد إصلاحه
//احصل على الملف من هنا
//https://gist.github.com/YaserAlOsh/48d9e5b1785135df1d4d4264437c8d03
GetComponent<FixArabicTMProUGUI>().UpdateText(requiredText);
} //إذا لم تكن اللغة عربية، أزل هذا الملف، لأنه سيؤذي النص والأداء
else if (GetComponent<FixArabicTMProUGUI>())
Destroy(GetComponent<FixArabicTMProUGUI>());
}
//هناك مشكلة تحدث عن تدمير الجسم
//حيث أن المرجع للملف المشترك في الحدث يبقى موجودًا، بعد تدمير الجسم..
//ثم عند تغيير اللغة يتم استدعاءء الحدث، ، ولكنه لم يعد موجودًا مما يعطي خطأً
//الحل هو إزالة المرجع عند تدمير الجسم
void OnDestroy()
{
LocalizationEvents.LanguageChanged -= LanguageChanged;
}
}
//تمت كتابة الملف من قبل الموقع مراحل - لمطوري الألعاب
//http://www.devjourney.epizy.com/عرض-النصوص-بلغات-متعددة-في-untiy/
//يمكن استخدامه في أي مشروع من دون ذكر المرجع
//ولكن لا يسمح بيعه على حدة
//فهو متوفر بشكل مجاني للجميع
using UnityEngine;
using Localization;
using UnityEngine.UI;
//فقط لأنني لا أحب كتابة أسماء طويلة، اختصرت الاسم هنا ^-^
using LM = Localization.LocalizationsMaster;
//يجب أن يحتوى الكائن على عنصر TextMeshProUGUI
[RequireComponent(typeof(Text))]
public class LocalizeTextMPro : MonoBehaviour
{
//حقول للمفتاح والقسم الخاص بالنص
[SerializeField]
string StringKey;
[SerializeField]
Localization.TextSections textSection;
private string requiredText;
Text textUI;
void Start()
{
textUI = GetComponent<Text>();
//نحصل على النص المطلوب من مدير التوطين، حسب المفتاح والقسم
//المحددين.
requiredText = LM.getInstance().GetText(StringKey, textSection);
//نعرض النص في العنصر textUI
textUI.text = requiredText;
CheckArabicLanguage();
Localization.LocalizationEvents.LanguageChanged += LanguageChanged;
}
private void LanguageChanged(Language language)
{
requiredText = LM.getInstance().GetText(StringKey, textSection);
textUI.text = requiredText;
CheckArabicLanguage();
}
//الحالة الخاصة التي تكون اللغة فيها عربية - أو حتى فارسية، إذا أردنا.
private void CheckArabicLanguage()
{
if (LM.getInstance().CurrentLanguage == Language.Arabic)
{
//هذا هو الملف البرمجي الذي كتبناه في المقال السابق
//تأكد من وجوده في مشروعك.
//https://gist.github.com/YaserAlOsh/48d9e5b1785135df1d4d4264437c8d03
//هنا نتأكد من وجوده في الكائن
if (!GetComponent<SetArabicFixedText>())
{
gameObject.AddComponent<SetArabicFixedText>();
}
//هذه الدالة هي المسؤولة عن إعدادالنص المراد إصلاحه
GetComponent<SetArabicFixedText>().UpdateText(requiredText);
} //إذا لم تكن اللغة عربية، أزل هذا الملف، لأنه سيؤذي النص والأداء
else if (GetComponent<SetArabicFixedText>())
Destroy(GetComponent<SetArabicFixedText>());
}
//هناك مشكلة تحدث عند تدمير الجسم
//حيث أن المرجع للملف المشترك في الحدث يبقى موجودًا، بعد تدمير الجسم..
//ثم عند تغيير اللغة يتم استدعاءء الحدث، ، ولكنه لم يعد موجودًا مما يعطي خطأً
//الحل هو إزالة المرجع عند تدمير الجسم
void OnDestroy()
{
LocalizationEvents.LanguageChanged -= LanguageChanged;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment