foxygit / Hush Log in
commits tags

/src/lang.c · 6.43 KB

raw
#include "lang.h"
#include "paths.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

/* File-format key for each StringId (see the .lang files under assets/lang). */
static const char *KEY_NAMES[STR_COUNT] = {
    [STR_UNTITLED] = "untitled",
    [STR_STATUSBAR_LINE] = "statusbar.line",
    [STR_STATUSBAR_LINES] = "statusbar.lines",
    [STR_QUIT_TITLE] = "quit.title",
    [STR_QUIT_SAVE] = "quit.save",
    [STR_QUIT_DISCARD] = "quit.discard",
    [STR_QUIT_CANCEL] = "quit.cancel",
    [STR_QUIT_TITLE_OPEN] = "quit.title_open",
    [STR_QUIT_SAVE_OPEN] = "quit.save_open",
    [STR_QUIT_DISCARD_OPEN] = "quit.discard_open",
    [STR_OPEN_TITLE] = "open.title",
    [STR_OPEN_CONFIRM] = "open.confirm",
    [STR_OPEN_ERROR] = "open.error",
    [STR_HELP_TITLE] = "help.title",
    [STR_HELP_ARROWS] = "help.arrows",
    [STR_HELP_SHIFT_ARROWS] = "help.shift_arrows",
    [STR_HELP_MOUSE_DRAG] = "help.mouse_drag",
    [STR_HELP_SAVE] = "help.save",
    [STR_HELP_OPEN] = "help.open",
    [STR_HELP_COPY_PASTE] = "help.copy_paste",
    [STR_HELP_UNDO_REDO] = "help.undo_redo",
    [STR_HELP_QUIT] = "help.quit",
    [STR_HELP_THIS_HELP] = "help.this_help",
    [STR_HELP_ENTER] = "help.enter",
    [STR_HELP_BACKSPACE_DELETE] = "help.backspace_delete",
    [STR_HELP_HOME_END] = "help.home_end",
    [STR_HELP_SOURCE_VIEW] = "help.source_view",
    [STR_HELP_ZOOM] = "help.zoom",
    [STR_HELP_QUICK_INSERT] = "help.quick_insert",
    [STR_HELP_CLOSE] = "help.close",
    [STR_SOURCE_HINT] = "source_hint",
    [STR_BLOCK_H1] = "block.h1",
    [STR_BLOCK_H2] = "block.h2",
    [STR_BLOCK_H3] = "block.h3",
    [STR_BLOCK_H4] = "block.h4",
    [STR_BLOCK_H5] = "block.h5",
    [STR_BLOCK_H6] = "block.h6",
    [STR_BLOCK_BULLET] = "block.bullet",
    [STR_BLOCK_NUMBERED] = "block.numbered",
    [STR_BLOCK_QUOTE] = "block.quote",
    [STR_BLOCK_CODE] = "block.code",
    [STR_BLOCK_TASK] = "block.task",
    [STR_BLOCK_HR] = "block.hr",
    [STR_BLOCK_IMAGE] = "block.image",
    [STR_BLOCK_TABLE] = "block.table",
    [STR_QUICK_INSERT_EMPTY] = "quick_insert.empty",
};

/* Built-in fallback, used for any key missing from the loaded language file (including when
   no file could be loaded at all). Kept in English as the baseline other translations are
   written against. */
