From 567ee506a190c8e531ca163ba1d73d6bfac444e9 Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Tue, 21 Jul 2026 16:50:25 +0200 Subject: [PATCH 1/2] feat(es_extended): fall back to English for missing locale keys When a key is absent from the active locale, Translate now borrows the English string instead of returning a placeholder. Locale loading is factored out so the target and English tables are lazily cached. Toggle with the esx:localeFallback convar (on by default) or per-resource Config.LocaleFallback. --- [core]/es_extended/locale.lua | 44 +++++++++++++++++------ [core]/es_extended/shared/config/main.lua | 5 +++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/[core]/es_extended/locale.lua b/[core]/es_extended/locale.lua index 3f059b62..8b909f30 100644 --- a/[core]/es_extended/locale.lua +++ b/[core]/es_extended/locale.lua @@ -1,20 +1,35 @@ Locales = {} +--- English fallback for missing keys. locale.lua runs in every resource VM, so +--- a resource may opt out through its own Config.LocaleFallback; otherwise the +--- shared "esx:localeFallback" convar decides (enabled by default). +local function localeFallbackEnabled() + if Config.LocaleFallback ~= nil then + return Config.LocaleFallback + end + + return GetConvar("esx:localeFallback", "true") ~= "false" +end + +--- Lazily load and cache a locale file, false if it cannot be read +local function loadLocale(locale) + if Locales[locale] == nil then + local success, result = pcall(function() + return assert(load(LoadResourceFile(GetCurrentResourceName(), ("locales/%s.lua"):format(locale))))() + end) + + Locales[locale] = success and result or false + end + + return Locales[locale] +end + function Translate(str, ...) -- Translate string if not str then error(("Resource ^5%s^1 You did not specify a parameter for the Translate function or the value is nil!"):format(GetInvokingResource() or GetCurrentResourceName())) end - --- Load the locale file if it hasn't been loaded yet - if Locales[Config.Locale] == nil then - local success, result = pcall(function() - return assert(load(LoadResourceFile(GetCurrentResourceName(), ("locales/%s.lua"):format(Config.Locale))))() - end) - - Locales[Config.Locale] = success and result or false - end - - local translations = Locales[Config.Locale] + local translations = loadLocale(Config.Locale) if not translations then if Config.Locale == "en" then return "Locale [en] does not exist" @@ -29,6 +44,15 @@ function Translate(str, ...) -- Translate string return translations[str]:format(...) end + -- The locale exists but is missing this key: borrow the English string + -- rather than surfacing a placeholder. + if Config.Locale ~= "en" and localeFallbackEnabled() then + local english = loadLocale("en") + if english and english[str] then + return english[str]:format(...) + end + end + return ("Translation [%s][%s] does not exist"):format(Config.Locale, str) end diff --git a/[core]/es_extended/shared/config/main.lua b/[core]/es_extended/shared/config/main.lua index 0bb55117..a1194e45 100644 --- a/[core]/es_extended/shared/config/main.lua +++ b/[core]/es_extended/shared/config/main.lua @@ -4,6 +4,11 @@ local txAdminLocale = GetConvar("txAdmin-locale", "en") local esxLocale = GetConvar("esx:locale", "invalid") Config.Locale = (esxLocale ~= "invalid") and esxLocale or (txAdminLocale ~= "custom" and txAdminLocale) or "en" +-- When a key is missing from the active locale, fall back to the English string +-- instead of returning a placeholder. Set convar "esx:localeFallback" to "false" +-- to keep the placeholder (useful when authoring a new translation). +Config.LocaleFallback = GetConvar("esx:localeFallback", "true") ~= "false" + -- For ox inventory, this will automatically be adjusted, do not change! For other inventories, leave as false unless specifically instructed to change. Config.CustomInventory = false From 25d534f46e62e878bdd6feac17c3d6c5096f4608 Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Tue, 21 Jul 2026 17:15:56 +0200 Subject: [PATCH 2/2] refactor(es_extended): backfill locale from English once at load Instead of checking the English fallback on every Translate call, fill the missing keys into the target locale the first time it loads, log the gaps, and drop the English table. The active locale ends up self-contained: no per-call fallback and only one table stays in memory. --- [core]/es_extended/locale.lua | 55 +++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/[core]/es_extended/locale.lua b/[core]/es_extended/locale.lua index 8b909f30..b19cd79f 100644 --- a/[core]/es_extended/locale.lua +++ b/[core]/es_extended/locale.lua @@ -11,14 +11,48 @@ local function localeFallbackEnabled() return GetConvar("esx:localeFallback", "true") ~= "false" end ---- Lazily load and cache a locale file, false if it cannot be read +--- Read and execute a locale file, returning its table or false if unreadable. +local function readLocale(locale) + local success, result = pcall(function() + return assert(load(LoadResourceFile(GetCurrentResourceName(), ("locales/%s.lua"):format(locale))))() + end) + + return success and result or false +end + +--- Copy every key the target locale is missing from English into it once, warn +--- about the gaps, then let the English table go. The locale ends up +--- self-contained, so no per-call fallback and only one table stays in memory. +local function backfillFromEnglish(locale, translations) + local english = readLocale("en") + if not english then + return + end + + local missing = {} + for key, value in pairs(english) do + if translations[key] == nil then + translations[key] = value + missing[#missing + 1] = key + end + end + + if #missing > 0 then + print(("[es_extended] locale [%s] is missing %d translation(s), falling back to English: %s") + :format(locale, #missing, table.concat(missing, ", "))) + end +end + +--- Lazily load and cache a locale file, false if it cannot be read. local function loadLocale(locale) if Locales[locale] == nil then - local success, result = pcall(function() - return assert(load(LoadResourceFile(GetCurrentResourceName(), ("locales/%s.lua"):format(locale))))() - end) + local translations = readLocale(locale) - Locales[locale] = success and result or false + if translations and locale ~= "en" and localeFallbackEnabled() then + backfillFromEnglish(locale, translations) + end + + Locales[locale] = translations end return Locales[locale] @@ -44,15 +78,6 @@ function Translate(str, ...) -- Translate string return translations[str]:format(...) end - -- The locale exists but is missing this key: borrow the English string - -- rather than surfacing a placeholder. - if Config.Locale ~= "en" and localeFallbackEnabled() then - local english = loadLocale("en") - if english and english[str] then - return english[str]:format(...) - end - end - return ("Translation [%s][%s] does not exist"):format(Config.Locale, str) end @@ -62,4 +87,4 @@ end _ = Translate -- luacheck: ignore _U -_U = TranslateCap \ No newline at end of file +_U = TranslateCap