التعريب والتدويل في FiveM: سكربتات متعددة اللغات
شحن سكربتات FiveM بعدة لغات. ملفات اللغة، مفاتيح الاسترجاع، الأدوات والأنماط المستخدمة من قبل ox_lib وغيرها من الموارد المفتوحة المصدر الكبرى.
Agency Scripts
المؤسس والمطور الرئيسي في Agency Scripts
لماذا تهم الترجمة لخوادم FiveM
مجتمعات لعب الأدوار في FiveM تمتد عبر العالم، مع قواعد لاعبين ضخمة في ألمانيا، فرنسا، البرازيل، تركيا، وعشرات الدول الأخرى. إذا كانت سكريبتاتك تدعم الإنجليزية فقط، فأنت تقطع جزءًا كبيرًا من العملاء المحتملين وتحد من المجتمعات التي يمكنها استخدام مواردك. السكريبت المترجم بشكل صحيح يكيف كل النصوص التي يراها المستخدم، الإشعارات، القوائم، ورسائل الخطأ إلى لغة اللاعب المفضلة. هذه ليست مجرد ميزة جودة حياة بل ميزة تنافسية تميز السكريبتات الهواة عن المحترفة. الخبر الجيد هو أن تنفيذ التدويل (i18n) في FiveM بسيط بمجرد فهم النمط.
إعداد نظام اللغة
أساس أي نظام تعريب هو طريقة منظمة لتخزين واسترجاع السلاسل المترجمة. النهج الأكثر شيوعًا في FiveM هو استخدام ملفات locale، ملف لكل لغة، مخزنة في locales الدليل داخل المورد الخاص بك. كل ملف يصدر جدول أزواج مفتاح-قيمة حيث المفتاح معرف فريد والقيمة هي النص المترجم. إليك كيفية هيكلة وحدة اللغة الخاصة بك:
-- locales/en.lua
Locales = Locales or {}
Locales['en'] = {
['job_started'] = 'You have started your shift as %s.',
['job_ended'] = 'You have ended your shift. Earnings: $%d',
['not_enough_money'] = 'You do not have enough money. You need $%d.',
['inventory_full'] = 'Your inventory is full. Free up some space first.',
['vehicle_spawned'] = 'Your vehicle has been spawned nearby.',
['access_denied'] = 'You do not have permission to do that.',
['cooldown_active'] = 'Please wait %d seconds before doing that again.',
['item_received'] = 'You received %dx %s.',
}
-- locales/de.lua
Locales = Locales or {}
Locales['de'] = {
['job_started'] = 'Du hast deine Schicht als %s begonnen.',
['job_ended'] = 'Du hast deine Schicht beendet. Verdienst: $%d',
['not_enough_money'] = 'Du hast nicht genug Geld. Du brauchst $%d.',
['inventory_full'] = 'Dein Inventar ist voll. Schaffe zuerst Platz.',
['vehicle_spawned'] = 'Dein Fahrzeug wurde in der Naehe gespawnt.',
['access_denied'] = 'Du hast keine Berechtigung dafuer.',
['cooldown_active'] = 'Bitte warte %d Sekunden, bevor du das erneut tust.',
['item_received'] = 'Du hast %dx %s erhalten.',
}
بناء دالة الترجمة
جوهر النظام هو دالة ترجمة تبحث عن المفاتيح وتدعم تنسيق السلاسل مع متغيرات وسيطة. يجب أن تعود هذه الدالة بسلاسة إلى لغة افتراضية عند فقدان مفتاح في اللغة النشطة، ويجب أن تحذر المطورين من الترجمات المفقودة أثناء التطوير بدلاً من عرض المفاتيح الخام للاعبين.
-- shared/locale.lua
local currentLocale = 'en'
local fallbackLocale = 'en'
function SetLocale(locale)
if Locales[locale] then
currentLocale = locale
else
print(('[^1LOCALE^0] Language "%s" not found, falling back to "%s"'):format(locale, fallbackLocale))
currentLocale = fallbackLocale
end
end
function L(key...)
local str = nil
if Locales[currentLocale] and Locales[currentLocale][key] then
str = Locales[currentLocale][key]
elseif Locales[fallbackLocale] and Locales[fallbackLocale][key] then
print(('[^3LOCALE^0] Missing key "%s" for locale "%s", using fallback'):format(key, currentLocale))
str = Locales[fallbackLocale][key]
end
if not str then
print(('[^1LOCALE^0] Missing translation key: "%s"'):format(key))
return key
end
if ... then
return str:format(...)
end
return str
end
استخدام الترجمات في نصوصك
بمجرد تحميل وحدة اللغة، استخدام الترجمات في جميع أنحاء سكربتك بسيط مثل استدعاء L() function مع المفتاح وأي وسائط تنسيق. هذا يحافظ على نظافة كود السكريبت ويفصل المحتوى عن المنطق تمامًا.
-- server/main.lua
RegisterNetEvent('myresource:startJob', function(jobName)
local src = source
local xPlayer = ESX.GetPlayerFromId(src) -- or your framework equivalent
if not xPlayer then return end
if not HasPermission(src, jobName) then
TriggerClientEvent('ox_lib:notify', src, {
title = L('access_denied'),
type = 'error'
})
return
end
ActiveJobs[src] = { name = jobName, started = os.time() }
TriggerClientEvent('ox_lib:notify', src, {
title = L('job_started', jobName),
type = 'success'
})
end)
كشف اللغة لكل لاعب
نظام تعريب احترافي حقًا يكتشف لغة كل لاعب تلقائيًا. يمكنك تحقيق ذلك بقراءة إعداد لغة اللعبة لدى العميل أو بالسماح للاعبين باختيار لغتهم من خلال تكوين أو أمر. يستخدم نهج الكشف على جانب العميل GetCurrentLanguage native، الذي يعيد لغة اللعبة كرمز مكون من حرفين. يمكنك بعد ذلك إرسال هذا إلى الخادم بحيث تستخدم جميع الإشعارات لذلك اللاعب لغته المفضلة.
-- client/locale_detect.lua
CreateThread(function()
local gameLang = GetCurrentLanguage()
-- Map GTA language codes to your locale codes
local langMap = {
['en-us'] = 'en',
['de-de'] = 'de',
['fr-fr'] = 'fr',
['es-es'] = 'es',
['pt-br'] = 'pt',
['it-it'] = 'it',
['pl-pl'] = 'pl',
['tr-tr'] = 'tr',
['ru-ru'] = 'ru',
['zh-cn'] = 'zh',
['ja-jp'] = 'ja',
['ko-kr'] = 'ko',
}
local detected = langMap[gameLang] or 'en'
SetLocale(detected)
TriggerServerEvent('myresource:setPlayerLocale', detected)
end)
تخزين اللغة المحلية لكل لاعب على جانب الخادم
على جانب الخادم، قم بتخزين تفضيل اللغة لكل لاعب حتى تتمكن من تنسيق الرسائل باللغة الصحيحة قبل إرسالها. هذا أمر حاسم للإشعارات التي يتم تشغيلها من الخادم ورسائل الدردشة التي تنشأ من منطق جانب الخادم حيث لا يمكنك الوصول المباشر إلى إعداد اللغة لدى العميل.
-- server/locale_manager.lua
local PlayerLocales = {}
RegisterNetEvent('myresource:setPlayerLocale', function(locale)
local src = source
if Locales[locale] then
PlayerLocales[src] = locale
else
PlayerLocales[src] = 'en'
end
end)
AddEventHandler('playerDropped', function()
PlayerLocales[source] = nil
end)
function GetPlayerLocale(src)
return PlayerLocales[src] or 'en'
end
function LForPlayer(src, key...)
local locale = GetPlayerLocale(src)
local str = nil
if Locales[locale] and Locales[locale][key] then
str = Locales[locale][key]
elseif Locales['en'] and Locales['en'][key] then
str = Locales['en'][key]
end
if not str then return key end
if ... then return str:format(...) end
return str
end
تعريب واجهات NUI وJavaScript
تستخدم العديد من سكربتات FiveM واجهات NUI (HTML/JS)، وهذه تحتاج إلى تعريب أيضًا. أفضل نهج هو إرسال جدول اللغة بالكامل إلى إطار NUI عند التهيئة، ثم استخدام دالة ترجمة جافا سكريبت تعكس دالة Lua. هذا يتجنب ردود NUI المستمرة لكل سلسلة نصية.
// nui/js/locale.js
let currentLocale = {};
let fallbackLocale = {};
window.addEventListener('message', (event) => {
if (event.data.action === 'setLocale') {
currentLocale = event.data.locale || {};
fallbackLocale = event.data.fallback || {};
updateAllTranslations();
}
});
function L(key...args) {
let str = currentLocale[key] || fallbackLocale[key] || key;
if (args.length > 0) {
let i = 0;
str = str.replace(/%[sd]/g, () => args[i++] ?? '');
}
return str;
}
function updateAllTranslations() {
document.querySelectorAll('[data-locale]').forEach((el) => {
const key = el.getAttribute('data-locale');
el.textContent = L(key);
});
}
تكوين ملف الموارد
خاصتك fxmanifest.lua يحتاج إلى تضمين جميع ملفات اللغة بحيث يتم تحميلها عند بدء المورد. استخدم نمط glob لالتقاط أي ملفات لغة جديدة تضيفها تلقائيًا دون الحاجة إلى تحديث البيان التعريفي في كل مرة. تأكد من تحميل وحدة اللغة المشتركة قبل ملفات بيانات اللغة.
-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
shared_scripts {
'shared/locale.lua',
'locales/*.lua',
}
client_scripts {
'client/locale_detect.lua',
'client/main.lua',
}
server_scripts {
'server/locale_manager.lua',
'server/main.lua',
}
ui_page 'nui/index.html'
files {
'nui/**/*',
}
أفضل الممارسات لتوطين FiveM
- استخدم مفاتيح وصفية بدلاً من المعرفات الرقمية. مفاتيح مثل
inventory_fullتوثق نفسها وتجعل الصيانة أسهل منmsg_042. - استخدم دائمًا نائبات التنسيق (
%s,%d) للقيم الديناميكية بدلاً من ربط السلاسل النصية. اللغات المختلفة لها ترتيب كلمات مختلف، لذا يجب أن تكون القيم قابلة للإدراج في مواقع مختلفة. - تضمين تعليقات السياق في ملفات اللغة الخاصة بك حتى يفهم المترجمون مكان عرض كل سلسلة وماذا تمثل وسائط التنسيق.
- اختبر مع سلاسل طويلة. النص الألماني عادة ما يكون أطول بنسبة 30% من النص الإنجليزي. تأكد من أن عناصر واجهة المستخدم الخاصة بك يمكنها التعامل مع الترجمات الأطول دون كسر التخطيط.
- لا تقم بترميز نصوص موجهة للمستخدم بشكل ثابت. يجب أن تمر كل إشعار، تسمية قائمة، نص مساعدة، ورسالة خطأ عبر
L()function، حتى لو كنت تدعم لغة واحدة فقط في البداية. - توفير أمر لغة مثل
/lang deحتى يتمكن اللاعبون من تجاوز اللغة المكتشفة تلقائيًا في أي وقت.