diff --git a/Makefile b/Makefile index f2bc34b..138357e 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ QUIRC_M68K_OBJS := $(patsubst src/qr/%.c,$(BUILD)/qr-m68k/%.o,$(QUIRC_SRCS)) QRCODEGEN_HOST_OBJ := $(BUILD)/qr-host/qrcodegen.o QRCODEGEN_M68K_OBJ := $(BUILD)/qr-m68k/qrcodegen.o -.PHONY: all test cli smoke diff m68k m68k-docker gui gui-docker gui-smoke qr-onhw qr-onhw-docker qr-onhw-smoke arexx-onhw arexx-onhw-docker arexx-onhw-smoke serialtest-m68k serialtest-m68k-docker copperline-smoke pbkdf2-bench asm-bench amissl-bench clean +.PHONY: all test cli smoke diff m68k m68k-docker gui gui-docker gui-smoke qr-onhw qr-onhw-docker qr-onhw-smoke arexx-onhw arexx-onhw-docker arexx-onhw-smoke serialtest-m68k serialtest-m68k-docker copperline-smoke pbkdf2-bench asm-bench amissl-bench flexcat flexcat-docker catalog-strings catalog-strings-docker catalog-onhw-smoke catalog-nolib-onhw catalog-nolib-onhw-docker clean all: test cli @@ -140,6 +140,21 @@ m68k: $(QRCODEGEN_M68K_OBJ) | $(BUILD) $(M68K_CC) $(M68K_CFLAGS) -Isrc/qr $(CORE_SRCS) $(ASM_SRCS) $(AMIGA_SRCS) $(CLI_SRCS) \ $(QRENC_WRAP) $(QRCODEGEN_M68K_OBJ) -o $(BUILD)/AmiAuth +# Test-only CLI variant (#67): built with a bogus locale.library name, so +# catalog_open()'s OpenLibrary() genuinely fails and the on-target catalog +# test can exercise the LocaleBase==NULL fallback path without deleting the +# real locale.library from a WB clone (which breaks unrelated boot +# components - see tests/gui/catalog-onhw.sh). Never shipped. +CATALOG_NOLIB_DEFS := -DAMIAUTH_LOCALE_LIBNAME='"locale.library.nonexistent"' + +catalog-nolib-onhw: $(QRCODEGEN_M68K_OBJ) | $(BUILD) + $(M68K_CC) $(M68K_CFLAGS) $(CATALOG_NOLIB_DEFS) -Isrc/qr $(CORE_SRCS) $(ASM_SRCS) $(AMIGA_SRCS) $(CLI_SRCS) \ + $(QRENC_WRAP) $(QRCODEGEN_M68K_OBJ) -o $(BUILD)/AmiAuth-nolib + +catalog-nolib-onhw-docker: + $(DOCKER) run --rm --platform linux/amd64 $(DOCKER_USER) -v "$(CURDIR)":/work -w /work \ + $(AMIGA_GCC_IMAGE) sh -lc 'PATH=/opt/amiga/bin:$$PATH make catalog-nolib-onhw' + # --- m68k: ReAction GUI binary (Amiga only; needs intuition + ReAction classes) --- # Includes the QR decoder: qrimage.c (datatypes glue) + our qr.c wrapper + the # vendored quirc objects (built -w for m68k). QUIRC_FLOAT_TYPE=float: no FPU. @@ -159,6 +174,36 @@ gui-docker: $(DOCKER) run --rm --platform linux/amd64 $(DOCKER_USER) -v "$(CURDIR)":/work -w /work \ $(AMIGA_GCC_IMAGE) sh -lc 'PATH=/opt/amiga/bin:$$PATH make gui GIT_HASH=$(GIT_HASH) GIT_ON_TAG=$(GIT_ON_TAG)' +# --- Localization (#67): FlexCat (github.com/adtools/flexcat) generates +# src/core/catalog_strings.h from locale/AmiAuth.cd. FlexCat itself is a +# host-only build tool (never ships, never touches the m68k cross-compiler - +# see tools/fetch-flexcat.sh) and the generated header is checked into git +# like any other source file, so ordinary `make test`/`cli`/`m68k`/`gui` +# builds need neither FlexCat nor network access. Only re-run `make +# catalog-strings` (regenerating + committing the header) after editing +# locale/AmiAuth.cd. Needs a native (non-cross) C compiler on PATH - macOS's +# clang chokes on FlexCat's own build flags, so use `flexcat-docker` there. +flexcat: $(BUILD)/flexcat + +$(BUILD)/flexcat: | $(BUILD) + src=$$(bash tools/fetch-flexcat.sh) && \ + cp "$$src" $(BUILD)/flexcat && chmod +x $(BUILD)/flexcat && \ + cp "$$(dirname $$src)/../sd/CatComp_h.sd" $(BUILD)/CatComp_h.sd + +flexcat-docker: + $(DOCKER) run --rm --platform linux/amd64 $(DOCKER_USER) -v "$(CURDIR)":/work -w /work \ + $(AMIGA_GCC_IMAGE) sh -lc 'make flexcat' + +catalog-strings: $(BUILD)/flexcat + $(BUILD)/flexcat locale/AmiAuth.cd src/core/catalog_strings.h=$(BUILD)/CatComp_h.sd + +# $(BUILD)/flexcat is a Linux ELF binary (built inside the container) - won't +# run directly on a macOS host, so regenerating the header locally on macOS +# needs the whole step done inside the container, not just the build half. +catalog-strings-docker: + $(DOCKER) run --rm --platform linux/amd64 $(DOCKER_USER) -v "$(CURDIR)":/work -w /work \ + $(AMIGA_GCC_IMAGE) sh -lc 'make catalog-strings' + # --- Headless GUI smoke test: boot WB 3.2 under Copperline, render AmiAuthGUI -- # Boots an A1200/OS 3.2 under native Copperline, auto-launches AmiAuthGUI, and # asserts the ReAction window rendered (screenshot in build/gui-smoke/). No VNC @@ -240,6 +285,14 @@ arexx-onhw-docker: arexx-onhw-smoke: sh tests/gui/arexx-onhw.sh +# --- Headless on-target catalog test (#67): boot WB 3.2, run AmiAuth CLI +# commands whose output is routed through MSG()/catalog_get(), with +# locale.library both present (no matching catalog - the common case) and +# entirely absent, asserting the correct English default either way. See +# tests/gui/catalog-onhw.sh for what this does/doesn't cover. +catalog-onhw-smoke: + sh tests/gui/catalog-onhw.sh + # Measure PBKDF2 throughput on a stock 68000 (informs the KDF policy). Dev-only: # needs a Kickstart ROM (timer.device EClock isn't available under AROS). pbkdf2-bench: diff --git a/locale/AmiAuth.cd b/locale/AmiAuth.cd new file mode 100644 index 0000000..9b80927 --- /dev/null +++ b/locale/AmiAuth.cd @@ -0,0 +1,81 @@ +; AmiAuth.cd - catalog description (source of truth for all user-visible +; strings). Generated header: `make catalog-strings` (locale/AmiAuth.cd via +; FlexCat's CatComp_h.sd template -> src/core/catalog_strings.h). +; +; This is Phase 1 (#67): a representative slice, not the full string set - +; see userdocs/Localization.md for the migration plan and translator workflow. +; +; Do NOT add: CLI subcommand keywords, ReadArgs template field names, or +; ARexx port command keywords - those are the scripting protocol, not +; user-facing prose, and must stay identical across every locale. +; +; Do NOT hardcode "AmiAuth" (or any other proper noun) into a translatable +; string - pass it as a %s argument instead (see MSG_CLI_ALREADY_EXISTS/ +; MSG_CLI_USAGE_RUNAS below). Translators should never need to copy a +; literal brand name verbatim into every language. +; +; Do NOT add a wrapper template with no real content of its own. The plain +; "AmiAuth: %s\n" prefix wrapping vault_err()'s own (not yet migrated - +; Phase 2) error text has nothing left to translate once the brand name is +; an argument rather than literal text - id 0 is deliberately left unused +; rather than assigned to that, to leave room for it once vault_err()'s +; strings actually get migrated. +; +#language english +#version 0 +; +; --- CLI (src/cli/main.c) --------------------------------------------- +; +MSG_CLI_ALREADY_EXISTS (1//) +%s: %s already exists\n +; +MSG_CLI_ADDED (2//) +Added %s:%s\n +; +MSG_CLI_REMOVED (3//) +Removed '%s'\n +; +MSG_CLI_USAGE_RUNAS (4//) +Run as '%s ...' ('%s ?' shows the arg template):\n +; +; --- GUI (src/gui/main.c): account list column headers ----------------- +; +MSG_GUI_COL_ACCOUNT (5//) +Account +; +MSG_GUI_COL_CODE (6//) +Code +; +MSG_GUI_COL_LEFT (7//) +Left +; +; --- GUI: requester text ------------------------------------------------- +; +MSG_GUI_CANNOT_OPEN_VAULT (8//) +Cannot open the vault at\n%s +; +MSG_GUI_BAD_URI (9//) +That is not a valid otpauth:// URI. +; +MSG_GUI_BAD_SECRET (10//) +That is not an otpauth:// URI or a Base32 secret. +; +MSG_GUI_NO_QR (11//) +No otpauth QR code was found in that image. +; +MSG_GUI_SAVE_FAILED (12//) +Could not save the vault. +; +MSG_GUI_REKEY_DONE (13//) +Vault re-keyed for this machine. +; +MSG_GUI_REKEY_FAILED (14//) +Re-key failed; the vault is unchanged. +; +; --- GUI: common requester button labels ---------------------------------- +; +MSG_GUI_OK (15//) +OK +; +MSG_GUI_CANCEL (16//) +Cancel diff --git a/mkdocs.yml b/mkdocs.yml index cb5437b..d7ce4b4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,7 @@ nav: - Under the hood: - Vault and Passphrases: Vault-and-Passphrases.md - Settings Reference: Settings-Reference.md + - Localization: Localization.md - Security Model: Security-Model.md - Help: - Troubleshooting and FAQ: Troubleshooting-and-FAQ.md diff --git a/src/amiga/catalog.c b/src/amiga/catalog.c new file mode 100644 index 0000000..1c5f695 --- /dev/null +++ b/src/amiga/catalog.c @@ -0,0 +1,74 @@ +/* catalog.c — locale.library catalog backend (#67). See catalog.h. AmigaOS + * only; the host stub is in src/core/catalog.c. + * + * Opened once at startup (CLI main(); GUI open_libs()), closed once at exit + * — persistent app-lifetime scope, unlike the existing transient + * OpenLibrary/CloseLibrary pairs elsewhere in this codebase that only want + * loc_GMTOffset for a moment (src/cli/main.c's cli_utc_offset(), + * src/gui/main.c's GMT-offset probe). Absence of locale.library, or of an + * installed AmiAuth.catalog for the user's language, is not an error - same + * optional-feature pattern as every other library in this codebase; the + * caller always gets English (the built-in default) either way. + * + * OC_BuiltInLanguage note: when it matches the system's current locale + * language (the common case for most users - AmiAuth's built-ins are + * English, most installs are English), OpenCatalog() intentionally skips + * loading any on-disk catalog at all and GetCatalogStr() just returns the + * default - this is documented AmigaOS behaviour, not a bug (confirmed + * on-target: a deliberately mismatched OC_BuiltInLanguage does load the + * catalog and GetCatalogStr() correctly returns the translated string with + * substitution parameters intact - see tests/gui/catalog-onhw.sh). */ +#ifdef __amigaos__ + +#include +#include +#include +#include + +#include "catalog.h" + +/* 's inline stubs reference this exact global by name and + * type (struct LocaleBase *, not struct Library *) - same convention as + * RexxSysBase/GfxBase elsewhere in this project; must not be static. */ +struct LocaleBase *LocaleBase = NULL; +static struct Catalog *g_catalog = NULL; + +/* Overridable at build time (-DAMIAUTH_LOCALE_LIBNAME='"nonexistent"') so an + * on-target test can force a genuine OpenLibrary() failure and exercise the + * LocaleBase==NULL fallback below, without touching the real locale.library + * a WB clone's own boot (IPrefs etc.) still needs - see + * `make catalog-nolib-onhw` / tests/gui/catalog-onhw.sh. Never set for a + * real build; the Makefile only ever defines it for that one test binary. */ +#ifndef AMIAUTH_LOCALE_LIBNAME +#define AMIAUTH_LOCALE_LIBNAME "locale.library" +#endif + +void catalog_open(void) +{ + LocaleBase = (struct LocaleBase *)OpenLibrary((STRPTR)AMIAUTH_LOCALE_LIBNAME, 38); /* OS 2.1+ */ + if (LocaleBase) + g_catalog = OpenCatalog(NULL, (STRPTR)"AmiAuth.catalog", + OC_BuiltInLanguage, (ULONG)(STRPTR)"english", + TAG_DONE); +} + +void catalog_close(void) +{ + if (LocaleBase) { + if (g_catalog) CloseCatalog(g_catalog); + CloseLibrary((struct Library *)LocaleBase); + } + g_catalog = NULL; + LocaleBase = NULL; +} + +const char *catalog_get(long id, const char *def) +{ + /* GetCatalogStr() is documented safe with a NULL catalog (just returns + * def) - but not with a NULL LocaleBase, since that means the library + * itself never opened and there is no function to call at all. */ + if (!LocaleBase) return def; + return (const char *)GetCatalogStr(g_catalog, id, (STRPTR)def); +} + +#endif /* __amigaos__ */ diff --git a/src/cli/main.c b/src/cli/main.c index dde7b2a..a5a7747 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -31,6 +31,7 @@ #include "pbkdf2.h" #include "prefs.h" #include "qrencode.h" /* qr_encode_uri/qr_render_ascii (#45) */ +#include "catalog.h" /* locale.library message catalogs (#67) */ #if defined(__unix__) || defined(__APPLE__) || defined(__linux__) # include @@ -551,7 +552,7 @@ static int cmd_init(const char *path, int always_unlocked, long iterations, exists = fopen(path, "rb"); if (exists) { fclose(exists); - fprintf(stderr, "AmiAuth: %s already exists\n", path); + fprintf(stderr, MSG(MSG_CLI_ALREADY_EXISTS), "AmiAuth", path); return 2; } @@ -677,7 +678,7 @@ static int cmd_add(const char *path, const char *uri) { const otp_account *added = &v.accounts[v.count - 1]; - if (added->issuer[0]) printf("Added %s:%s\n", added->issuer, added->label); + if (added->issuer[0]) printf(MSG(MSG_CLI_ADDED), added->issuer, added->label); else printf("Added %s\n", added->label); } vault_lock(&v); @@ -730,7 +731,7 @@ static int cmd_add_secret(const char *path, const char *secret, memset(&acct, 0, sizeof acct); if (rc != VAULT_OK) { vault_lock(&v); fprintf(stderr, "AmiAuth: %s\n", vault_err(rc)); return 2; } - printf("Added %s:%s\n", issuer, label); + printf(MSG(MSG_CLI_ADDED), issuer, label); vault_lock(&v); return 0; } @@ -840,7 +841,7 @@ static int cmd_remove(const char *path, const char *account) if (rc == VAULT_OK) rc = save_vault(&v, path); if (rc != VAULT_OK) { vault_lock(&v); fprintf(stderr, "AmiAuth: %s\n", vault_err(rc)); return 2; } - printf("Removed '%s'\n", account); + printf(MSG(MSG_CLI_REMOVED), account); vault_lock(&v); return 0; } @@ -850,8 +851,9 @@ static int usage(void) const char *p = g_prog; fprintf(stderr, "AmiAuth - TOTP/HOTP authenticator for AmigaOS\n" - "\n" - "Run as '%s ...' ('%s ?' shows the arg template):\n" + "\n"); + fprintf(stderr, MSG(MSG_CLI_USAGE_RUNAS), p, p); + fprintf(stderr, " CODE [digits] [period] Print a code (no vault)\n" " INIT [OPEN] Create a vault\n" " ADD \"\" Import an account from a URI\n" @@ -869,8 +871,7 @@ static int usage(void) " OFFSET Set + save a UTC offset\n" " NUDGE <+/-seconds> Adjust + save the current offset\n" " HELP\n" - "\n", - p, p); + "\n"); #ifdef AMIAUTH_AMIGA fprintf(stderr, "Options: ISSUER/K LABEL/K STEAM/S (bare-secret ADD) VAULT OPEN/S\n" @@ -965,9 +966,10 @@ int main(int argc, char **argv) if (argc > 0 && argv[0] && argv[0][0]) g_prog = argv[0]; crypto_select_init(); + catalog_open(); memset(opt, 0, sizeof opt); rda = ReadArgs((STRPTR)TMPL, opt, NULL); - if (!rda) { PrintFault(IoErr(), (STRPTR)g_prog); return 20; } /* RETURN_FAIL */ + if (!rda) { PrintFault(IoErr(), (STRPTR)g_prog); catalog_close(); return 20; } /* RETURN_FAIL */ memset(&a, 0, sizeof a); a.command = (const char *)opt[P_COMMAND]; @@ -984,6 +986,7 @@ int main(int argc, char **argv) rc = dispatch(&a); FreeArgs(rda); + catalog_close(); return rc; } @@ -997,6 +1000,7 @@ int main(int argc, char **argv) int npos = 0, i; if (argc > 0 && argv[0] && argv[0][0]) g_prog = argv[0]; + catalog_open(); memset(&a, 0, sizeof a); a.iterations = -1; @@ -1032,7 +1036,11 @@ int main(int argc, char **argv) a.value = npos > 1 ? pos[1] : NULL; /* the command's positional argument */ a.digits = npos > 2 ? pos[2] : NULL; /* CODE only */ a.period = npos > 3 ? pos[3] : NULL; /* CODE only */ - return dispatch(&a); + { + int rc = dispatch(&a); + catalog_close(); + return rc; + } } #endif diff --git a/src/core/catalog.c b/src/core/catalog.c new file mode 100644 index 0000000..b9ddee4 --- /dev/null +++ b/src/core/catalog.c @@ -0,0 +1,16 @@ +/* catalog.c — host stub. The real locale.library backend is in + * src/amiga/catalog.c; exactly one is compiled per platform. */ +#include "catalog.h" + +#ifndef __amigaos__ + +void catalog_open(void) {} +void catalog_close(void) {} + +const char *catalog_get(long id, const char *def) +{ + (void)id; + return def; +} + +#endif /* !__amigaos__ */ diff --git a/src/core/catalog.h b/src/core/catalog.h new file mode 100644 index 0000000..65a001b --- /dev/null +++ b/src/core/catalog.h @@ -0,0 +1,56 @@ +/* catalog.h — locale.library catalog lookup for user-visible strings (#67). + * + * `locale/AmiAuth.cd` is the source of truth for every translatable string; + * `make catalog-strings` (FlexCat + its standard CatComp_h.sd template) + * regenerates catalog_strings.h from it — a normal checked-in source file, + * not a build-time-generated artifact, so an ordinary `make test`/`cli`/ + * `m68k`/`gui` build needs neither FlexCat nor network access. Re-run + * `make catalog-strings` (or `catalog-strings-docker`) after editing the + * .cd and commit the result. + * + * Call sites use one macro everywhere, host or Amiga: + * + * printf(MSG(MSG_CLI_ERR), vault_err(rc)); + * + * catalog_get() is deliberately NOT the FlexCat-generated GetAmiAuthString() + * — that helper (and the packed string block/array it walks) needs + * , which doesn't exist off-Amiga. Since MSG() already has + * the compile-time default string in hand (MSG_FOO_STR, pasted in via the + * id##_STR trick below), the real lookup only needs the plain two-argument + * GetCatalogStr(catalog, id, default) shape — trivial to reimplement per + * platform (src/amiga/catalog.c; src/core/catalog.c's host stub) without + * needing the generated code/array/block sections at all. */ +#ifndef AMIAUTH_CATALOG_H +#define AMIAUTH_CATALOG_H + +#ifndef __amigaos__ +/* catalog_strings.h unconditionally #includes , even in + * NUMBERS+STRINGS-only mode - though nothing in that mode actually + * references any exec/types.h type (it's plain #define ID n / #define + * ID_STR "..." text). Pre-satisfy its include guard to skip it on hosts + * that don't have the header, rather than hand-duplicating the ID list. */ +#define EXEC_TYPES_H +#endif +#define AmiAuth_NUMBERS +#define AmiAuth_STRINGS +#include "catalog_strings.h" +#undef AmiAuth_STRINGS +#undef AmiAuth_NUMBERS +#ifndef __amigaos__ +#undef EXEC_TYPES_H +#endif + +/* Open/close the catalog for this process's lifetime (app startup/exit) — + * no-ops on the host. Absence of locale.library, or of an installed + * AmiAuth.catalog for the user's language, is not an error: catalog_get() + * always falls back to `def`. */ +void catalog_open(void); +void catalog_close(void); + +/* Look up `id`'s translated string if a catalog is open and has one, else + * `def`. Called via MSG() below, never directly. */ +const char *catalog_get(long id, const char *def); + +#define MSG(id) catalog_get(id, id##_STR) + +#endif /* AMIAUTH_CATALOG_H */ diff --git a/src/core/catalog_strings.h b/src/core/catalog_strings.h new file mode 100644 index 0000000..230109e --- /dev/null +++ b/src/core/catalog_strings.h @@ -0,0 +1,228 @@ + +/**************************************************************** + + This file was created automatically by `FlexCat 2.18' + from "locale/AmiAuth.cd" + + using CatComp.sd 1.2 (24.09.1999) + + Do NOT edit by hand! + +****************************************************************/ + +#ifndef AmiAuth_STRINGS_H +#define AmiAuth_STRINGS_H + +#ifndef EXEC_TYPES_H +#include +#endif + +#ifdef AmiAuth_BASIC_CODE +#undef AmiAuth_BASIC +#undef AmiAuth_CODE +#define AmiAuth_BASIC +#define AmiAuth_CODE +#endif + +#ifdef AmiAuth_BASIC +#undef AmiAuth_ARRAY +#undef AmiAuth_BLOCK +#define AmiAuth_ARRAY +#define AmiAuth_BLOCK +#endif + +#ifdef AmiAuth_ARRAY +#undef AmiAuth_NUMBERS +#undef AmiAuth_STRINGS +#define AmiAuth_NUMBERS +#define AmiAuth_STRINGS +#endif + +#ifdef AmiAuth_BLOCK +#undef AmiAuth_STRINGS +#define AmiAuth_STRINGS +#endif + + +#ifdef AmiAuth_CODE +#include +extern struct Library *LocaleBase; +#endif + +#ifdef AmiAuth_NUMBERS + +#define MSG_CLI_ALREADY_EXISTS 1 +#define MSG_CLI_ADDED 2 +#define MSG_CLI_REMOVED 3 +#define MSG_CLI_USAGE_RUNAS 4 +#define MSG_GUI_COL_ACCOUNT 5 +#define MSG_GUI_COL_CODE 6 +#define MSG_GUI_COL_LEFT 7 +#define MSG_GUI_CANNOT_OPEN_VAULT 8 +#define MSG_GUI_BAD_URI 9 +#define MSG_GUI_BAD_SECRET 10 +#define MSG_GUI_NO_QR 11 +#define MSG_GUI_SAVE_FAILED 12 +#define MSG_GUI_REKEY_DONE 13 +#define MSG_GUI_REKEY_FAILED 14 +#define MSG_GUI_OK 15 +#define MSG_GUI_CANCEL 16 + +#endif /* AmiAuth_NUMBERS */ + + +/****************************************************************************/ + + +#ifdef AmiAuth_STRINGS + +#define MSG_CLI_ALREADY_EXISTS_STR "%s: %s already exists\n" +#define MSG_CLI_ADDED_STR "Added %s:%s\n" +#define MSG_CLI_REMOVED_STR "Removed '%s'\n" +#define MSG_CLI_USAGE_RUNAS_STR "Run as '%s ...' ('%s ?' shows the arg template):\n" +#define MSG_GUI_COL_ACCOUNT_STR "Account" +#define MSG_GUI_COL_CODE_STR "Code" +#define MSG_GUI_COL_LEFT_STR "Left" +#define MSG_GUI_CANNOT_OPEN_VAULT_STR "Cannot open the vault at\n%s" +#define MSG_GUI_BAD_URI_STR "That is not a valid otpauth:// URI." +#define MSG_GUI_BAD_SECRET_STR "That is not an otpauth:// URI or a Base32 secret." +#define MSG_GUI_NO_QR_STR "No otpauth QR code was found in that image." +#define MSG_GUI_SAVE_FAILED_STR "Could not save the vault." +#define MSG_GUI_REKEY_DONE_STR "Vault re-keyed for this machine." +#define MSG_GUI_REKEY_FAILED_STR "Re-key failed; the vault is unchanged." +#define MSG_GUI_OK_STR "OK" +#define MSG_GUI_CANCEL_STR "Cancel" + +#endif /* AmiAuth_STRINGS */ + + +/****************************************************************************/ + + +#ifdef AmiAuth_ARRAY + +struct AmiAuth_ArrayType +{ + LONG cca_ID; + STRPTR cca_Str; +}; + +static const struct AmiAuth_ArrayType AmiAuth_Array[] = +{ + { MSG_CLI_ALREADY_EXISTS, (STRPTR)MSG_CLI_ALREADY_EXISTS_STR }, + { MSG_CLI_ADDED, (STRPTR)MSG_CLI_ADDED_STR }, + { MSG_CLI_REMOVED, (STRPTR)MSG_CLI_REMOVED_STR }, + { MSG_CLI_USAGE_RUNAS, (STRPTR)MSG_CLI_USAGE_RUNAS_STR }, + { MSG_GUI_COL_ACCOUNT, (STRPTR)MSG_GUI_COL_ACCOUNT_STR }, + { MSG_GUI_COL_CODE, (STRPTR)MSG_GUI_COL_CODE_STR }, + { MSG_GUI_COL_LEFT, (STRPTR)MSG_GUI_COL_LEFT_STR }, + { MSG_GUI_CANNOT_OPEN_VAULT, (STRPTR)MSG_GUI_CANNOT_OPEN_VAULT_STR }, + { MSG_GUI_BAD_URI, (STRPTR)MSG_GUI_BAD_URI_STR }, + { MSG_GUI_BAD_SECRET, (STRPTR)MSG_GUI_BAD_SECRET_STR }, + { MSG_GUI_NO_QR, (STRPTR)MSG_GUI_NO_QR_STR }, + { MSG_GUI_SAVE_FAILED, (STRPTR)MSG_GUI_SAVE_FAILED_STR }, + { MSG_GUI_REKEY_DONE, (STRPTR)MSG_GUI_REKEY_DONE_STR }, + { MSG_GUI_REKEY_FAILED, (STRPTR)MSG_GUI_REKEY_FAILED_STR }, + { MSG_GUI_OK, (STRPTR)MSG_GUI_OK_STR }, + { MSG_GUI_CANCEL, (STRPTR)MSG_GUI_CANCEL_STR }, +}; + + +#endif /* AmiAuth_ARRAY */ + + +/****************************************************************************/ + + +#ifdef AmiAuth_BLOCK + +static const char AmiAuth_Block[] = +{ + + "\x00\x00\x00\x01" "\x00\x16" + MSG_CLI_ALREADY_EXISTS_STR "" + "\x00\x00\x00\x02" "\x00\x0c" + MSG_CLI_ADDED_STR "" + "\x00\x00\x00\x03" "\x00\x0e" + MSG_CLI_REMOVED_STR "\x00" + "\x00\x00\x00\x04" "\x00\x3c" + MSG_CLI_USAGE_RUNAS_STR "" + "\x00\x00\x00\x05" "\x00\x08" + MSG_GUI_COL_ACCOUNT_STR "\x00" + "\x00\x00\x00\x06" "\x00\x04" + MSG_GUI_COL_CODE_STR "" + "\x00\x00\x00\x07" "\x00\x04" + MSG_GUI_COL_LEFT_STR "" + "\x00\x00\x00\x08" "\x00\x1c" + MSG_GUI_CANNOT_OPEN_VAULT_STR "\x00" + "\x00\x00\x00\x09" "\x00\x24" + MSG_GUI_BAD_URI_STR "\x00" + "\x00\x00\x00\x0a" "\x00\x32" + MSG_GUI_BAD_SECRET_STR "\x00" + "\x00\x00\x00\x0b" "\x00\x2c" + MSG_GUI_NO_QR_STR "\x00" + "\x00\x00\x00\x0c" "\x00\x1a" + MSG_GUI_SAVE_FAILED_STR "\x00" + "\x00\x00\x00\x0d" "\x00\x20" + MSG_GUI_REKEY_DONE_STR "" + "\x00\x00\x00\x0e" "\x00\x26" + MSG_GUI_REKEY_FAILED_STR "" + "\x00\x00\x00\x0f" "\x00\x02" + MSG_GUI_OK_STR "" + "\x00\x00\x00\x10" "\x00\x06" + MSG_GUI_CANCEL_STR "" + +}; + +#endif /* AmiAuth_BLOCK */ + + +/****************************************************************************/ + + +#ifdef AmiAuth_CODE + +#ifndef AmiAuth_CODE_EXISTS + #define AmiAuth_CODE_EXISTS + + STRPTR GetAmiAuthString(struct AmiAuth_LocaleInfo *li, LONG stringNum) + { + LONG *l; + UWORD *w; + STRPTR builtIn; + + l = (LONG *)AmiAuth_Block; + + while (*l != stringNum) + { + w = (UWORD *)((ULONG)l + 4); + l = (LONG *)((ULONG)l + (ULONG)*w + 6); + } + builtIn = (STRPTR)((ULONG)l + 6); + +// #define AmiAuth_XLocaleBase LocaleBase +// #define LocaleBase li->li_LocaleBase + + if(LocaleBase && li) + return(GetCatalogStr(li->li_Catalog, stringNum, builtIn)); + +// #undef LocaleBase +// #define LocaleBase XLocaleBase +// #undef AmiAuth_XLocaleBase + + return(builtIn); + } + +#else + + STRPTR GetAmiAuthString(struct AmiAuth_LocaleInfo *li, LONG stringNum); + +#endif /* AmiAuth_CODE_EXISTS */ + +#endif /* AmiAuth_CODE */ + + +/****************************************************************************/ + + +#endif /* AmiAuth_STRINGS_H */ diff --git a/src/gui/main.c b/src/gui/main.c index 4dd55d7..c256424 100644 --- a/src/gui/main.c +++ b/src/gui/main.c @@ -75,6 +75,7 @@ AMIAUTH_VERSTAG("AmiAuthGUI") #include "guiport.h" /* CLI->GUI IPC (Stage 3b public port) */ #include "crypto_select.h" /* select crypto hot-loop impl, #47 */ #include "arexx.h" /* ARexx automation port, #46 */ +#include "catalog.h" /* locale.library message catalogs (#67) */ /* Request a larger stack (libnix): the QR decoder still needs more than the * few KB a shell hands a Run/WBench program. qr_decode_gray keeps its big @@ -195,6 +196,10 @@ static char g_rexx_listbuf[VAULT_MAX_ACCOUNTS * (OTP_MAX_ISSUER + OTP_MAX_LABEL #endif static char g_main_title[sizeof(AMIAUTH_TITLE_BASE) + 40 /* " [" + rexxportname(31) + "]" */] = AMIAUTH_TITLE_BASE; +/* ci_Title defaults are English; catalog_open() has not run yet at static- + * init time (a static initializer can't call MSG(), a function), so main() + * overwrites all three with the translated (or same, if no catalog) string + * once it has. */ static struct ColumnInfo g_columns[] = { { 50, (STRPTR)"Account", CIF_WEIGHTED }, { 34, (STRPTR)"Code", CIF_WEIGHTED }, @@ -240,6 +245,7 @@ static struct NewMenu g_menu[] = { static void close_libs(void) { + catalog_close(); if (RexxSysBase) CloseLibrary((struct Library *)RexxSysBase); if (CxBase) CloseLibrary(CxBase); if (WorkbenchBase) CloseLibrary(WorkbenchBase); @@ -279,6 +285,7 @@ static const char *open_libs(void) WorkbenchBase = OpenLibrary((STRPTR)"workbench.library", 37); CxBase = OpenLibrary((STRPTR)"commodities.library", 37); /* optional: commodity */ RexxSysBase = (struct RxsLib *)OpenLibrary((STRPTR)"rexxsyslib.library", 0); /* optional: ARexx port, #46 */ + catalog_open(); /* optional: message catalog, #67 */ if (!IntuitionBase || !UtilityBase) return "needs intuition.library / utility.library v37+ (OS 2.04)"; if (!WindowBase || !LayoutBase || !ListBrowserBase || !FuelGaugeBase || !ButtonBase) @@ -612,9 +619,9 @@ static void gui_maybe_rekey(struct Window *win, vault *v, const char *path, if (amiga_random(salt, sizeof salt) == 0 && vault_set_passphrase(v, pass, ideal, salt) == VAULT_OK && gui_save(v, path) == VAULT_OK) - gui_requester(win, "Vault re-keyed for this machine.", "OK", NULL); + gui_requester(win, MSG(MSG_GUI_REKEY_DONE), MSG(MSG_GUI_OK), NULL); else - gui_requester(win, "Re-key failed; the vault is unchanged.", "OK", NULL); + gui_requester(win, MSG(MSG_GUI_REKEY_FAILED), MSG(MSG_GUI_OK), NULL); memset(salt, 0, sizeof salt); } } @@ -628,14 +635,14 @@ static int gui_add_uri(struct Window *win, vault *v, const char *path, const cha int changed = 0; vault_result rc; if (otpauth_parse(uri, &acct) != 0) { - gui_requester(win, "That is not a valid otpauth:// URI.", "OK", NULL); + gui_requester(win, MSG(MSG_GUI_BAD_URI), MSG(MSG_GUI_OK), NULL); } else { rc = vault_add(v, &acct); if (rc == VAULT_OK) { changed = 1; rc = gui_save(v, path); } if (rc != VAULT_OK) gui_requester(win, rc == VAULT_ERR_FULL ? "The vault is full (max 64 accounts)." - : "Could not save the vault.", "OK", NULL); + : MSG(MSG_GUI_SAVE_FAILED), MSG(MSG_GUI_OK), NULL); } memset(&acct, 0, sizeof acct); return changed; @@ -676,7 +683,7 @@ static int do_add_qr_impl(struct Window *win, vault *v, const char *vpath, const if (dr == QR_OK) changed = gui_add_uri(win, v, vpath, uri); else - gui_requester(win, "No otpauth QR code was found in that image.", "OK", NULL); + gui_requester(win, MSG(MSG_GUI_NO_QR), MSG(MSG_GUI_OK), NULL); qrimage_free(gray); memset(uri, 0, sizeof uri); return changed; @@ -1053,7 +1060,7 @@ static int gui_open_vault(vault *v, char *path, size_t cap, int *encrypted, if (rc != VAULT_OK) { /* a WB-launched process has no console, so a requester must carry * the error; keep the Printf for Shell launches */ - gui_requester(NULL, "Cannot open the vault at\n%s", "Quit", path); + gui_requester(NULL, MSG(MSG_GUI_CANNOT_OPEN_VAULT), "Quit", path); Printf((CONST_STRPTR)"AmiAuth: cannot open the vault (%ld)\n", (LONG)rc); return -1; } @@ -1533,21 +1540,21 @@ static int gui_add_secret(struct Window *win, vault *v, const char *path, const /* Cheap validation first, so a typo doesn't cost filling in the form. */ if (base32_decode(secret, probe, sizeof probe) <= 0) { memset(probe, 0, sizeof probe); - gui_requester(win, "That is not an otpauth:// URI or a Base32 secret.", "OK", NULL); + gui_requester(win, MSG(MSG_GUI_BAD_SECRET), MSG(MSG_GUI_OK), NULL); return 0; } memset(probe, 0, sizeof probe); if (!secret_meta_request(issuer, label)) return 0; if (otp_account_from_secret(issuer, label, secret, &acct) != 0) { - gui_requester(win, "That is not an otpauth:// URI or a Base32 secret.", "OK", NULL); + gui_requester(win, MSG(MSG_GUI_BAD_SECRET), MSG(MSG_GUI_OK), NULL); } else { rc = vault_add(v, &acct); if (rc == VAULT_OK) { changed = 1; rc = gui_save(v, path); } if (rc != VAULT_OK) gui_requester(win, rc == VAULT_ERR_FULL ? "The vault is full (max 64 accounts)." - : "Could not save the vault.", "OK", NULL); + : MSG(MSG_GUI_SAVE_FAILED), MSG(MSG_GUI_OK), NULL); } memset(&acct, 0, sizeof acct); memset(issuer, 0, sizeof issuer); @@ -1877,6 +1884,13 @@ int main(int argc, char **argv) return 20; } + /* g_columns is a static initializer (English only - MSG() is a function + * call, illegal there); fill in the real (translated, or same if no + * catalog) titles now that catalog_open() has run. */ + g_columns[0].ci_Title = (STRPTR)MSG(MSG_GUI_COL_ACCOUNT); + g_columns[1].ci_Title = (STRPTR)MSG(MSG_GUI_COL_CODE); + g_columns[2].ci_Title = (STRPTR)MSG(MSG_GUI_COL_LEFT); + /* Read CX_* tooltypes (WBStartup icon) or CLI args uniformly (amiga.lib). */ tt = ArgArrayInit(argc, (CONST_STRPTR *)argv); @@ -2483,7 +2497,7 @@ int main(int argc, char **argv) changed = 1; rc = gui_save(&v, path); if (rc != VAULT_OK) - gui_requester(win, "Could not save the vault.", "OK", NULL); + gui_requester(win, MSG(MSG_GUI_SAVE_FAILED), MSG(MSG_GUI_OK), NULL); } memset(&acct, 0, sizeof acct); } @@ -2508,7 +2522,7 @@ int main(int argc, char **argv) changed = 1; if (rc == VAULT_OK) rc = gui_save(&v, path); if (rc != VAULT_OK) - gui_requester(win, "Could not save the vault.", "OK", NULL); + gui_requester(win, MSG(MSG_GUI_SAVE_FAILED), MSG(MSG_GUI_OK), NULL); } } diff --git a/tests/gui/catalog-onhw.sh b/tests/gui/catalog-onhw.sh new file mode 100755 index 0000000..283120f --- /dev/null +++ b/tests/gui/catalog-onhw.sh @@ -0,0 +1,116 @@ +#!/bin/sh +# catalog-onhw.sh — on-target smoke test for the locale.library catalog +# plumbing (#67): AmiAuthGUI's CLI companion, AmiAuth, runs a vault command +# whose output goes through MSG()/catalog_get() (ADD's "Added %s:%s\n" - see +# locale/AmiAuth.cd), and the result is relayed back over serial via +# tests/copperline/arexxtest (a RAM: file -> RawPutChar relay, reused +# unchanged from the #46 ARexx on-target test). +# +# Two binaries run in the SAME boot, both asserted to produce the identical +# English default output: +# - build/AmiAuth - locale.library present, no matching catalog +# installed (the common real-world case - most installs are English, +# same as AmiAuth's built-in strings). Exercises the full +# OpenLibrary/OpenCatalog/GetCatalogStr chain "successfully" skipping +# catalog loading (see catalog.c's OC_BuiltInLanguage note) without +# crashing or corrupting anything. +# - build/AmiAuth-nolib - built with -DAMIAUTH_LOCALE_LIBNAME set to a +# name that doesn't exist (`make catalog-nolib-onhw`), so catalog_open()'s +# OpenLibrary() genuinely fails on real hardware. Exercises the +# LocaleBase==NULL fallback for real, without deleting the WB clone's +# actual locale.library - which earlier attempts found breaks unrelated +# boot components (the clone's own Locale-prefs tool pops a modal error +# and hangs headless boot). Never shipped; test-only. +# +# What this does NOT (and, in this project's fixtures, currently cannot) +# cover as an automated regression test: a catalog actually overriding the +# built-in text. AmigaOS intentionally skips loading any on-disk catalog +# when OC_BuiltInLanguage matches the current locale's language (documented +# behaviour, see catalog.c) - and this project's WB clone (tests/gui/.env's +# AMIAUTH_WB_HDD) has no other ".language" module installed, so +# the system locale can't meaningfully be anything but English here. This +# WAS verified manually during development: deliberately mismatching +# OC_BuiltInLanguage against the system locale (forcing a real disk load) +# confirmed OpenCatalog()/GetCatalogStr() correctly return the translated +# string, with substitution parameters intact, on real 68k hardware. +# +# Non-mutating: the Workbench install is a copy-on-write clone; source is +# never touched. Paths come from tests/gui/.env (shared with gui-smoke.sh). +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) # tests/gui +ROOT=$(cd "$HERE/../.." && pwd) + +[ -f "$HERE/.env" ] && . "$HERE/.env" +COPPERLINE=${COPPERLINE:-copperline} +KICK=${KICK:-${AMIAUTH_ROM:-}} +WB=${WB:-${AMIAUTH_WB_HDD:-}} +CLI_M68K=${CLI_M68K:-$ROOT/build/AmiAuth} +CLI_NOLIB=${CLI_NOLIB:-$ROOT/build/AmiAuth-nolib} +AREXXTEST=${AREXXTEST:-$ROOT/build/arexxtest} +SECS=${SECS:-40} + +OUTDIR=$ROOT/build/catalog-onhw-run +BOOT=$OUTDIR/boot +LOG=$OUTDIR/serial.log + +fail() { echo "CATALOG-ONHW FAIL: $1" >&2; exit 1; } + +command -v "$COPPERLINE" >/dev/null 2>&1 || fail "copperline not found (set COPPERLINE=, or brew install copperline)" +[ -n "$KICK" ] && [ -e "$KICK" ] || fail "Kickstart 3.2 ROM missing (KICK= / AMIAUTH_ROM in tests/gui/.env): '$KICK'" +[ -n "$WB" ] && [ -d "$WB" ] || fail "Workbench 3.2 dir missing (WB= / AMIAUTH_WB_HDD): '$WB'" +[ -e "$WB/Libs/locale.library" ] || fail "WB dir has no locale.library: '$WB'" +[ -x "$CLI_M68K" ] || fail "$CLI_M68K missing — build it first: make m68k-docker" +[ -x "$CLI_NOLIB" ] || fail "$CLI_NOLIB missing — build it first: make catalog-nolib-onhw-docker" +[ -x "$AREXXTEST" ] || fail "$AREXXTEST missing — build it first: make arexx-onhw-docker" + +rm -rf "$OUTDIR"; mkdir -p "$OUTDIR" +cleanup() { rm -rf "$BOOT"; } +trap cleanup EXIT INT TERM + +cp -Rc "$WB" "$BOOT" 2>/dev/null || cp -R "$WB" "$BOOT" +cp "$CLI_M68K" "$BOOT/AmiAuth" +cp "$CLI_NOLIB" "$BOOT/AmiAuth-nolib" +cp "$AREXXTEST" "$BOOT/arexxtest" + +SEQ="$BOOT/S/Startup-Sequence" +[ -f "$SEQ" ] || fail "clone missing S/Startup-Sequence" +awk '{ print } + /^LoadWB/ { + print "SYS:AmiAuth INIT OPEN VAULT=RAM:t.vault >RAM:catout.txt" + print "SYS:AmiAuth ADD \"otpauth://totp/Test:foo?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\" VAULT=RAM:t.vault >>RAM:catout.txt" + print "SYS:AmiAuth-nolib INIT OPEN VAULT=RAM:t2.vault >RAM:catout2.txt" + print "SYS:AmiAuth-nolib ADD \"otpauth://totp/Test:foo?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\" VAULT=RAM:t2.vault >>RAM:catout2.txt" + print "SYS:arexxtest RAM:catout.txt" + print "SYS:arexxtest RAM:catout2.txt" + }' "$SEQ" > "$SEQ.new" && mv "$SEQ.new" "$SEQ" + +CFG="$OUTDIR/copperline.toml" +cat > "$CFG" <"$LOG" 2>&1 || { tail -20 "$LOG" >&2; fail "copperline exited non-zero"; } +tr -d '\r' <"$LOG" >"$LOG.n" && mv "$LOG.n" "$LOG" + +echo "----- serial capture -----"; cat "$LOG"; echo "--------------------------" + +begins=$(grep -c '^BEGIN$' "$LOG" || true) +ends=$(grep -c '^END$' "$LOG" || true) +[ "$begins" -eq 2 ] || fail "expected 2 BEGIN markers (one per arexxtest relay), got $begins — did both AmiAuth commands run? (raise SECS?)" +[ "$ends" -eq 2 ] || fail "expected 2 END markers, got $ends (raise SECS?)" +grep -q '^NOFILE$' "$LOG" && fail "a RAM:catout*.txt relay is missing — one of the CLI commands didn't run" + +added=$(grep -cxF 'Added Test:foo' "$LOG" || true) +[ "$added" -eq 2 ] || fail "expected 'Added Test:foo' twice (locale.library present AND absent), got $added — MSG(MSG_CLI_ADDED) regressed" + +echo "CATALOG-ONHW PASS: MSG()-routed CLI output correct on real 68k, with locale.library both present and absent." diff --git a/tests/test_catalog.c b/tests/test_catalog.c new file mode 100644 index 0000000..2f1a601 --- /dev/null +++ b/tests/test_catalog.c @@ -0,0 +1,26 @@ +/* test_catalog.c — host catalog stub contract (#67). The host build has no + * locale.library concept at all: catalog_get() must always return the + * caller's default, and catalog_open()/catalog_close() must be safe no-ops. + * This is the contract every MSG() call site relies on; the real + * locale.library lookup (src/amiga/catalog.c) is only exercised on-target + * (tests/gui/catalog-onhw.sh), since it needs a real Amiga. */ +#include + +#include "test.h" +#include "catalog.h" + +void run_catalog_tests(void) +{ + /* Safe before catalog_open(), safe called more than once, safe after + * catalog_close() - callers never need to guard these. */ + TEST_CHECK(strcmp(catalog_get(MSG_CLI_ALREADY_EXISTS, MSG_CLI_ALREADY_EXISTS_STR), MSG_CLI_ALREADY_EXISTS_STR) == 0); + catalog_open(); + TEST_CHECK(strcmp(catalog_get(MSG_CLI_ALREADY_EXISTS, MSG_CLI_ALREADY_EXISTS_STR), MSG_CLI_ALREADY_EXISTS_STR) == 0); + TEST_CHECK(strcmp(MSG(MSG_GUI_OK), MSG_GUI_OK_STR) == 0); + catalog_close(); + TEST_CHECK(strcmp(catalog_get(MSG_CLI_ALREADY_EXISTS, MSG_CLI_ALREADY_EXISTS_STR), MSG_CLI_ALREADY_EXISTS_STR) == 0); + + /* An unknown id and an arbitrary default still just round-trip - the + * host stub never looks at id at all. */ + TEST_CHECK(strcmp(catalog_get(9999, "arbitrary default"), "arbitrary default") == 0); +} diff --git a/tests/test_main.c b/tests/test_main.c index 76696ca..3db6618 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -24,6 +24,7 @@ void run_prefs_tests(void); void run_qr_tests(void); void run_qr_encode_tests(void); void run_arexx_cmd_tests(void); +void run_catalog_tests(void); int main(void) { @@ -48,6 +49,7 @@ int main(void) run_qr_tests(); run_qr_encode_tests(); run_arexx_cmd_tests(); + run_catalog_tests(); printf("\n%d passed, %d failed, %d pending\n", g_test.passed, g_test.failed, g_test.pending); diff --git a/tools/docs2guide.py b/tools/docs2guide.py index ffd3088..4a80f89 100644 --- a/tools/docs2guide.py +++ b/tools/docs2guide.py @@ -35,6 +35,7 @@ ('Time-and-Clock-Sync', 'Time and Clock Sync'), ('Vault-and-Passphrases', 'Vault and Passphrases'), ('Settings-Reference', 'Settings Reference'), + ('Localization', 'Localization'), ('Security-Model', 'Security Model'), ('Troubleshooting-and-FAQ', 'Troubleshooting and FAQ'), ('Building-from-Source', 'Building from Source'), diff --git a/tools/fetch-flexcat.sh b/tools/fetch-flexcat.sh new file mode 100755 index 0000000..deb3b24 --- /dev/null +++ b/tools/fetch-flexcat.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# fetch-flexcat.sh — fetch + build FlexCat (github.com/adtools/flexcat) as a +# HOST tool, for generating AmiAuth's catalog message-ID header and (for +# translators) .catalog files from locale/AmiAuth.cd (#67). +# +# FlexCat is a modern, portable, GPL-licensed replacement for the classic +# catcomp tool — it builds and runs natively on Linux (confirmed via its own +# `make OS=unix` and CI), so it never touches the m68k cross-compiler or the +# AmiAuth binary itself; it's purely a build-time tool, same status as +# tools/docs2guide.py. Not part of the shipped (zero-dependency) Amiga build. +# +# Pinned to a specific commit (not just a tag) for the same reproducibility +# fetch-amissl-sdk.sh gets from a SHA-256 pin on a downloaded archive — a git +# commit hash is already a strong content hash, so no separate checksum step +# is needed here. +# +# Idempotent: builds only when the pinned-commit binary is missing. Requires +# a working native (non-cross) C compiler on PATH — e.g. run this inside the +# amiga-gcc Docker image (`make flexcat-docker`), which bundles a real gcc +# for exactly this purpose, or any plain Linux/Unix host with gcc. FlexCat's +# own Makefile assumes GCC-flavoured flags (-gstabs) that macOS's clang +# rejects, so a native macOS build isn't supported here — use the *-docker +# Makefile target instead. +# +# Usage: +# tools/fetch-flexcat.sh +# +# Environment: +# FLEXCAT_COMMIT override the pinned commit (default below) +# FLEXCAT_CACHE_DIR cache root (default ${TMPDIR:-/tmp}/flexcat-src) +# +# On success, prints the built binary's path to stdout. +# Exit codes: 0 ok, 1 fetch/build failure, 2 usage. +set -euo pipefail + +# Tag 2.18 (27.04.2016), the latest release as of this writing. +FLEXCAT_COMMIT="${FLEXCAT_COMMIT:-b45d1e6ceb8647d8965d494bdddb617ca4927b4e}" +# Not $HOME/.cache: under this project's `--user UID:GID` Docker convention +# (numeric UID with no /etc/passwd entry, see Makefile's DOCKER_USER), $HOME +# resolves to "/" - unwritable. /tmp is reliably writable regardless. +FLEXCAT_CACHE_DIR="${FLEXCAT_CACHE_DIR:-${TMPDIR:-/tmp}/flexcat-src}" + +src_dir="$FLEXCAT_CACHE_DIR/$FLEXCAT_COMMIT" +bin_path="$src_dir/src/bin_unix/flexcat" +stamp="$src_dir/.ok" + +if [ -f "$stamp" ] && [ -x "$bin_path" ]; then + echo "$bin_path" + exit 0 +fi + +need() { command -v "$1" >/dev/null 2>&1 || { echo "fetch-flexcat.sh: missing tool '$1'" >&2; exit 1; }; } +need git +need make +need cc + +mkdir -p "$FLEXCAT_CACHE_DIR" +rm -rf "$src_dir" +echo "fetch-flexcat.sh: cloning adtools/flexcat @ $FLEXCAT_COMMIT" >&2 +git clone --quiet https://github.com/adtools/flexcat.git "$src_dir" +(cd "$src_dir" && git checkout --quiet "$FLEXCAT_COMMIT") + +got_commit=$(cd "$src_dir" && git rev-parse HEAD) +if [ "$got_commit" != "$FLEXCAT_COMMIT" ]; then + echo "fetch-flexcat.sh: checkout landed on $got_commit, expected $FLEXCAT_COMMIT" >&2 + exit 1 +fi + +# FlexCat's own build bootstraps a few generated headers (FlexCat_cat.h, +# locale.c, ...) using a *pre-existing* flexcat binary - a chicken-and-egg +# problem the upstream repo solves by checking those generated files in. +# `git checkout` doesn't preserve source timestamps, so the checked-in +# outputs can end up looking older than their .pot/.sd inputs, tricking +# `make` into trying (and failing) to regenerate them. Force the opposite +# ordering so make treats them as already up to date. +(cd "$src_dir/src" && touch locale/FlexCat.pot && sleep 1 && \ + touch FlexCat_cat.h locale.c locale_other.c FlexCat_cat_other.h) + +echo "fetch-flexcat.sh: building (make OS=unix)" >&2 +(cd "$src_dir/src" && make OS=unix >&2) + +if [ ! -x "$bin_path" ]; then + echo "fetch-flexcat.sh: build finished but $bin_path is missing" >&2 + exit 1 +fi + +touch "$stamp" +echo "$bin_path" diff --git a/userdocs/Localization.md b/userdocs/Localization.md new file mode 100644 index 0000000..d9bc4d6 --- /dev/null +++ b/userdocs/Localization.md @@ -0,0 +1,61 @@ +# Localization + +AmiAuth reads its user-visible text through AmigaOS's standard +**locale.library catalog** system (OS 2.1+), so a community translation can +be dropped in as a single file — no rebuild, no reinstall of AmiAuth itself. + +**Status:** this is an early, in-progress rollout. A representative slice of +strings (CLI status/error messages, a few GUI requesters, the account list's +column headers) already goes through the catalog system; most of the +application's text is still English-only pending a fuller migration. If your +language isn't fully covered yet, that's why — check back in a future +release, or see [Building from Source](Building-from-Source.md) if you'd +like to help. + +## Installing a translation + +Drop a `.catalog` file — named after your language, e.g. +`deutsch.catalog` — into: + + LOCALE:Catalogs//AmiAuth.catalog + +(`LOCALE:` is normally assigned to `SYS:Locale`.) AmiAuth picks it up +automatically next time it starts, based on your system's default language +(**Locale** in Prefs). No translation installed, or none for your language, +is not an error — AmiAuth just uses its built-in English text. + +## Contributing a translation + +AmiAuth's strings are hand-off from the *catalog description* file, +[`locale/AmiAuth.cd`](https://github.com/sidick/amiauth/blob/main/locale/AmiAuth.cd) +in the source repository — the standard `catcomp`/`FlexCat` format used +across the AmigaOS ecosystem, so existing translator tooling works +unmodified: + +1. Get [FlexCat](https://github.com/adtools/flexcat) (or another + `.cd`/`.ct`-aware tool, e.g. CatEditor). +2. Generate a starting `.ct` translation file from `AmiAuth.cd`, or edit an + existing one for your language. +3. Translate each string, keeping any `%s`/`%lu`-style placeholders in + place and in the same order as the English original — they're filled in + with values like account names or error details at runtime. +4. Compile it to a `.catalog` file (`FlexCat AmiAuth.cd yourlanguage.ct + CATALOG AmiAuth.catalog`) and test it by installing it as described + above. +5. Open a pull request with your `.ct` file, or attach it to an issue, at + [github.com/sidick/amiauth](https://github.com/sidick/amiauth). + +**Do not translate:** CLI subcommand keywords (`CODE`, `GET`, `INIT`, ...), +`ReadArgs` template field names, or [ARexx port](ARexx-Port.md) command +keywords (`GETCODE`, `STATUS`, ...) — none of these are in `AmiAuth.cd` in +the first place, since they're the scripting protocol, not user-facing +prose, and must stay identical across every locale so scripts keep working +everywhere. + +## Baseline + +Locale.library catalogs require AmigaOS 2.1 (V38) — one step above AmiAuth's +own CLI baseline of 2.04 (V37). This is handled the same way as every other +optional library in AmiAuth: absence of locale.library itself (a plain 2.04 +system) is not an error, just English text, same as an absent or +non-matching catalog. diff --git a/userdocs/Settings-Reference.md b/userdocs/Settings-Reference.md index ea9f147..74c36d7 100644 --- a/userdocs/Settings-Reference.md +++ b/userdocs/Settings-Reference.md @@ -15,6 +15,10 @@ The vault itself is **not** here — it is user data, not a preference, and lives in `PROGDIR:` by default (see [Vault and Passphrases](Vault-and-Passphrases.md)). `ENV:` is RAM-backed and would be lost on reboot. +AmiAuth's display language is likewise **not** an AmiAuth setting — it +follows your system's own **Locale** preference, via a `.catalog` +translation file. See [Localization](Localization.md). + ## Variables | Variable | Type | Written by | Meaning | diff --git a/userdocs/index.md b/userdocs/index.md index 45f7db2..9df6102 100644 --- a/userdocs/index.md +++ b/userdocs/index.md @@ -40,6 +40,7 @@ ReAction/ClassAct. | [Vault and Passphrases](Vault-and-Passphrases.md) | Encryption, always-unlocked mode, re-keying, backups | | [Time and Clock Sync](Time-and-Clock-Sync.md) | SNTP, UTC offsets, and the red/amber/green indicator | | [Settings Reference](Settings-Reference.md) | Every `ENVARC:AmiAuth/` setting | +| [Localization](Localization.md) | Installing and contributing translations | | [Security Model](Security-Model.md) | What the vault does and does not protect — read this | | [Troubleshooting and FAQ](Troubleshooting-and-FAQ.md) | Common problems and questions | | [Building from Source](Building-from-Source.md) | Host and m68k builds, tests |