aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfrosty <gabriel@bwaaa.monster>2026-04-01 00:31:44 +0300
committerfrosty <gabriel@bwaaa.monster>2026-04-01 00:31:44 +0300
commit24f602663878f211143bf892159588aa5b7294e9 (patch)
tree4c58847fdaec9d6d361f51328956b8d926fba0d7
parent7ee1b45af11535ae7e4081d5fabc8eec31715a52 (diff)
downloadbeaker-24f602663878f211143bf892159588aa5b7294e9.tar.gz
feat: beginning work on localisation api
-rw-r--r--beaker.h36
-rw-r--r--src/beaker_globals.c8
-rw-r--r--src/beaker_globals.h6
-rw-r--r--src/l10n.c261
-rw-r--r--src/template.c128
5 files changed, 434 insertions, 5 deletions
diff --git a/beaker.h b/beaker.h
index 96125d3..a5f374e 100644
--- a/beaker.h
+++ b/beaker.h
@@ -20,6 +20,14 @@
#define TEMPLATES_DIR "templates/"
#define STATIC_DIR "static/"
+#define LOCALES_DIR "locales/"
+
+#define INITIAL_LOCALES_CAPACITY 8
+#define INITIAL_LOCALE_KEYS_CAPACITY 16
+#define MAX_LOCALES_HARD 1024
+#define MAX_LOCALE_KEYS_HARD 65536
+#define MAX_LOCALE_ID_LEN 64
+#define MAX_LOCALE_VALUE_LEN 512
typedef enum {
CONTEXT_TYPE_STRING,
@@ -68,6 +76,28 @@ typedef struct {
bool secure;
} Cookie;
+typedef struct {
+ char id[MAX_LOCALE_ID_LEN];
+ char name[MAX_VALUE_LEN];
+ char direction[16];
+} LocaleMeta;
+
+typedef struct {
+ char key[MAX_KEY_LEN];
+ char value[MAX_LOCALE_VALUE_LEN];
+} LocaleKV;
+
+typedef struct {
+ LocaleMeta meta;
+ LocaleKV *keys;
+ int key_count;
+ int key_capacity;
+} Locale;
+
+typedef struct {
+ LocaleMeta meta;
+} LocaleInfo;
+
typedef int (*RequestHandler)(UrlParams *params);
typedef struct {
@@ -85,6 +115,12 @@ void context_set_array_of_arrays(TemplateContext *ctx, const char *key,
void free_context(TemplateContext *ctx);
char *render_template(const char *template_file, TemplateContext *ctx);
+int beaker_load_locales(void);
+void beaker_set_locale(TemplateContext *ctx, const char *locale_id);
+int beaker_get_all_locales(LocaleInfo *out, int max_count);
+const LocaleMeta *beaker_get_locale_meta(const char *locale_id);
+void beaker_free_locales(void);
+
void send_response(const char *html);
void send_redirect(const char *location);
void set_cookie(const char *name, const char *value, const char *expires,
diff --git a/src/beaker_globals.c b/src/beaker_globals.c
index 29c301e..1f81a5e 100644
--- a/src/beaker_globals.c
+++ b/src/beaker_globals.c
@@ -10,4 +10,10 @@ __thread Cookie cookies_to_set[MAX_COOKIES];
__thread int cookies_to_set_count = 0;
-__thread char current_request_buffer[BUFFER_SIZE]; \ No newline at end of file
+__thread char current_request_buffer[BUFFER_SIZE];
+
+Locale *locales = NULL;
+
+int locale_count = 0;
+
+int locale_capacity = 0; \ No newline at end of file
diff --git a/src/beaker_globals.h b/src/beaker_globals.h
index 0682533..7418808 100644
--- a/src/beaker_globals.h
+++ b/src/beaker_globals.h
@@ -16,4 +16,10 @@ extern __thread int cookies_to_set_count;
extern __thread char current_request_buffer[BUFFER_SIZE];
+extern Locale *locales;
+
+extern int locale_count;
+
+extern int locale_capacity;
+
#endif \ No newline at end of file
diff --git a/src/l10n.c b/src/l10n.c
new file mode 100644
index 0000000..f6e748f
--- /dev/null
+++ b/src/l10n.c
@@ -0,0 +1,261 @@
+#include "../beaker.h"
+#include "beaker_globals.h"
+#include <dirent.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef enum {
+ SECTION_NONE,
+ SECTION_META,
+ SECTION_KEYS,
+} IniSection;
+
+static char *trim(char *str) {
+ if (str == NULL) return NULL;
+ while (*str == ' ' || *str == '\t') str++;
+ if (*str == '\0') return str;
+ char *end = str + strlen(str) - 1;
+ while (end > str && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
+ *end = '\0';
+ end--;
+ }
+ return str;
+}
+
+static void unquote_value(char *value) {
+ size_t len = strlen(value);
+ if (len >= 2 && value[0] == '"' && value[len - 1] == '"') {
+ memmove(value, value + 1, len - 2);
+ value[len - 2] = '\0';
+ }
+}
+
+static int safe_grow_capacity(int current, int hard_max) {
+ int new_cap;
+ if (current == 0) {
+ new_cap = (hard_max < INITIAL_LOCALE_KEYS_CAPACITY) ? hard_max : INITIAL_LOCALE_KEYS_CAPACITY;
+ } else {
+ new_cap = current * 2;
+ if (new_cap > hard_max) new_cap = hard_max;
+ }
+ return new_cap;
+}
+
+static int parse_ini_file(const char *path, Locale *locale) {
+ FILE *fp = fopen(path, "r");
+ if (fp == NULL) {
+ fprintf(stderr, "[ERROR] parse_ini_file: Could not open '%s': %s\n",
+ path, strerror(errno));
+ return -1;
+ }
+
+ memset(&locale->meta, 0, sizeof(locale->meta));
+ locale->keys = NULL;
+ locale->key_count = 0;
+ locale->key_capacity = 0;
+ IniSection current_section = SECTION_NONE;
+ char line[BUFFER_SIZE];
+
+ while (fgets(line, sizeof(line), fp) != NULL) {
+ char *trimmed = trim(line);
+
+ if (*trimmed == '\0' || *trimmed == ';' || *trimmed == '#') {
+ continue;
+ }
+
+ if (*trimmed == '[') {
+ char *close = strchr(trimmed, ']');
+ if (close == NULL) continue;
+ *close = '\0';
+ char *section_name = trimmed + 1;
+ section_name = trim(section_name);
+
+ if (strcmp(section_name, "Meta") == 0) {
+ current_section = SECTION_META;
+ } else if (strcmp(section_name, "Keys") == 0) {
+ current_section = SECTION_KEYS;
+ } else {
+ current_section = SECTION_NONE;
+ }
+ continue;
+ }
+
+ char *eq = strchr(trimmed, '=');
+ if (eq == NULL) continue;
+
+ *eq = '\0';
+ char *key = trim(trimmed);
+ char *value = trim(eq + 1);
+ unquote_value(value);
+
+ if (current_section == SECTION_META) {
+ if (strcmp(key, "Id") == 0) {
+ strncpy(locale->meta.id, value, MAX_LOCALE_ID_LEN - 1);
+ locale->meta.id[MAX_LOCALE_ID_LEN - 1] = '\0';
+ } else if (strcmp(key, "Name") == 0) {
+ strncpy(locale->meta.name, value, MAX_VALUE_LEN - 1);
+ locale->meta.name[MAX_VALUE_LEN - 1] = '\0';
+ } else if (strcmp(key, "Direction") == 0) {
+ strncpy(locale->meta.direction, value, sizeof(locale->meta.direction) - 1);
+ locale->meta.direction[sizeof(locale->meta.direction) - 1] = '\0';
+ }
+ } else if (current_section == SECTION_KEYS) {
+ if (locale->key_count >= locale->key_capacity) {
+ int new_cap = safe_grow_capacity(locale->key_capacity, MAX_LOCALE_KEYS_HARD);
+ if (new_cap <= locale->key_count) {
+ fprintf(stderr,
+ "[WARNING] parse_ini_file: Hard key limit (%d) reached in '%s', "
+ "skipping key '%s'.\n",
+ MAX_LOCALE_KEYS_HARD, path, key);
+ continue;
+ }
+ LocaleKV *new_keys = realloc(locale->keys, (size_t)new_cap * sizeof(LocaleKV));
+ if (new_keys == NULL) {
+ fprintf(stderr,
+ "[ERROR] parse_ini_file: Memory allocation failed for keys in '%s'.\n",
+ path);
+ continue;
+ }
+ locale->keys = new_keys;
+ locale->key_capacity = new_cap;
+ }
+ LocaleKV *kv = &locale->keys[locale->key_count];
+ strncpy(kv->key, key, MAX_KEY_LEN - 1);
+ kv->key[MAX_KEY_LEN - 1] = '\0';
+ strncpy(kv->value, value, MAX_LOCALE_VALUE_LEN - 1);
+ kv->value[MAX_LOCALE_VALUE_LEN - 1] = '\0';
+ locale->key_count++;
+ }
+ }
+
+ fclose(fp);
+
+ if (locale->meta.id[0] == '\0') {
+ fprintf(stderr,
+ "[WARNING] parse_ini_file: No Id in [Meta] section of '%s'.\n",
+ path);
+ return -1;
+ }
+
+ return 0;
+}
+
+int beaker_load_locales(void) {
+ locale_count = 0;
+
+ DIR *dir = opendir(LOCALES_DIR);
+ if (dir == NULL) {
+ fprintf(stderr,
+ "[ERROR] beaker_load_locales: Could not open directory '%s': %s\n",
+ LOCALES_DIR, strerror(errno));
+ return 0;
+ }
+
+ struct dirent *entry;
+ while ((entry = readdir(dir)) != NULL) {
+ const char *name = entry->d_name;
+ size_t name_len = strlen(name);
+
+ if (name_len < 5 || strcmp(name + name_len - 4, ".ini") != 0) {
+ continue;
+ }
+
+ if (locale_count >= locale_capacity) {
+ int new_cap = safe_grow_capacity(locale_capacity, MAX_LOCALES_HARD);
+ if (new_cap <= locale_count) {
+ fprintf(stderr,
+ "[WARNING] beaker_load_locales: Hard locale limit (%d) reached, "
+ "skipping '%s'.\n",
+ MAX_LOCALES_HARD, name);
+ break;
+ }
+ Locale *new_locales = realloc(locales, (size_t)new_cap * sizeof(Locale));
+ if (new_locales == NULL) {
+ fprintf(stderr,
+ "[ERROR] beaker_load_locales: Memory allocation failed for locales.\n");
+ break;
+ }
+ locales = new_locales;
+ locale_capacity = new_cap;
+ }
+
+ char path[512];
+ snprintf(path, sizeof(path), "%s%s", LOCALES_DIR, name);
+
+ Locale *locale = &locales[locale_count];
+ if (parse_ini_file(path, locale) == 0) {
+ fprintf(stderr, "[INFO] beaker_load_locales: Loaded locale '%s' from '%s'\n",
+ locale->meta.id, name);
+ locale_count++;
+ } else {
+ free(locale->keys);
+ locale->keys = NULL;
+ locale->key_count = 0;
+ locale->key_capacity = 0;
+ }
+ }
+
+ closedir(dir);
+ return locale_count;
+}
+
+void beaker_set_locale(TemplateContext *ctx, const char *locale_id) {
+ if (ctx == NULL || locale_id == NULL) {
+ fprintf(stderr,
+ "[ERROR] beaker_set_locale: Invalid NULL input (ctx or locale_id).\n");
+ return;
+ }
+
+ context_set(ctx, "__locale", locale_id);
+
+ const LocaleMeta *meta = beaker_get_locale_meta(locale_id);
+ if (meta != NULL) {
+ context_set(ctx, "__locale_id", meta->id);
+ context_set(ctx, "__locale_name", meta->name);
+ context_set(ctx, "__locale_direction", meta->direction);
+ } else {
+ fprintf(stderr,
+ "[WARNING] beaker_set_locale: Locale '%s' not found. Context vars "
+ "not set.\n",
+ locale_id);
+ }
+}
+
+int beaker_get_all_locales(LocaleInfo *out, int max_count) {
+ if (out == NULL || max_count <= 0) {
+ return 0;
+ }
+
+ int count = (locale_count < max_count) ? locale_count : max_count;
+ for (int i = 0; i < count; i++) {
+ out[i].meta = locales[i].meta;
+ }
+ return count;
+}
+
+const LocaleMeta *beaker_get_locale_meta(const char *locale_id) {
+ if (locale_id == NULL) {
+ return NULL;
+ }
+ for (int i = 0; i < locale_count; i++) {
+ if (strcmp(locales[i].meta.id, locale_id) == 0) {
+ return &locales[i].meta;
+ }
+ }
+ return NULL;
+}
+
+void beaker_free_locales(void) {
+ for (int i = 0; i < locale_count; i++) {
+ free(locales[i].keys);
+ locales[i].keys = NULL;
+ locales[i].key_count = 0;
+ locales[i].key_capacity = 0;
+ }
+ free(locales);
+ locales = NULL;
+ locale_count = 0;
+ locale_capacity = 0;
+}
diff --git a/src/template.c b/src/template.c
index 76e8662..6255d1b 100644
--- a/src/template.c
+++ b/src/template.c
@@ -1,4 +1,5 @@
#include "../beaker.h"
+#include "beaker_globals.h"
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
@@ -530,6 +531,7 @@ typedef struct {
ConditionType type;
char var_name[MAX_KEY_LEN];
char compare_value[MAX_VALUE_LEN];
+ int compare_index;
bool negate;
} Condition;
@@ -564,11 +566,45 @@ static bool evaluate_condition(const Condition *cond, TemplateContext *ctx) {
return cond->negate ? !exists : exists;
}
- case CONDITION_EQUAL:
- return (strcmp(var_value, cond->compare_value) == 0);
+ case CONDITION_EQUAL: {
+ const char *compare_value = cond->compare_value;
+ char compare_buf[MAX_VALUE_LEN];
+ ContextVar *compare_var = find_context_var(ctx, cond->compare_value);
+ if (cond->compare_index >= 0) {
+ if (compare_var != NULL &&
+ compare_var->type == CONTEXT_TYPE_STRING_ARRAY &&
+ cond->compare_index >= 0 &&
+ cond->compare_index < compare_var->value.string_array_data.count) {
+ compare_value = compare_var->value.string_array_data
+ .values[cond->compare_index];
+ }
+ } else if (compare_var != NULL && compare_var->type == CONTEXT_TYPE_STRING) {
+ strncpy(compare_buf, compare_var->value.string_val, MAX_VALUE_LEN - 1);
+ compare_buf[MAX_VALUE_LEN - 1] = '\0';
+ compare_value = compare_buf;
+ }
+ return (strcmp(var_value, compare_value) == 0);
+ }
- case CONDITION_NOT_EQUAL:
- return (strcmp(var_value, cond->compare_value) != 0);
+ case CONDITION_NOT_EQUAL: {
+ const char *compare_value = cond->compare_value;
+ char compare_buf[MAX_VALUE_LEN];
+ ContextVar *compare_var = find_context_var(ctx, cond->compare_value);
+ if (cond->compare_index >= 0) {
+ if (compare_var != NULL &&
+ compare_var->type == CONTEXT_TYPE_STRING_ARRAY &&
+ cond->compare_index >= 0 &&
+ cond->compare_index < compare_var->value.string_array_data.count) {
+ compare_value = compare_var->value.string_array_data
+ .values[cond->compare_index];
+ }
+ } else if (compare_var != NULL && compare_var->type == CONTEXT_TYPE_STRING) {
+ strncpy(compare_buf, compare_var->value.string_val, MAX_VALUE_LEN - 1);
+ compare_buf[MAX_VALUE_LEN - 1] = '\0';
+ compare_value = compare_buf;
+ }
+ return (strcmp(var_value, compare_value) != 0);
+ }
default:
return false;
@@ -580,6 +616,7 @@ static Condition parse_condition(const char *condition_str) {
cond.type = CONDITION_NONE;
cond.var_name[0] = '\0';
cond.compare_value[0] = '\0';
+ cond.compare_index = -1;
cond.negate = false;
if (condition_str == NULL || *condition_str == '\0') {
@@ -707,6 +744,15 @@ static Condition parse_condition(const char *condition_str) {
end--;
}
+ char *bracket = strchr(cond.compare_value, '[');
+ if (bracket != NULL) {
+ char *close_bracket = strchr(bracket, ']');
+ if (close_bracket != NULL) {
+ *bracket = '\0';
+ cond.compare_index = atoi(bracket + 1);
+ }
+ }
+
return cond;
}
@@ -1184,6 +1230,80 @@ static char *render_template_segment(const char *template_segment,
}
}
+ else if (strncmp(trimmed_tag_content, "l(\"", 3) == 0) {
+ const char *key_start = trimmed_tag_content + 3;
+ const char *key_end = strstr(key_start, "\")");
+ if (key_end == NULL) {
+ fprintf(stderr,
+ "[ERROR] render_template_segment: Malformed l() tag '%s'. "
+ "Expected l(\"key\"). Appending as-is.\n",
+ trimmed_tag_content);
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, "{{");
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ trimmed_tag_content);
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, "}}");
+ free(tag_content_raw);
+ current_pos = end_tag + 2;
+ continue;
+ }
+
+ size_t key_len = key_end - key_start;
+ char l10n_key[MAX_KEY_LEN];
+ if (key_len >= MAX_KEY_LEN) key_len = MAX_KEY_LEN - 1;
+ strncpy(l10n_key, key_start, key_len);
+ l10n_key[key_len] = '\0';
+
+ bool is_safe = false;
+ const char *after_close = key_end + 2;
+ if (strncmp(after_close, "|safe", 5) == 0) {
+ is_safe = true;
+ }
+
+ const char *value_to_append = NULL;
+
+ ContextVar *locale_var = find_context_var(ctx, "__locale");
+ if (locale_var != NULL && locale_var->type == CONTEXT_TYPE_STRING) {
+ const char *current_locale_id = locale_var->value.string_val;
+ for (int i = 0; i < locale_count; i++) {
+ if (strcmp(locales[i].meta.id, current_locale_id) == 0) {
+ for (int j = 0; j < locales[i].key_count; j++) {
+ if (strcmp(locales[i].keys[j].key, l10n_key) == 0) {
+ value_to_append = locales[i].keys[j].value;
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ if (value_to_append != NULL) {
+ if (is_safe) {
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ value_to_append);
+ } else {
+ char *escaped = html_escape(value_to_append);
+ if (escaped) {
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, escaped);
+ free(escaped);
+ } else {
+ append_to_buffer(&rendered_buffer, &current_len, &max_len,
+ value_to_append);
+ }
+ }
+ } else {
+ fprintf(stderr,
+ "[WARNING] render_template_segment: l10n key '%s' not found "
+ "for current locale. Appending key as-is.\n",
+ l10n_key);
+ append_to_buffer(&rendered_buffer, &current_len, &max_len, l10n_key);
+ }
+
+ free(tag_content_raw);
+ current_pos = end_tag + 2;
+ continue;
+ }
+
else {
bool is_safe = false;
char *processing_tag_content = strdup(trimmed_tag_content);