static const char *DEFAULTS[STR_COUNT] = {
    [STR_UNTITLED] = "Untitled",
    [STR_STATUSBAR_LINE] = "line",
    [STR_STATUSBAR_LINES] = "lines",
    [STR_QUIT_TITLE] = "Save changes before quitting?",
    [STR_QUIT_SAVE] = "Save and quit",
    [STR_QUIT_DISCARD] = "Quit without saving",
    [STR_QUIT_CANCEL] = "Cancel",
    [STR_QUIT_TITLE_OPEN] = "Save changes before opening another file?",
    [STR_QUIT_SAVE_OPEN] = "Save and open",
    [STR_QUIT_DISCARD_OPEN] = "Open without saving",
    [STR_OPEN_TITLE] = "Open file",
    [STR_OPEN_CONFIRM] = "Open",
    [STR_OPEN_ERROR] = "Could not open that file.",
    [STR_HELP_TITLE] = "Keyboard shortcuts",
    [STR_HELP_ARROWS] = "Arrow keys \xE2\x80\x94 move the cursor",
    [STR_HELP_SHIFT_ARROWS] = "Shift + arrow keys \xE2\x80\x94 select text",
    [STR_HELP_MOUSE_DRAG] = "Click + drag \xE2\x80\x94 select text",
    [STR_HELP_SAVE] = "Ctrl+S \xE2\x80\x94 Save",
    [STR_HELP_OPEN] = "Ctrl+O \xE2\x80\x94 Open a file",
    [STR_HELP_COPY_PASTE] = "Ctrl+C / Ctrl+V \xE2\x80\x94 Copy / paste",
    [STR_HELP_UNDO_REDO] = "Ctrl+Z / Ctrl+Shift+Z \xE2\x80\x94 Undo / redo",
    [STR_HELP_QUIT] = "Ctrl+X \xE2\x80\x94 Quit",
    [STR_HELP_THIS_HELP] = "F1 \xE2\x80\x94 This help",
    [STR_HELP_ENTER] = "Enter \xE2\x80\x94 New line / new block",
    [STR_HELP_BACKSPACE_DELETE] = "Backspace / Delete \xE2\x80\x94 Delete a character",
    [STR_HELP_HOME_END] = "Home / End \xE2\x80\x94 Start/end of line",
    [STR_HELP_SOURCE_VIEW] = "Ctrl+M \xE2\x80\x94 View raw markdown source",
    [STR_HELP_ZOOM] = "Ctrl++ / Ctrl+- \xE2\x80\x94 Zoom in / out (Ctrl+0 resets)",
    [STR_HELP_QUICK_INSERT] = "\"@\" in an empty paragraph \xE2\x80\x94 insert menu",
    [STR_HELP_CLOSE] = "Close",
    [STR_SOURCE_HINT] = "Raw markdown source \xE2\x80\x94 read-only \xC2\xB7 Ctrl+M or Esc to go back",
    [STR_BLOCK_H1] = "Heading 1",
    [STR_BLOCK_H2] = "Heading 2",
    [STR_BLOCK_H3] = "Heading 3",
    [STR_BLOCK_H4] = "Heading 4",
    [STR_BLOCK_H5] = "Heading 5",
    [STR_BLOCK_H6] = "Heading 6",
    [STR_BLOCK_BULLET] = "Bullet list",
    [STR_BLOCK_NUMBERED] = "Numbered list",
    [STR_BLOCK_QUOTE] = "Quote",
    [STR_BLOCK_CODE] = "Code block",
    [STR_BLOCK_TASK] = "Task list",
    [STR_BLOCK_HR] = "Horizontal rule",
    [STR_BLOCK_IMAGE] = "Image",
    [STR_BLOCK_TABLE] = "Table",
    [STR_QUICK_INSERT_EMPTY] = "No matches",
};

static char *g_strings[STR_COUNT];

static const char *trim(const char *s, int len, int *outLen) {
    while (len > 0 && isspace((unsigned char)s[0])) { s++; len--; }
    while (len > 0 && isspace((unsigned char)s[len - 1])) len--;
    *outLen = len;
    return s;
}

bool lang_parse_line(const char *line, int len) {
    int trimmedLen;
    const char *t = trim(line, len, &trimmedLen);
    if (trimmedLen == 0 || t[0] == '#') return false;

    const char *eq = memchr(t, '=', (size_t)trimmedLen);
    if (!eq) return false;

    int keyLen;
    const char *key = trim(t, (int)(eq - t), &keyLen);
    int valueLen;
    const char *value = trim(eq + 1, (int)(t + trimmedLen - (eq + 1)), &valueLen);
    if (keyLen == 0 || valueLen == 0) return false;

    for (int i = 0; i < STR_COUNT; i++) {
        size_t nameLen = strlen(KEY_NAMES[i]);
        if ((size_t)keyLen == nameLen && memcmp(key, KEY_NAMES[i], nameLen) == 0) {
            free(g_strings[i]);
            g_strings[i] = malloc((size_t)valueLen + 1);
            memcpy(g_strings[i], value, (size_t)valueLen);
            g_strings[i][valueLen] = '\0';
            return true;
        }
    }

    return false;
}

void lang_load(const char *code) {
    if (!code || !code[0]) return;

    char path[1024];
    snprintf(path, sizeof path, "%s/lang/%s.lang", paths_assets_dir(), code);

    FILE *f = fopen(path, "rb");
    if (!f) return;

    char line[1024];
    while (fgets(line, sizeof line, f)) {
        int len = (int)strlen(line);
        while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) len--;
        lang_parse_line(line, len);
    }
    fclose(f);
}

void lang_free(void) {
    for (int i = 0; i < STR_COUNT; i++) {
        free(g_strings[i]);
        g_strings[i] = NULL;
    }
}

const char *lang_get(StringId id) {
    return g_strings[id] ? g_strings[id] : DEFAULTS[id];
}