commit 84b14f73aaf44bd767a3e900834d8b453c29fba9
Author: MrJensK <jens.se@icloud.com>
AuthorDate: Mon Aug 24 19:03:09 2026 +0200
Commit: MrJensK <jens.se@icloud.com>
CommitDate: Mon Aug 24 19:03:09 2026 +0200
Add a translatable language file system
Status bar text and the quit-confirm/shortcuts-help dialogs now go
through a small string table (src/lang.c) loaded from plain-text
assets/lang/<code>.lang files, instead of being hardcoded. Ships with
sv.lang (the previous hardcoded Swedish, still the default) and
en.lang (new; also the compiled-in fallback for any missing key).
Switch with `language = <code>` in the config file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
CMakeLists.txt | 2 +
README.md | 13 ++++++
assets/lang/en.lang | 35 +++++++++++++++
assets/lang/sv.lang | 32 ++++++++++++++
src/config.c | 24 +++++++++++
src/config.h | 1 +
src/editor.c | 3 +-
src/lang.c | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lang.h | 51 ++++++++++++++++++++++
src/main.c | 3 ++
src/render.c | 49 +++++++++++----------
src/test_main.c | 23 ++++++++++
12 files changed, 334 insertions(+), 23 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3cd53c0..d5d20a5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -35,6 +35,7 @@ add_executable(hush
src/theme.c
src/undo.c
src/config.c
+ src/lang.c
src/editor.c
src/render.c
src/markdown_io.c
@@ -64,6 +65,7 @@ add_executable(hush_test
src/theme.c
src/undo.c
src/config.c
+ src/lang.c
src/editor.c
src/render.c
src/markdown_io.c
diff --git a/README.md b/README.md
index 6d0c817..d2f7002 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ Markdown syntax you type (`# `, `**bold**`, `` `code` ``, `- `, `1. `, `> `, ``
- Undo/redo
- A status bar showing the document name, size, and line count
- A config file for font, text color, and background color
+- Translatable UI (Swedish and English included; see [Language](#language) below)
- Full Swedish/Latin-1 character support (åäö, etc.)
## Building
@@ -73,6 +74,18 @@ font_mono_bold_italic = /path/to/mono-bold-italic.ttf
Font paths are optional per style; any left unset fall back to the bundled DejaVu fonts. `text_color` and `bg_color` accept `#RRGGBB` (the `#` is optional).
+## Language
+
+The status bar and the quit/shortcuts dialogs are translated via plain-text files in `assets/lang/` (document content itself is never touched — Hush doesn't translate what *you* write). Two are included: `sv.lang` (Swedish, the default) and `en.lang` (English, also the built-in fallback for any string a translation leaves out).
+
+Set the active language in the config file:
+
+```
+language = en
+```
+
+To add a new language, copy `assets/lang/en.lang` to `assets/lang/<code>.lang`, translate the values (not the keys), and set `language = <code>`. Malformed lines are ignored, same as the main config file. Note that the bundled fonts only cover ASCII and Latin-1 (Western European accented characters); a translation needing other scripts would need a matching font as well (see Configuration above).
+
## Testing
```sh
diff --git a/assets/lang/en.lang b/assets/lang/en.lang
new file mode 100644
index 0000000..85bcd34
--- /dev/null
+++ b/assets/lang/en.lang
@@ -0,0 +1,35 @@
+# Hush language file -- English (built-in default)
+#
+# This file is provided as a complete, up-to-date reference translation: every string hush
+# knows how to show is listed here. To add a new language, copy this file to <code>.lang
+# (e.g. de.lang for German) in this same directory, translate the values, and set
+# `language = <code>` in your hush config file (see the README). Keys are fixed and must not
+# be translated or renamed. Lines starting with # are comments; blank lines are ignored.
+# Save this file as UTF-8.
+
+untitled = Untitled
+
+# Status bar: "<name> · 1.2 KB · 3 <line/lines>"
+statusbar.line = line
+statusbar.lines = lines
+
+# Ctrl+X / window-close quit-confirmation dialog (only shown if there are unsaved changes)
+quit.title = Save changes before quitting?
+quit.save = Save and quit
+quit.discard = Quit without saving
+quit.cancel = Cancel
+
+# F1 / Ctrl+/ shortcuts-help dialog
+help.title = Keyboard shortcuts
+help.arrows = Arrow keys — move the cursor
+help.shift_arrows = Shift + arrow keys — select text
+help.mouse_drag = Click + drag — select text
+help.save = Ctrl+S — Save
+help.copy_paste = Ctrl+C / Ctrl+V — Copy / paste
+help.undo_redo = Ctrl+Z / Ctrl+Shift+Z — Undo / redo
+help.quit = Ctrl+X — Quit
+help.this_help = F1 — This help
+help.enter = Enter — New line / new block
+help.backspace_delete = Backspace / Delete — Delete a character
+help.home_end = Home / End — Start/end of line
+help.close = Close
diff --git a/assets/lang/sv.lang b/assets/lang/sv.lang
new file mode 100644
index 0000000..1d39c36
--- /dev/null
+++ b/assets/lang/sv.lang
@@ -0,0 +1,32 @@
+# Hush språkfil -- svenska
+#
+# Se assets/lang/en.lang för en fullständig referens över alla tillgängliga nycklar (nycklarna
+# själva ska inte översättas). Rader som börjar med # är kommentarer; tomma rader ignoreras.
+# Spara filen som UTF-8.
+
+untitled = Namnlös
+
+# Statusfältet: "<namn> · 1,2 KB · 3 <rad/rader>"
+statusbar.line = rad
+statusbar.lines = rader
+
+# Bekräftelsedialog vid Ctrl+X / stängning av fönstret (visas bara om det finns osparade ändringar)
+quit.title = Spara ändringar innan du avslutar?
+quit.save = Spara och avsluta
+quit.discard = Avsluta utan att spara
+quit.cancel = Avbryt
+
+# Genvägshjälp vid F1 / Ctrl+/
+help.title = Kortkommandon
+help.arrows = Piltangenter — flytta markören
+help.shift_arrows = Shift + piltangenter — markera text
+help.mouse_drag = Musklick + drag — markera text
+help.save = Ctrl+S — Spara
+help.copy_paste = Ctrl+C / Ctrl+V — Kopiera / klistra in
+help.undo_redo = Ctrl+Z / Ctrl+Shift+Z — Ångra / gör om
+help.quit = Ctrl+X — Avsluta
+help.this_help = F1 — Den här hjälpen
+help.enter = Enter — Ny rad / nytt block
+help.backspace_delete = Backspace / Delete — Ta bort tecken
+help.home_end = Home / End — Radens början/slut
+help.close = Stäng
diff --git a/src/config.c b/src/config.c
index 0ec6bc2..8c7f03d 100644
--- a/src/config.c
+++ b/src/config.c
@@ -11,10 +11,20 @@ static const char *FONT_KEY_NAMES[FONT_COUNT] = {
"font_mono_regular", "font_mono_bold", "font_mono_italic", "font_mono_bold_italic",
};
+#define DEFAULT_LANGUAGE "sv"
+
+static char *dup_str(const char *s) {
+ size_t n = strlen(s);
+ char *copy = malloc(n + 1);
+ memcpy(copy, s, n + 1);
+ return copy;
+}
+
static void config_set_defaults(Config *cfg) {
cfg->textColor = DEFAULT_COL_TEXT;
cfg->bgColor = DEFAULT_COL_BG;
for (int i = 0; i < FONT_COUNT; i++) cfg->fontPaths[i] = NULL;
+ cfg->language = dup_str(DEFAULT_LANGUAGE);
}
void config_free(Config *cfg) {
@@ -22,6 +32,8 @@ void config_free(Config *cfg) {
free(cfg->fontPaths[i]);
cfg->fontPaths[i] = NULL;
}
+ free(cfg->language);
+ cfg->language = NULL;
}
static const char *trim(const char *s, int len, int *outLen) {
@@ -70,6 +82,13 @@ bool config_parse_line(const char *line, int len, Config *cfg) {
if (keyLen == 10 && memcmp(key, "text_color", 10) == 0) return parse_hex_color(value, valueLen, &cfg->textColor);
if (keyLen == 8 && memcmp(key, "bg_color", 8) == 0) return parse_hex_color(value, valueLen, &cfg->bgColor);
+ if (keyLen == 8 && memcmp(key, "language", 8) == 0) {
+ free(cfg->language);
+ cfg->language = malloc((size_t)valueLen + 1);
+ memcpy(cfg->language, value, (size_t)valueLen);
+ cfg->language[valueLen] = '\0';
+ return true;
+ }
for (int i = 0; i < FONT_COUNT; i++) {
size_t nameLen = strlen(FONT_KEY_NAMES[i]);
@@ -119,6 +138,11 @@ static void write_default_config(const char *path) {
"# Uncomment and edit any line below to override the default. Unknown or malformed\n"
"# lines are ignored, so it's safe to experiment.\n"
"\n"
+ "# Language for the status bar and the quit/shortcuts dialogs. See assets/lang/ next to\n"
+ "# the hush binary for available codes (or add your own -- copy en.lang, translate it,\n"
+ "# name it <code>.lang, and set that code here).\n"
+ "# language = " DEFAULT_LANGUAGE "\n"
+ "\n"
"# text_color = #%02X%02X%02X\n"
"# bg_color = #%02X%02X%02X\n"
"\n"
diff --git a/src/config.h b/src/config.h
index 5ca17ac..5bcd3dd 100644
--- a/src/config.h
+++ b/src/config.h
@@ -9,6 +9,7 @@ typedef struct {
Clay_Color textColor;
Clay_Color bgColor;
char *fontPaths[FONT_COUNT]; /* malloc'd override path per slot, or NULL = bundled default */
+ char *language; /* malloc'd language code (e.g. "sv"), passed to lang_load(); never NULL */
} Config;
/* Fills `out` with the built-in defaults, then overlays whatever is set in the user's config
diff --git a/src/editor.c b/src/editor.c
index 0126c0b..50a6ce5 100644
--- a/src/editor.c
+++ b/src/editor.c
@@ -2,6 +2,7 @@
#include "markdown_io.h"
#include "hittest.h"
#include "utf8.h"
+#include "lang.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
@@ -62,7 +63,7 @@ bool editor_save(EditorState *ed) {
}
const char *editor_display_name(const EditorState *ed) {
- if (!ed->filePath) return "Untitled";
+ if (!ed->filePath) return lang_get(STR_UNTITLED);
const char *slash = strrchr(ed->filePath, '/');
return slash ? slash + 1 : ed->filePath;
}
diff --git a/src/lang.c b/src/lang.c
new file mode 100644
index 0000000..74cb0be
--- /dev/null
+++ b/src/lang.c
@@ -0,0 +1,121 @@
+#include "lang.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_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_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_CLOSE] = "help.close",
+};
+
+/* 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_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_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_CLOSE] = "Close",
+};
+
+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", 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];
+}
diff --git a/src/lang.h b/src/lang.h
new file mode 100644
index 0000000..caae546
--- /dev/null
+++ b/src/lang.h
@@ -0,0 +1,51 @@
+#ifndef HUSH_LANG_H
+#define HUSH_LANG_H
+
+#include <stdbool.h>
+
+/* All user-facing UI text (status bar, quit-confirm and shortcuts-help modals) goes through
+ this table, so a translation only requires a new assets/lang/<code>.lang file -- no source
+ changes. Every StringId has a compiled-in English fallback (see lang.c), used for any key
+ missing from the loaded language file, so a partial translation never shows blank text. */
+typedef enum {
+ STR_UNTITLED,
+ STR_STATUSBAR_LINE,
+ STR_STATUSBAR_LINES,
+ STR_QUIT_TITLE,
+ STR_QUIT_SAVE,
+ STR_QUIT_DISCARD,
+ STR_QUIT_CANCEL,
+ STR_HELP_TITLE,
+ STR_HELP_ARROWS,
+ STR_HELP_SHIFT_ARROWS,
+ STR_HELP_MOUSE_DRAG,
+ STR_HELP_SAVE,
+ STR_HELP_COPY_PASTE,
+ STR_HELP_UNDO_REDO,
+ STR_HELP_QUIT,
+ STR_HELP_THIS_HELP,
+ STR_HELP_ENTER,
+ STR_HELP_BACKSPACE_DELETE,
+ STR_HELP_HOME_END,
+ STR_HELP_CLOSE,
+ STR_COUNT,
+} StringId;
+
+/* Loads assets/lang/<code>.lang (e.g. code="sv" -> ASSETS_DIR "/lang/sv.lang"). Any key not
+ present in the file (or if the file can't be opened at all, e.g. an unknown language code)
+ silently keeps the built-in English default for that string -- never fatal. Call once at
+ startup, before the main loop; the loaded strings live for the rest of the process. */
+void lang_load(const char *code);
+void lang_free(void);
+
+/* Returns the current text for `id` -- the loaded translation if set, else the English
+ default. The returned pointer is valid for the remaining lifetime of the process, so it's
+ safe to hand directly to Clay without a per-frame copy. */
+const char *lang_get(StringId id);
+
+/* Exposed for headless unit testing of the line parser without touching a real file. Returns
+ true if the line set a recognized key; false for comments/blank/unrecognized/malformed
+ lines (never fatal, same contract as config_parse_line). */
+bool lang_parse_line(const char *line, int len);
+
+#endif
diff --git a/src/main.c b/src/main.c
index f6c7342..31758eb 100644
--- a/src/main.c
+++ b/src/main.c
@@ -7,6 +7,7 @@
#include "theme.h"
#include "app_mode.h"
#include "config.h"
+#include "lang.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
@@ -32,6 +33,7 @@ static void update_window_title(EditorState *ed) {
int main(int argc, char **argv) {
Config cfg;
config_load(&cfg); /* pure file I/O, no raylib dependency -- safe to run before window init */
+ lang_load(cfg.language);
theme_init_defaults();
g_theme.bg = cfg.bgColor;
g_theme.text = cfg.textColor;
@@ -178,6 +180,7 @@ int main(int argc, char **argv) {
layout_cache_free(&cache);
editor_free(&ed);
fonts_unload(fonts);
+ lang_free();
Clay_Raylib_Close();
return 0;
}
diff --git a/src/render.c b/src/render.c
index 675a516..e064da0 100644
--- a/src/render.c
+++ b/src/render.c
@@ -6,6 +6,7 @@
#include "wrap.h"
#include "hittest.h"
#include "markdown_io.h"
+#include "lang.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
@@ -314,6 +315,12 @@ static void modal_text_line(const char *text) {
CLAY_TEXT(s, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = FS_MODAL, .textColor = g_theme.text, .wrapMode = CLAY_TEXT_WRAP_NONE }));
}
+/* A modal's title line (bold, otherwise styled like modal_text_line). */
+static void modal_text_line_bold(const char *text) {
+ Clay_String s = { .isStaticallyAllocated = true, .length = (int)strlen(text), .chars = text };
+ CLAY_TEXT(s, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_BOLD, .fontSize = FS_MODAL, .textColor = g_theme.text, .wrapMode = CLAY_TEXT_WRAP_NONE }));
+}
+
/* A clickable modal button: highlights on hover, and sets *action to `thisAction` on click. */
static void modal_button(Clay_ElementId id, const char *label, ModalAction *action, ModalAction thisAction) {
bool hovered = Clay_PointerOver(id);
@@ -364,26 +371,24 @@ static ModalAction emit_modal(AppMode mode, float winW, float winH) {
.border = { .color = COL_MODAL_BORDER, .width = CLAY_BORDER_OUTSIDE(1) },
}) {
if (mode == MODE_QUIT_CONFIRM) {
- CLAY_TEXT(CLAY_STRING("Spara \xC3\xA4ndringar innan du avslutar?"),
- CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_BOLD, .fontSize = FS_MODAL, .textColor = g_theme.text }));
- modal_button(CLAY_ID("BtnSaveQuit"), "Spara och avsluta", &action, MODAL_ACTION_SAVE_QUIT);
- modal_button(CLAY_ID("BtnDiscardQuit"), "Avsluta utan att spara", &action, MODAL_ACTION_DISCARD_QUIT);
- modal_button(CLAY_ID("BtnCancel"), "Avbryt", &action, MODAL_ACTION_CANCEL);
+ modal_text_line_bold(lang_get(STR_QUIT_TITLE));
+ modal_button(CLAY_ID("BtnSaveQuit"), lang_get(STR_QUIT_SAVE), &action, MODAL_ACTION_SAVE_QUIT);
+ modal_button(CLAY_ID("BtnDiscardQuit"), lang_get(STR_QUIT_DISCARD), &action, MODAL_ACTION_DISCARD_QUIT);
+ modal_button(CLAY_ID("BtnCancel"), lang_get(STR_QUIT_CANCEL), &action, MODAL_ACTION_CANCEL);
} else {
- CLAY_TEXT(CLAY_STRING("Kortkommandon"),
- CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_BOLD, .fontSize = FS_MODAL, .textColor = g_theme.text }));
- modal_text_line("Piltangenter \xE2\x80\x94 flytta mark\xC3\xB6ren");
- modal_text_line("Shift + piltangenter \xE2\x80\x94 markera text");
- modal_text_line("Musklick + drag \xE2\x80\x94 markera text");
- modal_text_line("Ctrl+S \xE2\x80\x94 Spara");
- modal_text_line("Ctrl+C / Ctrl+V \xE2\x80\x94 Kopiera / klistra in");
- modal_text_line("Ctrl+Z / Ctrl+Shift+Z \xE2\x80\x94 \xC3\x85ngra / g\xC3\xB6r om");
- modal_text_line("Ctrl+X \xE2\x80\x94 Avsluta");
- modal_text_line("F1 \xE2\x80\x94 Den h\xC3\xA4r hj\xC3\xA4lpen");
- modal_text_line("Enter \xE2\x80\x94 Ny rad / nytt block");
- modal_text_line("Backspace / Delete \xE2\x80\x94 Ta bort tecken");
- modal_text_line("Home / End \xE2\x80\x94 Radens b\xC3\xB6rjan/slut");
- modal_button(CLAY_ID("BtnCloseHelp"), "St\xC3\xA4ng", &action, MODAL_ACTION_CLOSE_HELP);
+ modal_text_line_bold(lang_get(STR_HELP_TITLE));
+ modal_text_line(lang_get(STR_HELP_ARROWS));
+ modal_text_line(lang_get(STR_HELP_SHIFT_ARROWS));
+ modal_text_line(lang_get(STR_HELP_MOUSE_DRAG));
+ modal_text_line(lang_get(STR_HELP_SAVE));
+ modal_text_line(lang_get(STR_HELP_COPY_PASTE));
+ modal_text_line(lang_get(STR_HELP_UNDO_REDO));
+ modal_text_line(lang_get(STR_HELP_QUIT));
+ modal_text_line(lang_get(STR_HELP_THIS_HELP));
+ modal_text_line(lang_get(STR_HELP_ENTER));
+ modal_text_line(lang_get(STR_HELP_BACKSPACE_DELETE));
+ modal_text_line(lang_get(STR_HELP_HOME_END));
+ modal_button(CLAY_ID("BtnCloseHelp"), lang_get(STR_HELP_CLOSE), &action, MODAL_ACTION_CLOSE_HELP);
}
}
}
@@ -454,9 +459,9 @@ ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float
const char *name = editor_display_name(ed);
char tmp[512];
- int n = snprintf(tmp, sizeof tmp, "%s%s \xC2\xB7 %.1f KB \xC2\xB7 %d line%s",
- ed->dirty ? "*" : "", name, stats.byteSize / 1024.0,
- stats.lineCount, stats.lineCount == 1 ? "" : "s");
+ int n = snprintf(tmp, sizeof tmp, "%s%s \xC2\xB7 %.1f KB \xC2\xB7 %d %s",
+ ed->dirty ? "*" : "", name, stats.byteSize / 1024.0, stats.lineCount,
+ lang_get(stats.lineCount == 1 ? STR_STATUSBAR_LINE : STR_STATUSBAR_LINES));
if (n > (int)sizeof tmp) n = (int)sizeof tmp;
char *scratchBuf = scratch_alloc(n);
memcpy(scratchBuf, tmp, (size_t)n);
diff --git a/src/test_main.c b/src/test_main.c
index 9e42c58..e39f806 100644
--- a/src/test_main.c
+++ b/src/test_main.c
@@ -6,6 +6,7 @@
#include "inline_parse.h"
#include "undo.h"
#include "config.h"
+#include "lang.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
@@ -412,6 +413,27 @@ static void test_config_parse_line(void) {
config_free(&cfg);
}
+static void test_lang_parse_line(void) {
+ CHECK(strcmp(lang_get(STR_HELP_CLOSE), "Close") == 0, "before loading anything, lang_get returns the built-in English default");
+
+ CHECK(!lang_parse_line("# a comment", 11), "a comment line returns false");
+ CHECK(!lang_parse_line("", 0), "a blank line returns false");
+ CHECK(!lang_parse_line("unknown.key = foo", 18), "an unrecognized key returns false and does not crash");
+
+ const char *l1 = "help.close = St\xC3\xA4ng";
+ CHECK(lang_parse_line(l1, (int)strlen(l1)), "a recognized key returns true");
+ CHECK(strcmp(lang_get(STR_HELP_CLOSE), "St\xC3\xA4ng") == 0, "lang_get now returns the overridden value");
+ CHECK(strcmp(lang_get(STR_HELP_TITLE), "Keyboard shortcuts") == 0,
+ "a string not present in the override still falls back to its English default");
+
+ const char *l2 = "help.close=Fermer";
+ CHECK(lang_parse_line(l2, (int)strlen(l2)), "no spaces around '=' still parses");
+ CHECK(strcmp(lang_get(STR_HELP_CLOSE), "Fermer") == 0, "re-setting the same key overwrites the previous override");
+
+ lang_free();
+ CHECK(strcmp(lang_get(STR_HELP_CLOSE), "Close") == 0, "lang_free() clears overrides back to the English defaults");
+}
+
int main(void) {
test_heading_autoformat();
test_bullet_list_continuation();
@@ -433,6 +455,7 @@ int main(void) {
test_selection_replace_on_type();
test_selection_replace_on_backspace_across_blocks();
test_config_parse_line();
+ test_lang_parse_line();
printf("\n%s (%d failure%s)\n", g_failures == 0 ? "ALL PASS" : "SOME FAILED", g_failures, g_failures == 1 ? "" : "s");
return g_failures == 0 ? 0 : 1;