Added proper plural support
This commit is contained in:
@@ -14,7 +14,7 @@ module('locale')
|
||||
screenSetBackground(colorCornflowerBlue())
|
||||
camera = cameraCreate(CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC)
|
||||
|
||||
text = localeGetText('cart.item_count', 2, 2)
|
||||
text = localeGetText('cart.item_count', 52, 2)
|
||||
|
||||
function sceneDispose()
|
||||
end
|
||||
|
||||
@@ -11,15 +11,293 @@
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
errorret_t assetLocaleLoader(assetfile_t *file) {
|
||||
errorThrow("Locale asset loading is not yet implemented.");
|
||||
errorret_t assetLocaleFileInit(
|
||||
assetlocalefile_t *localeFile,
|
||||
const char_t *path
|
||||
) {
|
||||
assertNotNull(localeFile, "Locale file cannot be NULL.");
|
||||
assertNotNull(path, "Locale file path cannot be NULL.");
|
||||
|
||||
memoryZero(localeFile, sizeof(assetlocalefile_t));
|
||||
|
||||
// Init the asset file.
|
||||
errorChain(assetFileInit(&localeFile->file, path, NULL, NULL));
|
||||
|
||||
// Open the file handle
|
||||
errorChain(assetFileOpen(&localeFile->file));
|
||||
|
||||
// Get the blank key, this is basically the header info for po files
|
||||
char_t buffer[1024];
|
||||
errorChain(assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer)));
|
||||
errorChain(assetLocaleParseHeader(localeFile, buffer, sizeof(buffer)));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetLocaleLoad(
|
||||
const char_t *path,
|
||||
void *nothing
|
||||
errorret_t assetLocaleFileDispose(assetlocalefile_t *localeFile) {
|
||||
assertNotNull(localeFile, "Locale file cannot be NULL.");
|
||||
|
||||
errorChain(assetFileClose(&localeFile->file));
|
||||
errorChain(assetFileDispose(&localeFile->file));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetLocaleParseHeader(
|
||||
assetlocalefile_t *localeFile,
|
||||
char_t *headerBuffer,
|
||||
const size_t headerBufferSize
|
||||
) {
|
||||
errorThrow("Locale asset loading is not yet implemented.");
|
||||
assertNotNull(localeFile, "Locale file cannot be NULL.");
|
||||
assertNotNull(headerBuffer, "Header buffer cannot be NULL.");
|
||||
assertTrue(headerBufferSize > 0, "Header buffer size must be > 0.");
|
||||
|
||||
// Find "Plural-Forms: " line and parse out plural form info
|
||||
char_t *pluralFormsLine = strstr(headerBuffer, "Plural-Forms:");
|
||||
if(!pluralFormsLine) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
pluralFormsLine += strlen("Plural-Forms:");
|
||||
|
||||
// Expect nplurals
|
||||
char_t *npluralsStr = strstr(pluralFormsLine, "nplurals=");
|
||||
if(!npluralsStr) {
|
||||
errorThrow("Failed to find nplurals in Plural-Forms header.");
|
||||
}
|
||||
npluralsStr += strlen("nplurals=");
|
||||
localeFile->pluralStateCount = (uint8_t)atoi(npluralsStr);
|
||||
|
||||
if(localeFile->pluralStateCount == 0) {
|
||||
errorThrow("nplurals must be greater than 0.");
|
||||
}
|
||||
if(localeFile->pluralStateCount > ASSET_LOCALE_FILE_PLURAL_FORM_COUNT) {
|
||||
errorThrow(
|
||||
"nplurals exceeds maximum supported plural forms: %d > %d",
|
||||
localeFile->pluralStateCount,
|
||||
ASSET_LOCALE_FILE_PLURAL_FORM_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
// Expect plural=
|
||||
char_t *pluralStr = strstr(pluralFormsLine, "plural=");
|
||||
if(!pluralStr) {
|
||||
errorThrow("Failed to find plural in Plural-Forms header.");
|
||||
}
|
||||
pluralStr += strlen("plural=");
|
||||
|
||||
// Expect ( [expressions] )
|
||||
char_t *openParen = strchr(pluralStr, '(');
|
||||
char_t *closeParen = strrchr(pluralStr, ')');
|
||||
if(!openParen || !closeParen || closeParen < openParen) {
|
||||
errorThrow("Failed to find plural expression in Plural-Forms header.");
|
||||
}
|
||||
|
||||
// Parse:
|
||||
// n [op] value ? index : n [op] value ? index : ... : final_index
|
||||
char_t *ptr = openParen + 1;
|
||||
uint8_t pluralIndex = 0;
|
||||
uint8_t definedCount = 0;
|
||||
|
||||
while(1) {
|
||||
while(*ptr == ' ') ptr++;
|
||||
|
||||
// Allow grouped subexpressions like:
|
||||
// (n<7 ? 2 : 3)
|
||||
// or
|
||||
// (((3)))
|
||||
uint8_t parenDepth = 0;
|
||||
while(*ptr == '(') {
|
||||
parenDepth++;
|
||||
ptr++;
|
||||
while(*ptr == ' ') ptr++;
|
||||
}
|
||||
|
||||
// Final fallback: just an integer
|
||||
if(*ptr != 'n') {
|
||||
char_t *endPtr = NULL;
|
||||
int32_t finalIndex = (int32_t)strtol(ptr, &endPtr, 10);
|
||||
if(endPtr == ptr) {
|
||||
errorThrow("Expected final plural index.");
|
||||
}
|
||||
ptr = endPtr;
|
||||
|
||||
while(*ptr == ' ') ptr++;
|
||||
while(parenDepth > 0) {
|
||||
if(*ptr != ')') {
|
||||
errorThrow("Expected ')' after final plural index.");
|
||||
}
|
||||
ptr++;
|
||||
parenDepth--;
|
||||
|
||||
while(*ptr == ' ') ptr++;
|
||||
}
|
||||
|
||||
if(*ptr != ')') {
|
||||
errorThrow("Expected ')' at end of plural expression.");
|
||||
}
|
||||
|
||||
if(finalIndex < 0 || finalIndex >= localeFile->pluralStateCount) {
|
||||
errorThrow(
|
||||
"Final plural expression index out of bounds: %d (nplurals: %d)",
|
||||
finalIndex,
|
||||
localeFile->pluralStateCount
|
||||
);
|
||||
}
|
||||
|
||||
localeFile->pluralDefaultIndex = (uint8_t)finalIndex;
|
||||
definedCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
if(pluralIndex >= localeFile->pluralStateCount - 1) {
|
||||
errorThrow(
|
||||
"Too many plural conditions. Expected %d conditional clauses for nplurals=%d.",
|
||||
localeFile->pluralStateCount - 1,
|
||||
localeFile->pluralStateCount
|
||||
);
|
||||
}
|
||||
|
||||
ptr++; // skip 'n'
|
||||
|
||||
while(*ptr == ' ') ptr++;
|
||||
|
||||
// Determine operator
|
||||
assetlocalepluraloperation_t op;
|
||||
if(strncmp(ptr, "==", 2) == 0) {
|
||||
op = ASSET_LOCALE_PLURAL_OP_EQUAL;
|
||||
ptr += 2;
|
||||
} else if(strncmp(ptr, "!=", 2) == 0) {
|
||||
op = ASSET_LOCALE_PLURAL_OP_NOT_EQUAL;
|
||||
ptr += 2;
|
||||
} else if(strncmp(ptr, "<=", 2) == 0) {
|
||||
op = ASSET_LOCALE_PLURAL_OP_LESS_EQUAL;
|
||||
ptr += 2;
|
||||
} else if(strncmp(ptr, ">=", 2) == 0) {
|
||||
op = ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL;
|
||||
ptr += 2;
|
||||
} else if(*ptr == '<') {
|
||||
op = ASSET_LOCALE_PLURAL_OP_LESS;
|
||||
ptr++;
|
||||
} else if(*ptr == '>') {
|
||||
op = ASSET_LOCALE_PLURAL_OP_GREATER;
|
||||
ptr++;
|
||||
} else {
|
||||
errorThrow("Unsupported plural operator.");
|
||||
}
|
||||
|
||||
while(*ptr == ' ') ptr++;
|
||||
|
||||
// Parse the comparitor value
|
||||
char_t *endPtr = NULL;
|
||||
int32_t value = (int32_t)strtol(ptr, &endPtr, 10);
|
||||
if(endPtr == ptr) {
|
||||
errorThrow("Expected value for plural expression.");
|
||||
}
|
||||
ptr = endPtr;
|
||||
|
||||
while(*ptr == ' ') ptr++;
|
||||
|
||||
// Parse ternary operator
|
||||
if(*ptr != '?') {
|
||||
errorThrow("Expected '?' after plural expression.");
|
||||
}
|
||||
ptr++;
|
||||
|
||||
while(*ptr == ' ') ptr++;
|
||||
|
||||
// Parse the indice
|
||||
endPtr = NULL;
|
||||
int32_t index = (int32_t)strtol(ptr, &endPtr, 10);
|
||||
if(endPtr == ptr) {
|
||||
errorThrow("Expected index for plural expression.");
|
||||
}
|
||||
ptr = endPtr;
|
||||
|
||||
if(index < 0 || index >= localeFile->pluralStateCount) {
|
||||
errorThrow(
|
||||
"Plural expression index out of bounds: %d (nplurals: %d)",
|
||||
index,
|
||||
localeFile->pluralStateCount
|
||||
);
|
||||
}
|
||||
|
||||
// Store plural expression.
|
||||
localeFile->pluralIndices[pluralIndex] = (uint8_t)index;
|
||||
localeFile->pluralOps[pluralIndex] = op;
|
||||
localeFile->pluralValues[pluralIndex] = value;
|
||||
pluralIndex++;
|
||||
definedCount++;
|
||||
|
||||
while(*ptr == ' ') ptr++;
|
||||
|
||||
// Close any grouping parens that wrapped this conditional branch
|
||||
while(parenDepth > 0) {
|
||||
if(*ptr != ')') {
|
||||
break;
|
||||
}
|
||||
ptr++;
|
||||
parenDepth--;
|
||||
|
||||
while(*ptr == ' ') ptr++;
|
||||
}
|
||||
|
||||
if(*ptr != ':') {
|
||||
errorThrow("Expected ':' after plural expression.");
|
||||
}
|
||||
ptr++;
|
||||
}
|
||||
|
||||
// Must define exactly nplurals outcomes:
|
||||
// (nplurals - 1) conditional results + 1 final fallback result
|
||||
if(
|
||||
pluralIndex != localeFile->pluralStateCount - 1 ||
|
||||
definedCount != localeFile->pluralStateCount
|
||||
) {
|
||||
errorThrow("Plural expression count does not match nplurals.");
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
uint8_t assetLocaleEvaluatePlural(
|
||||
assetlocalefile_t *file,
|
||||
const int32_t pluralCount
|
||||
) {
|
||||
assertNotNull(file, "Locale file cannot be NULL.");
|
||||
assertTrue(pluralCount >= 0, "Plural count cannot be negative.");
|
||||
|
||||
for(uint8_t i = 0; i < file->pluralStateCount - 1; i++) {
|
||||
int32_t value = file->pluralValues[i];
|
||||
switch(file->pluralOps[i]) {
|
||||
case ASSET_LOCALE_PLURAL_OP_EQUAL:
|
||||
if(pluralCount == value) return file->pluralIndices[i];
|
||||
break;
|
||||
|
||||
case ASSET_LOCALE_PLURAL_OP_NOT_EQUAL:
|
||||
if(pluralCount != value) return file->pluralIndices[i];
|
||||
break;
|
||||
|
||||
case ASSET_LOCALE_PLURAL_OP_LESS:
|
||||
if(pluralCount < value) return file->pluralIndices[i];
|
||||
break;
|
||||
|
||||
case ASSET_LOCALE_PLURAL_OP_LESS_EQUAL:
|
||||
if(pluralCount <= value) return file->pluralIndices[i];
|
||||
break;
|
||||
|
||||
case ASSET_LOCALE_PLURAL_OP_GREATER:
|
||||
if(pluralCount > value) return file->pluralIndices[i];
|
||||
break;
|
||||
|
||||
case ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL:
|
||||
if(pluralCount >= value) return file->pluralIndices[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return file->pluralDefaultIndex;
|
||||
}
|
||||
|
||||
errorret_t assetLocaleLineSkipBlanks(
|
||||
@@ -140,15 +418,15 @@ errorret_t assetLocaleLineUnbuffer(
|
||||
}
|
||||
|
||||
errorret_t assetLocaleGetString(
|
||||
assetfile_t *file,
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
const int32_t pluralIndex,
|
||||
const int32_t pluralCount,
|
||||
char_t *stringBuffer,
|
||||
const size_t stringBufferSize
|
||||
) {
|
||||
assertNotNull(file, "Asset file cannot be NULL.");
|
||||
assertNotNull(messageId, "Message ID cannot be NULL.");
|
||||
assertTrue(pluralIndex >= 0, "Plural index cannot be negative.");
|
||||
assertTrue(pluralCount >= 0, "Plural index cannot be negative.");
|
||||
assertNotNull(stringBuffer, "String buffer cannot be NULL.");
|
||||
assertTrue(stringBufferSize > 0, "String buffer size must be > 0");
|
||||
assetfilelinereader_t reader;
|
||||
@@ -158,16 +436,17 @@ errorret_t assetLocaleGetString(
|
||||
uint8_t msgidPluralBuffer[256];
|
||||
uint8_t readBuffer[1024];
|
||||
uint8_t lineBuffer[1024];
|
||||
uint8_t pluralIndex = 0xFF;
|
||||
|
||||
msgidBuffer[0] = '\0';
|
||||
msgidPluralBuffer[0] = '\0';
|
||||
stringBuffer[0] = '\0';
|
||||
|
||||
// Rewind and start reading lines.
|
||||
errorChain(assetFileRewind(&LOCALE.file));
|
||||
errorChain(assetFileRewind(&file->file));
|
||||
assetFileLineReaderInit(
|
||||
&reader,
|
||||
&LOCALE.file,
|
||||
&file->file,
|
||||
readBuffer,
|
||||
sizeof(readBuffer),
|
||||
lineBuffer,
|
||||
@@ -219,6 +498,13 @@ errorret_t assetLocaleGetString(
|
||||
sizeof(msgidPluralBuffer)
|
||||
);
|
||||
msgidPluralFound = true;
|
||||
|
||||
// At this point we determine the plural index to use by running the
|
||||
// plural formula
|
||||
pluralIndex = assetLocaleEvaluatePlural(
|
||||
file,
|
||||
pluralCount
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -281,5 +567,241 @@ errorret_t assetLocaleGetString(
|
||||
errorThrow("Failed to find msgstr for message ID: %s", messageId);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetLocaleGetStringWithVA(
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
const int32_t pluralIndex,
|
||||
char_t *buffer,
|
||||
const size_t bufferSize,
|
||||
...
|
||||
) {
|
||||
assertNotNull(file, "Asset file cannot be NULL.");
|
||||
assertNotNull(messageId, "Message ID cannot be NULL.");
|
||||
assertNotNull(buffer, "Buffer cannot be NULL.");
|
||||
assertTrue(bufferSize > 0, "Buffer size must be > 0.");
|
||||
assertTrue(pluralIndex >= 0, "Plural cannot be negative.");
|
||||
|
||||
char_t *tempBuffer;
|
||||
tempBuffer = memoryAllocate(bufferSize);
|
||||
errorret_t ret = assetLocaleGetString(
|
||||
file,
|
||||
messageId,
|
||||
pluralIndex,
|
||||
tempBuffer,
|
||||
bufferSize
|
||||
);
|
||||
if(ret.code != ERROR_OK) {
|
||||
memoryFree(tempBuffer);
|
||||
return ret;
|
||||
}
|
||||
|
||||
va_list args;
|
||||
va_start(args, bufferSize);
|
||||
int result = vsnprintf(buffer, bufferSize, tempBuffer, args);
|
||||
va_end(args);
|
||||
memoryFree(tempBuffer);
|
||||
|
||||
if(result < 0) {
|
||||
errorThrow("Failed to format locale string for ID: %s", messageId);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
errorret_t assetLocaleGetStringWithArgs(
|
||||
assetlocalefile_t *file,
|
||||
const char_t *id,
|
||||
const int32_t plural,
|
||||
char_t *buffer,
|
||||
const size_t bufferSize,
|
||||
const assetlocalearg_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(
|
||||
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 != ASSET_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 != ASSET_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 != ASSET_LOCALE_ARG_FLOAT &&
|
||||
args[nextArg].type != ASSET_LOCALE_ARG_INT
|
||||
) {
|
||||
memoryFree(format);
|
||||
errorThrow("Expected float or int locale argument for ID: %s", id);
|
||||
}
|
||||
|
||||
float_t floatValue = (
|
||||
args[nextArg].type == ASSET_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();
|
||||
}
|
||||
@@ -7,26 +7,66 @@
|
||||
|
||||
#pragma once
|
||||
#include "asset/asset.h"
|
||||
#include "locale/localemanager.h"
|
||||
|
||||
#define ASSET_LOCALE_FILE_PLURAL_FORM_COUNT 6
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOCALE_PLURAL_OP_EQUAL,
|
||||
ASSET_LOCALE_PLURAL_OP_NOT_EQUAL,
|
||||
ASSET_LOCALE_PLURAL_OP_LESS,
|
||||
ASSET_LOCALE_PLURAL_OP_LESS_EQUAL,
|
||||
ASSET_LOCALE_PLURAL_OP_GREATER,
|
||||
ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL
|
||||
} assetlocalepluraloperation_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOCALE_ARG_STRING,
|
||||
ASSET_LOCALE_ARG_INT,
|
||||
ASSET_LOCALE_ARG_FLOAT
|
||||
} assetlocaleargtype_t;
|
||||
|
||||
typedef struct {
|
||||
assetlocaleargtype_t type;
|
||||
union {
|
||||
const char_t *stringValue;
|
||||
int32_t intValue;
|
||||
float_t floatValue;
|
||||
};
|
||||
} assetlocalearg_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetlocalepluraloperation_t pluralOps[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT];
|
||||
int32_t pluralValues[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT];
|
||||
int32_t pluralIndices[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT];
|
||||
uint8_t pluralStateCount;
|
||||
uint8_t pluralDefaultIndex;
|
||||
} assetlocalefile_t;
|
||||
|
||||
/**
|
||||
* Handler for locale assets.
|
||||
* Initialize a locale asset file.
|
||||
*
|
||||
* @param file Asset file to load the locale from.
|
||||
* @return Any error that occurs during loading.
|
||||
* @param localeFile The locale file to initialize.
|
||||
* @param path The path to the locale file.
|
||||
* @return An error code if a failure occurs.
|
||||
*/
|
||||
errorret_t assetLocaleLoader(assetfile_t *file);
|
||||
errorret_t assetLocaleFileInit(
|
||||
assetlocalefile_t *localeFile,
|
||||
const char_t *path
|
||||
);
|
||||
|
||||
/**
|
||||
* Loads a locale from the specified path.
|
||||
* Dispose of a locale asset file.
|
||||
*
|
||||
* @param path Path to the locale asset.
|
||||
* @param nothing Nothing yet.
|
||||
* @return Any error that occurs during loading.
|
||||
* @param localeFile The locale file to dispose of.
|
||||
* @return An error code if a failure occurs.
|
||||
*/
|
||||
errorret_t assetLocaleLoad(
|
||||
const char_t *path,
|
||||
void *nothing
|
||||
errorret_t assetLocaleFileDispose(assetlocalefile_t *localeFile);
|
||||
|
||||
errorret_t assetLocaleParseHeader(
|
||||
assetlocalefile_t *localeFile,
|
||||
char_t *headerBuffer,
|
||||
const size_t headerBufferSize
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -65,15 +105,57 @@ errorret_t assetLocaleLineUnbuffer(
|
||||
*
|
||||
* @param file Asset file to test loading from.
|
||||
* @param messageId The message ID to retrieve.
|
||||
* @param pluralIndex The plural index to retrieve.
|
||||
* @param pluralCount Count for formulating the plural variant.
|
||||
* @param stringBuffer Buffer to write the retrieved string to.
|
||||
* @param stringBufferSize Size of the string buffer.
|
||||
* @return Any error that occurs during testing.
|
||||
*/
|
||||
errorret_t assetLocaleGetString(
|
||||
assetfile_t *file,
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
const int32_t pluralIndex,
|
||||
const int32_t pluralCount,
|
||||
char_t *stringBuffer,
|
||||
const size_t stringBufferSize
|
||||
);
|
||||
|
||||
/**
|
||||
* Test function for locale asset loading with a variable argument list.
|
||||
*
|
||||
* @param file Asset file to test loading from.
|
||||
* @param messageId The message ID to retrieve.
|
||||
* @param pluralCount Count for formulating the plural variant.
|
||||
* @param buffer Buffer to write the retrieved string to.
|
||||
* @param bufferSize Size of the buffer.
|
||||
* @param ... Additional arguments for formatting the string.
|
||||
* @return Any error that occurs during testing.
|
||||
*/
|
||||
errorret_t assetLocaleGetStringWithVA(
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
const int32_t pluralCount,
|
||||
char_t *buffer,
|
||||
const size_t bufferSize,
|
||||
...
|
||||
);
|
||||
|
||||
/**
|
||||
* Test function for locale asset loading with a list of arguments.
|
||||
*
|
||||
* @param file Asset file to test loading from.
|
||||
* @param messageId The message ID to retrieve.
|
||||
* @param pluralCount Count for formulating the plural variant.
|
||||
* @param buffer Buffer to write the retrieved string to.
|
||||
* @param bufferSize Size of the buffer.
|
||||
* @param args List of arguments for formatting the string.
|
||||
* @param argCount Number of arguments in the list.
|
||||
* @return Any error that occurs during testing.
|
||||
*/
|
||||
errorret_t assetLocaleGetStringWithArgs(
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
const int32_t pluralCount,
|
||||
char_t *buffer,
|
||||
const size_t bufferSize,
|
||||
const assetlocalearg_t *args,
|
||||
const size_t argCount
|
||||
);
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#include "localemanager.h"
|
||||
#include "util/memory.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
localemanager_t LOCALE;
|
||||
@@ -22,254 +21,20 @@ errorret_t localeManagerInit() {
|
||||
|
||||
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
||||
if(LOCALE.fileOpen) {
|
||||
errorChain(assetFileClose(&LOCALE.file));
|
||||
errorCatch(errorPrint(assetFileDispose(&LOCALE.file)));
|
||||
errorChain(assetLocaleFileDispose(&LOCALE.file));
|
||||
LOCALE.fileOpen = false;
|
||||
}
|
||||
|
||||
// Init the asset file
|
||||
errorChain(assetFileInit(&LOCALE.file, locale->file, NULL, NULL));
|
||||
errorChain(assetLocaleFileInit(&LOCALE.file, locale->file));
|
||||
LOCALE.fileOpen = true;
|
||||
|
||||
// Open the file handle.
|
||||
errorChain(assetFileOpen(&LOCALE.file));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t localeManagerGetText(
|
||||
const char_t *id,
|
||||
char_t *buffer,
|
||||
const size_t bufferSize,
|
||||
const int32_t plural,
|
||||
...
|
||||
) {
|
||||
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.");
|
||||
|
||||
char_t *tempBuffer;
|
||||
tempBuffer = memoryAllocate(bufferSize);
|
||||
errorret_t ret = assetLocaleGetString(
|
||||
&LOCALE.file,
|
||||
id,
|
||||
plural,
|
||||
tempBuffer,
|
||||
bufferSize
|
||||
);
|
||||
if(ret.code != ERROR_OK) {
|
||||
memoryFree(tempBuffer);
|
||||
return ret;
|
||||
}
|
||||
|
||||
va_list args;
|
||||
va_start(args, plural);
|
||||
int result = vsnprintf(buffer, bufferSize, tempBuffer, args);
|
||||
va_end(args);
|
||||
memoryFree(tempBuffer);
|
||||
|
||||
if(result < 0) {
|
||||
errorThrow("Failed to format locale string for ID: %s", id);
|
||||
}
|
||||
|
||||
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) {
|
||||
errorCatch(errorPrint(assetFileClose(&LOCALE.file)));
|
||||
errorCatch(errorPrint(assetFileDispose(&LOCALE.file)));
|
||||
errorCatch(errorPrint(assetLocaleFileDispose(&LOCALE.file)));
|
||||
LOCALE.fileOpen = false;
|
||||
}
|
||||
}
|
||||
@@ -9,29 +9,14 @@
|
||||
#include "error/error.h"
|
||||
#include "localemanager.h"
|
||||
#include "locale/localeinfo.h"
|
||||
#include "asset/assetfile.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
|
||||
typedef struct {
|
||||
const localeinfo_t *locale;
|
||||
assetfile_t file;
|
||||
assetlocalefile_t file;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -59,13 +44,15 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale);
|
||||
* @param ... Additional arguments for formatting the string.
|
||||
* @return An error code if a failure occurs.
|
||||
*/
|
||||
errorret_t localeManagerGetText(
|
||||
const char_t *id,
|
||||
char_t *buffer,
|
||||
const size_t bufferSize,
|
||||
const int32_t plural,
|
||||
...
|
||||
);
|
||||
#define localeManagerGetText(id, buffer, bufferSize, plural, ...) \
|
||||
assetLocaleGetStringWithVA( \
|
||||
&LOCALE.file, \
|
||||
id, \
|
||||
plural, \
|
||||
buffer, \
|
||||
bufferSize, \
|
||||
__VA_ARGS__ \
|
||||
)
|
||||
|
||||
/**
|
||||
* Get a localized string for the given message ID with a list of arguments.
|
||||
@@ -78,14 +65,18 @@ errorret_t localeManagerGetText(
|
||||
* @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
|
||||
);
|
||||
#define localeManagerGetTextArgs( \
|
||||
id, buffer, bufferSize, plural, args, argCount \
|
||||
) \
|
||||
assetLocaleGetStringWithArgs( \
|
||||
&LOCALE.file, \
|
||||
id, \
|
||||
plural, \
|
||||
buffer, \
|
||||
bufferSize, \
|
||||
args, \
|
||||
argCount \
|
||||
)
|
||||
|
||||
/**
|
||||
* Dispose of the locale system.
|
||||
|
||||
@@ -52,8 +52,8 @@ int moduleLocaleGetText(lua_State *L) {
|
||||
// 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;
|
||||
assetlocalearg_t argsStack[MODULE_LOCALE_MAX_ARGS];
|
||||
assetlocalearg_t *args = argsStack;
|
||||
|
||||
if(argCount > MODULE_LOCALE_MAX_ARGS) {
|
||||
luaL_error(L, "Too many args (max %d)", MODULE_LOCALE_MAX_ARGS);
|
||||
@@ -64,13 +64,13 @@ int moduleLocaleGetText(lua_State *L) {
|
||||
int luaIndex = argStart + (int)i;
|
||||
|
||||
if(lua_isinteger(L, luaIndex)) {
|
||||
args[i].type = LOCALE_ARG_INT;
|
||||
args[i].type = ASSET_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].type = ASSET_LOCALE_ARG_FLOAT;
|
||||
args[i].floatValue = lua_tonumber(L, luaIndex);
|
||||
} else if(lua_isstring(L, luaIndex)) {
|
||||
args[i].type = LOCALE_ARG_STRING;
|
||||
args[i].type = ASSET_LOCALE_ARG_STRING;
|
||||
args[i].stringValue = lua_tostring(L, luaIndex);
|
||||
} else {
|
||||
luaL_error(L, "Unsupported localization argument type");
|
||||
|
||||
@@ -82,7 +82,7 @@ int_t memoryCompare(
|
||||
) {
|
||||
assertNotNull(a, "Cannot compare NULL memory.");
|
||||
assertNotNull(b, "Cannot compare NULL memory.");
|
||||
assertTrue(size > 0, "Cannot compare 0 bytes of memory.");
|
||||
assertTrue(size >= 0, "Cannot compare negative bytes of memory.");
|
||||
return memcmp(a, b, size);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user