/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "assetlocaleloader.h" #include "util/memory.h" #include "util/math.h" #include "util/string.h" #include "assert/assert.h" 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 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 ) { 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( assetfilelinereader_t *reader, uint8_t *lineBuffer ) { while(!reader->eof) { // Skip blank lines if(lineBuffer[0] == '\0') { errorChain(assetFileLineReaderNext(reader)); continue; } // Skip comment lines if(lineBuffer[0] == '#') { errorChain(assetFileLineReaderNext(reader)); continue; } // Is line only spaces? size_t lineLength = strlen((char_t *)lineBuffer); size_t i; bool_t onlySpaces = true; for(i = 0; i < lineLength; i++) { if(lineBuffer[i] != ' ') { onlySpaces = false; break; } } if(onlySpaces) { errorChain(assetFileLineReaderNext(reader)); continue; } break; } errorOk(); } errorret_t assetLocaleLineUnbuffer( assetfilelinereader_t *reader, uint8_t *lineBuffer, uint8_t *stringBuffer, const size_t stringBufferSize ) { stringBuffer[0] = '\0'; // At the point this funciton is called, we are looking for an opening quote. char_t *start = strchr((char_t *)lineBuffer, '"'); if(!start) { errorThrow("Expected open (0) \""); } char *end = strchr(start + 1, '"'); if(!end) { errorThrow("Expected close (0) \""); } *end = '\0'; if(strlen(start) >= stringBufferSize) { errorThrow("String buffer overflow"); } memoryCopy(stringBuffer, start + 1, strlen(start)); // Now start buffering lines out while(!reader->eof) { errorChain(assetFileLineReaderNext(reader)); // Skip blank lines errorChain(assetLocaleLineSkipBlanks(reader, lineBuffer)); // Skip starting spaces char_t *ptr = (char_t *)lineBuffer; while(*ptr == ' ') { ptr++; } // Only consider lines starting with quote if(*ptr != '"') { break; } ptr++; // move past first quote bool_t escaping = false; char_t *dest = (char_t *)stringBuffer + strlen((char_t *)stringBuffer); while(*ptr) { if(escaping) { // Handle escape sequences switch(*ptr) { case 'n': *dest++ = '\n'; break; case 't': *dest++ = '\t'; break; case '\\': *dest++ = '\\'; break; case '"': *dest++ = '"'; break; default: errorThrow("Unknown escape sequence: \\%c", *ptr); } escaping = false; } else if(*ptr == '\\') { escaping = true; } else if(*ptr == '"') { // End of string break; } else { // Regular character *dest++ = *ptr; } if((size_t)(dest - (char_t *)stringBuffer) >= stringBufferSize) { errorThrow("String buffer overflow"); } ptr++; } *dest = '\0'; } errorOk(); } errorret_t assetLocaleGetString( assetlocalefile_t *file, const char_t *messageId, 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(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; bool_t msgidFound = false, msgidPluralFound = false, msgstrFound = false; uint8_t msgidBuffer[256]; 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(&file->file)); assetFileLineReaderInit( &reader, &file->file, readBuffer, sizeof(readBuffer), lineBuffer, sizeof(lineBuffer) ); // Skip blanks, comments, etc and start looking for msgid's errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer)); while(!reader.eof) { // Is this msgid? if(memoryCompare(lineBuffer, "msgid", 5) != 0) { errorChain(assetFileLineReaderNext(&reader)); msgidBuffer[0] = '\0'; continue; } // Unbuffer the msgid assetLocaleLineUnbuffer( &reader, lineBuffer, (uint8_t *)msgidBuffer, sizeof(msgidBuffer) ); // Is this the needle? if(memoryCompare(msgidBuffer, messageId, strlen(messageId)) != 0) { continue; } msgidFound = true; break; } if(!msgidFound) { errorThrow("Failed to find message ID: %s", messageId); } // We are either going to see a msgstr or a msgid_plural while(!reader.eof) { errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer)); // Is msgid_plural? if( !msgidPluralFound && memoryCompare(lineBuffer, "msgid_plural", 12) == 0 ) { // Yes, start reading plural ID assetLocaleLineUnbuffer( &reader, lineBuffer, (uint8_t *)msgidPluralBuffer, sizeof(msgidPluralBuffer) ); msgidPluralFound = true; // At this point we determine the plural index to use by running the // plural formula pluralIndex = assetLocaleEvaluatePlural( file, pluralCount ); continue; } // Should be msgstr if not plural. if(memoryCompare(lineBuffer, "msgstr", 6) != 0) { errorThrow("Expected msgstr after msgid, found: %s", lineBuffer); continue; } // If plural we need an index if(msgidPluralFound) { // Skip blank chars char_t *ptr = (char_t *)lineBuffer + 6; while(*ptr == ' ') { ptr++; } if(*ptr != '[') { errorThrow("Expected [ for plural form, found: %s", lineBuffer); } ptr++; // move past [ // Parse until ] char *end = strchr(ptr, ']'); if(!end) { errorThrow("Expected ] for plural form, found: %s", lineBuffer); } *end = '\0'; int32_t index = atoi(ptr); if(index != pluralIndex) { // Not the plural form we want, skip to the next useable line while(!reader.eof) { errorChain(assetFileLineReaderNext(&reader)); errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer)); if( lineBuffer[0] == '\"' || lineBuffer[0] == '\0' || lineBuffer[0] == '#' ) continue; break; } continue; } // Undo damage to line buffer from unbuffering. *end = ']'; } // Parse out msgstr errorChain(assetLocaleLineUnbuffer( &reader, lineBuffer, (uint8_t *)stringBuffer, stringBufferSize )); msgstrFound = true; break; }; if(!msgstrFound) { 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(); }