From 64735bdf436f19c78ee67d6be946f756ea8cc344 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 4 Apr 2026 11:32:46 -0500 Subject: [PATCH] Implemented lua locale gettext --- assets/scene/main_menu/main_menu.lua | 4 +- src/dusk/engine/engine.c | 11 -- src/dusk/locale/localemanager.c | 190 +++++++++++++++++++ src/dusk/locale/localemanager.h | 35 ++++ src/dusk/script/module/locale/modulelocale.c | 125 +++++++----- src/dusk/script/module/locale/modulelocale.h | 22 +-- 6 files changed, 304 insertions(+), 83 deletions(-) diff --git a/assets/scene/main_menu/main_menu.lua b/assets/scene/main_menu/main_menu.lua index 2d0642c..b099254 100644 --- a/assets/scene/main_menu/main_menu.lua +++ b/assets/scene/main_menu/main_menu.lua @@ -14,6 +14,8 @@ module('locale') screenSetBackground(colorCornflowerBlue()) camera = cameraCreate(CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) +text = localeGetText('cart.item_count', 2, 2) + function sceneDispose() end @@ -39,6 +41,6 @@ function sceneRender() -- ) -- spriteBatchFlush() - textDraw(10, 10, "Hello World\nHow are you?", colorRed()) + textDraw(10, 10, text, colorWhite()) spriteBatchFlush() end \ No newline at end of file diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 0f39f45..e0bceca 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -39,17 +39,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(sceneInit()); errorChain(gameInit()); - char_t buffer[256]; - errorChain(localeManagerGetText( - "user.invite_status", - buffer, - sizeof(buffer), - 2, - "Dominic", - 2 - )); - assertUnreachable(buffer); - // Run the initial script. scriptcontext_t ctx; errorChain(scriptContextInit(&ctx)); diff --git a/src/dusk/locale/localemanager.c b/src/dusk/locale/localemanager.c index 5624137..102f655 100644 --- a/src/dusk/locale/localemanager.c +++ b/src/dusk/locale/localemanager.c @@ -75,6 +75,196 @@ errorret_t localeManagerGetText( return ret; } +errorret_t localeManagerGetTextArgs( + const char_t *id, + char_t *buffer, + const size_t bufferSize, + const int32_t plural, + const localearg_t *args, + const size_t argCount +) { + assertNotNull(id, "Message ID cannot be NULL."); + assertNotNull(buffer, "Buffer cannot be NULL."); + assertTrue(bufferSize > 0, "Buffer size must be > 0."); + assertTrue(plural >= 0, "Plural cannot be negative."); + assertTrue(argCount == 0 || args != NULL, "Args cannot be NULL when argCount > 0."); + + char_t *format = memoryAllocate(bufferSize); + if(format == NULL) { + errorThrow("Failed to allocate format buffer."); + } + + errorret_t ret = assetLocaleGetString( + &LOCALE.file, + id, + plural, + format, + bufferSize + ); + if(ret.code != ERROR_OK) { + memoryFree(format); + return ret; + } + + size_t inIndex = 0; + size_t outIndex = 0; + size_t nextArg = 0; + + buffer[0] = '\0'; + + while(format[inIndex] != '\0') { + if(format[inIndex] != '%') { + if(outIndex + 1 >= bufferSize) { + memoryFree(format); + errorThrow("Formatted locale string buffer overflow for ID: %s", id); + } + + buffer[outIndex++] = format[inIndex++]; + continue; + } + + inIndex++; + + /* Escaped percent */ + if(format[inIndex] == '%') { + if(outIndex + 1 >= bufferSize) { + memoryFree(format); + errorThrow("Formatted locale string buffer overflow for ID: %s", id); + } + + buffer[outIndex++] = '%'; + inIndex++; + continue; + } + + if(nextArg >= argCount) { + memoryFree(format); + errorThrow("Not enough locale arguments for ID: %s", id); + } + + { + char_t specBuffer[32]; + char_t valueBuffer[256]; + size_t specLength = 0; + int written = 0; + char_t specifier; + + specBuffer[specLength++] = '%'; + + /* Allow flags / width / precision */ + while(format[inIndex] != '\0') { + char_t ch = format[inIndex]; + + if( + ch == '-' || ch == '+' || ch == ' ' || ch == '#' || ch == '0' || + ch == '.' || (ch >= '0' && ch <= '9') + ) { + if(specLength + 1 >= sizeof(specBuffer)) { + memoryFree(format); + errorThrow("Format specifier too long for ID: %s", id); + } + + specBuffer[specLength++] = ch; + inIndex++; + continue; + } + + break; + } + + if(format[inIndex] == '\0') { + memoryFree(format); + errorThrow("Incomplete format specifier for ID: %s", id); + } + + specifier = format[inIndex++]; + + if(specifier != 's' && specifier != 'd' && specifier != 'f') { + memoryFree(format); + errorThrow( + "Unsupported format specifier '%%%c' for ID: %s", + specifier, + id + ); + } + + specBuffer[specLength++] = specifier; + specBuffer[specLength] = '\0'; + + switch(specifier) { + case 's': + if(args[nextArg].type != LOCALE_ARG_STRING) { + memoryFree(format); + errorThrow("Expected string locale argument for ID: %s", id); + } + + written = snprintf( + valueBuffer, + sizeof(valueBuffer), + specBuffer, + args[nextArg].stringValue ? args[nextArg].stringValue : "" + ); + break; + + case 'd': + if(args[nextArg].type != LOCALE_ARG_INT) { + memoryFree(format); + errorThrow("Expected int locale argument for ID: %s", id); + } + + written = snprintf( + valueBuffer, + sizeof(valueBuffer), + specBuffer, + args[nextArg].intValue + ); + break; + + case 'f': + if( + args[nextArg].type != LOCALE_ARG_FLOAT && + args[nextArg].type != LOCALE_ARG_INT + ) { + memoryFree(format); + errorThrow("Expected float or int locale argument for ID: %s", id); + } + + float_t floatValue = ( + args[nextArg].type == LOCALE_ARG_FLOAT ? + args[nextArg].floatValue : + (float_t)args[nextArg].intValue + ); + + written = snprintf( + valueBuffer, + sizeof(valueBuffer), + specBuffer, + floatValue + ); + break; + } + + nextArg++; + + if(written < 0) { + memoryFree(format); + errorThrow("Failed to format locale string for ID: %s", id); + } + + if(outIndex + (size_t)written >= bufferSize) { + memoryFree(format); + errorThrow("Formatted locale string buffer overflow for ID: %s", id); + } + + memoryCopy(buffer + outIndex, valueBuffer, (size_t)written); + outIndex += (size_t)written; + } + } + + buffer[outIndex] = '\0'; + memoryFree(format); + errorOk(); +} void localeManagerDispose() { if(LOCALE.fileOpen) { diff --git a/src/dusk/locale/localemanager.h b/src/dusk/locale/localemanager.h index d720c11..3d3dfba 100644 --- a/src/dusk/locale/localemanager.h +++ b/src/dusk/locale/localemanager.h @@ -17,6 +17,21 @@ typedef struct { bool_t fileOpen; } localemanager_t; +typedef enum { + LOCALE_ARG_STRING, + LOCALE_ARG_INT, + LOCALE_ARG_FLOAT +} localeargtype_t; + +typedef struct { + localeargtype_t type; + union { + const char_t *stringValue; + int32_t intValue; + float_t floatValue; + }; +} localearg_t; + extern localemanager_t LOCALE; /** @@ -52,6 +67,26 @@ errorret_t localeManagerGetText( ... ); +/** + * Get a localized string for the given message ID with a list of arguments. + * + * @param id The message ID to retrieve. + * @param buffer Buffer to write the retrieved string to. + * @param bufferSize Size of the buffer. + * @param plural Plural index to retrieve. + * @param args List of arguments for formatting the string. + * @param argCount Number of arguments in the list. + * @return An error code if a failure occurs. + */ +errorret_t localeManagerGetTextArgs( + const char_t *id, + char_t *buffer, + const size_t bufferSize, + const int32_t plural, + const localearg_t *args, + const size_t argCount +); + /** * Dispose of the locale system. */ diff --git a/src/dusk/script/module/locale/modulelocale.c b/src/dusk/script/module/locale/modulelocale.c index b5f4956..3ebb59a 100644 --- a/src/dusk/script/module/locale/modulelocale.c +++ b/src/dusk/script/module/locale/modulelocale.c @@ -13,66 +13,87 @@ void moduleLocale(scriptcontext_t *context) { assertNotNull(context, "Script context cannot be NULL"); // Execute the locale script definitions. - // scriptContextExec(context, LOCALE_SCRIPT); - - // lua_register(context->luaState, "localeGet", moduleLocaleGet); - // lua_register(context->luaState, "localeSet", moduleLocaleSet); - // lua_register(context->luaState, "localeGetName", moduleLocaleGetName); + lua_register(context->luaState, "localeGetText", moduleLocaleGetText); } -// int moduleLocaleGet(lua_State *L) { -// assertNotNull(L, "Lua state cannot be NULL"); +int moduleLocaleGetText(lua_State *L) { + // Expect string param for the id + if(!lua_isstring(L, 1)) { + luaL_error(L, "Expected message ID as first argument"); + return 0; + } -// // No arguments expected -// dusklocale_t locale = LOCALE.locale; -// lua_pushnumber(L, (lua_Number)locale); -// return 1; -// } + const char_t *id = lua_tostring(L, 1); + if(id == NULL || strlen(id) == 0) { + luaL_error(L, "Message ID cannot be NULL or empty"); + return 0; + } -// int moduleLocaleSet(lua_State *L) { -// assertNotNull(L, "Lua state cannot be NULL"); + // Optional plural param, default to 0 + int32_t plural = 0; + int top = lua_gettop(L); + int argStart = 2; -// // Requires locale ID -// if(!lua_isnumber(L, 1)) { -// luaL_error(L, "localeSet: Expected locale ID as first argument"); -// return 0; -// } + if(top >= 2 && !lua_isnil(L, 2)) { + if(!lua_isnumber(L, 2)) { + luaL_error(L, "Expected plural as second argument"); + return 0; + } -// errorret_t err; -// dusklocale_t locale = (dusklocale_t)lua_tonumber(L, 1); -// if(locale >= DUSK_LOCALE_COUNT || locale == DUSK_LOCALE_NULL) { -// luaL_error(L, "localeSet: Invalid locale ID"); -// return 0; -// } + plural = (int32_t)lua_tointeger(L, 2); + if(plural < 0) { + luaL_error(L, "Plural cannot be negative"); + return 0; + } -// err = localeManagerSetLocale(locale); -// if(err.code != ERROR_OK) { -// luaL_error(L, "localeSet: Failed to set locale"); -// errorCatch(errorPrint(err)); -// return 0; -// } + argStart = 3; + } -// return 0; -// } + // Build structured arg list from remaining Lua args + size_t argCount = (top >= argStart) ? (size_t)(top - argStart + 1) : 0; + #define MODULE_LOCALE_MAX_ARGS 16 + localearg_t argsStack[MODULE_LOCALE_MAX_ARGS]; + localearg_t *args = argsStack; -// int moduleLocaleGetName(lua_State *L) { -// assertNotNull(L, "Lua state cannot be NULL"); + if(argCount > MODULE_LOCALE_MAX_ARGS) { + luaL_error(L, "Too many args (max %d)", MODULE_LOCALE_MAX_ARGS); + return 0; + } -// // Optional ID, otherwise return current locale name -// dusklocale_t locale = LOCALE.locale; -// if(lua_gettop(L) >= 1) { -// if(!lua_isnumber(L, 1)) { -// luaL_error(L, "localeGetName: Expected locale ID as first argument"); -// return 0; -// } -// locale = (dusklocale_t)lua_tonumber(L, 1); -// if(locale >= DUSK_LOCALE_COUNT || locale == DUSK_LOCALE_NULL) { -// luaL_error(L, "localeGetName: Invalid locale ID"); -// return 0; -// } -// } + for(size_t i = 0; i < argCount; ++i) { + int luaIndex = argStart + (int)i; -// const char_t *localeName = LOCALE_INFOS[locale].file; -// lua_pushstring(L, localeName); -// return 1; -// } \ No newline at end of file + if(lua_isinteger(L, luaIndex)) { + args[i].type = LOCALE_ARG_INT; + args[i].intValue = (int32_t)lua_tonumber(L, luaIndex); + } else if(lua_isnumber(L, luaIndex)) { + args[i].type = LOCALE_ARG_FLOAT; + args[i].floatValue = lua_tonumber(L, luaIndex); + } else if(lua_isstring(L, luaIndex)) { + args[i].type = LOCALE_ARG_STRING; + args[i].stringValue = lua_tostring(L, luaIndex); + } else { + luaL_error(L, "Unsupported localization argument type"); + return 0; + } + } + + char_t buffer[1024]; + errorret_t err = localeManagerGetTextArgs( + id, + buffer, + sizeof(buffer), + plural, + args, + argCount + ); + + if(err.code != ERROR_OK) { + errorCatch(errorPrint(err)); + luaL_error(L, "Failed to get localized text for ID '%s'", id); + return 0; + } + + lua_pushstring(L, buffer); + return 1; +} \ No newline at end of file diff --git a/src/dusk/script/module/locale/modulelocale.h b/src/dusk/script/module/locale/modulelocale.h index e3abc1d..b93c6c6 100644 --- a/src/dusk/script/module/locale/modulelocale.h +++ b/src/dusk/script/module/locale/modulelocale.h @@ -16,25 +16,9 @@ void moduleLocale(scriptcontext_t *context); /** - * Script binding for getting the current locale ID. + * Script binding for getting a localized string. * * @param L The Lua state. - * @return Number of return values on the Lua stack. + * @return The number of return values on the Lua stack. */ -int moduleLocaleGet(lua_State *L); - -/** - * Script binding for setting the current locale ID. - * - * @param L The Lua state. - * @return Number of return values on the Lua stack. - */ -int moduleLocaleSet(lua_State *L); - -/** - * Script binding for getting the name of a locale by its ID. - * - * @param L The Lua state. - * @return Number of return values on the Lua stack. - */ -int moduleLocaleGetName(lua_State *L); +int moduleLocaleGetText(lua_State *L); \ No newline at end of file