العودة إلى المدونة
Tutorial6 دقيقة قراءة

التعريب والتدويل في 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 حتى يتمكن اللاعبون من تجاوز اللغة المكتشفة تلقائيًا في أي وقت.

جاهز للبدء؟

احصل على السكربتات من متجرنا، أو انضم إلى Discord للدعم والتحديثات ونظرة على ما هو قادم.