返回博客
Tutorial3 分钟阅读

FiveM本地化与国际化:多语言脚本

以多语言发布 FiveM 脚本。Locale 文件、回退键、ox_lib 及其他主要开源资源使用的工具和模式。

Agency Scripts

Agency Scripts 创始人兼首席开发者

为什么本地化对 FiveM 服务器很重要

FiveM 角色扮演社区遍布全球,德国、法国、巴西、土耳其及其他数十个国家拥有庞大玩家基础。如果你的脚本仅支持英语,将失去大量潜在客户并限制社区使用。合适的本地化脚本会根据玩家偏好语言调整所有用户界面文本、通知、菜单和错误信息。这不仅是提升体验的功能,更是区分业余与专业脚本的竞争优势。好消息是,一旦理解模式,在 FiveM 中实现国际化(i18n)非常简单。

设置本地化系统

任何本地化系统的基础是结构化的方式来存储和检索翻译字符串。在 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 框架初始化时发送整个本地化表,然后使用一个 JavaScript 翻译函数,功能与 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 需要包含所有本地化文件,以便资源启动时加载。使用通配符模式自动拾取你添加的任何新本地化文件,无需每次更新清单。确保共享本地化模块在本地化数据文件之前加载。

-- 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本地化的最佳实践

  • 使用描述性键名 改用而非数字ID。键如 inventory_full 是自我文档化的,使维护比以往更容易 msg_042.
  • 始终使用格式占位符 (%s, %d)用于动态值,避免字符串拼接。不同语言词序不同,值需可插入不同位置。
  • 包含上下文注释 在您的本地化文件中,以便翻译者了解每个字符串显示位置及格式参数含义。
  • 测试长字符串。 德语文本通常比英语长 30%。确保您的 UI 元素能处理较长的翻译而不破坏布局。
  • 切勿硬编码面向用户的字符串。 每条通知、菜单标签、帮助文本和错误信息都应通过 L() function,即使你最初只支持一种语言。
  • 提供语言命令 喜欢 /lang de 以便玩家随时覆盖自动检测的语言。

准备好开始了吗?

在我们的商店获取脚本,或加入 Discord 获取支持、更新以及新功能预告。