diff --git a/src/Makefile.in b/src/Makefile.in index a4fabaae..e2ae2c13 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -29,7 +29,7 @@ MANS= $(srcdir)/../man/rem2ps.1 $(srcdir)/../man/remind.1 \ REMINDSRCS= calendar.c dedupe.c dynbuf.c dorem.c dosubst.c expr.c \ files.c funcs.c globals.c hashtab.c hashtab_stats.c \ hbcal.c init.c main.c md5.c moon.c omit.c queue.c \ - sort.c token.c trigger.c userfns.c utils.c var.c + sort.c token.c trans.c trigger.c userfns.c utils.c var.c REMINDHDRS=config.h custom.h dynbuf.h err.h globals.h hashtab.h \ lang.h md5.h protos.h rem2ps.h types.h version.h diff --git a/src/calendar.c b/src/calendar.c index a1942651..7118af06 100644 --- a/src/calendar.c +++ b/src/calendar.c @@ -1711,6 +1711,7 @@ static void GenerateCalEntries(int col) case T_Push: r=PushOmitContext(&p); break; case T_Preserve: r=DoPreserve(&p); break; case T_Expr: r = DoExpr(&p); break; + case T_Translate: r = DoTranslate(&p); break; case T_RemType: if (tok.val == RUN_TYPE) { r=DoRun(&p); break; diff --git a/src/funcs.c b/src/funcs.c index 1fd0377c..b50e5085 100644 --- a/src/funcs.c +++ b/src/funcs.c @@ -68,6 +68,7 @@ static int solstice_equinox_for_year(int y, int which); /* Function prototypes */ +static int F_ (func_info *); static int FADawn (func_info *); static int FADusk (func_info *); static int FAbs (func_info *); @@ -226,7 +227,7 @@ static int CacheHebYear, CacheHebMon, CacheHebDay; /* The array holding the built-in functions. */ BuiltinFunc Func[] = { /* Name minargs maxargs is_constant func newfunc*/ - + { "_", 1, 1, 0, F_, NULL }, { "abs", 1, 1, 1, FAbs, NULL }, { "access", 2, 2, 0, FAccess, NULL }, { "adawn", 0, 1, 0, FADawn, NULL}, @@ -372,6 +373,23 @@ static int RetStrVal(char const *s, func_info *info) } +/***************************************************************/ +/* */ +/* F_ - look up a string in the translation table */ +/* */ +/***************************************************************/ +static int F_(func_info *info) +{ + char const *translated; + ASSERT_TYPE(0, STR_TYPE); + translated = GetTranslatedString(ARGSTR(0)); + if (!translated) { + DCOPYVAL(RetVal, ARG(0)); + return OK; + } + return RetStrVal(translated, info); +} + /***************************************************************/ /* */ /* FStrlen - string length */ diff --git a/src/init.c b/src/init.c index 016471bd..4e1c1370 100644 --- a/src/init.c +++ b/src/init.c @@ -185,6 +185,8 @@ void InitRemind(int argc, char const *argv[]) /* Initialize user-defined functions hash table */ InitUserFunctions(); + InitTranslationTable(); + /* If stdout is a terminal, initialize $FormWidth to terminal width-8, but clamp to [20, 500] */ InitCalWidthAndFormWidth(STDOUT_FILENO); diff --git a/src/main.c b/src/main.c index 8c8d3989..2d2eeb65 100644 --- a/src/main.c +++ b/src/main.c @@ -71,6 +71,8 @@ exitfunc(void) fprintf(stderr, " Func hash: total = %d; maxlen = %d; avglen = %.3f\n", total, maxlen, avglen); get_dedupe_hash_stats(&total, &maxlen, &avglen); fprintf(stderr, "Dedup hash: total = %d; maxlen = %d; avglen = %.3f\n", total, maxlen, avglen); + get_translation_hash_stats(&total, &maxlen, &avglen); + fprintf(stderr, "Trans hash: total = %d; maxlen = %d; avglen = %.3f\n", total, maxlen, avglen); UnsetAllUserFuncs(); print_expr_nodes_stats(); } @@ -352,6 +354,7 @@ static void DoReminders(void) case T_Preserve: r=DoPreserve(&p); break; case T_Push: r=PushOmitContext(&p); break; case T_Expr: r = DoExpr(&p); break; + case T_Translate: r = DoTranslate(&p); break; case T_RemType: if (tok.val == RUN_TYPE) { r=DoRun(&p); } else { @@ -609,6 +612,9 @@ int ParseQuotedString(ParsePtr p, DynamicBuffer *dbuf) DBufFree(dbuf); c = ParseNonSpaceChar(p, &err, 0); if (err) return err; + if (!c) { + return E_EOLN; + } if (c != '"') { return E_MISS_QUOTE; } diff --git a/src/protos.h b/src/protos.h index bc3096aa..4558dbbb 100644 --- a/src/protos.h +++ b/src/protos.h @@ -87,6 +87,7 @@ void FromDSE (int dse, int *y, int *m, int *d); int JulianToGregorianOffset(int y, int m); int ParseChar (ParsePtr p, int *err, int peek); int ParseToken (ParsePtr p, DynamicBuffer *dbuf); +int ParseQuotedString (ParsePtr p, DynamicBuffer *dbuf); int ParseIdentifier (ParsePtr p, DynamicBuffer *dbuf); expr_node * ParseExpr(ParsePtr p, int *r); void print_expr_nodes_stats(void); @@ -111,6 +112,7 @@ int DoDebug (ParsePtr p); int DoBanner (ParsePtr p); int DoRun (ParsePtr p); int DoExpr (ParsePtr p); +int DoTranslate (ParsePtr p); int DoErrMsg (ParsePtr p); int ClearGlobalOmits (void); int DoClear (ParsePtr p); @@ -253,10 +255,14 @@ void print_remind_tokens(void); void get_var_hash_stats(int *total, int *maxlen, double *avglen); void get_userfunc_hash_stats(int *total, int *maxlen, double *avglen); void get_dedupe_hash_stats(int *total, int *maxlen, double *avglen); +void get_translation_hash_stats(int *total, int *maxlen, double *avglen); /* Dedupe code */ int ShouldDedupe(int trigger_date, int trigger_time, char const *body); void ClearDedupeTable(void); void InitDedupeTable(void); + void InitVars(void); void InitUserFunctions(void); +void InitTranslationTable(void); +char const *GetTranslatedString(char const *orig); diff --git a/src/token.c b/src/token.c index 91e09fef..a29267d5 100644 --- a/src/token.c +++ b/src/token.c @@ -111,6 +111,7 @@ Token TokArray[] = { { "third", 5, T_Ordinal, 2 }, { "through", 7, T_Through, 0 }, { "thursday", 3, T_WkDay, 3 }, + { "translate", 5, T_Translate, 0 }, { "tuesday", 3, T_WkDay, 1 }, { "unset", 5, T_UnSet, 0 }, { "until", 5, T_Until, 0 }, diff --git a/src/trans.c b/src/trans.c new file mode 100644 index 00000000..2ba6043e --- /dev/null +++ b/src/trans.c @@ -0,0 +1,271 @@ +/***************************************************************/ +/* */ +/* TRANS.C */ +/* */ +/* Functions to manage the translation table. Implements */ +/* the TRANSLATE keyword. */ +/* */ +/* This file is part of REMIND. */ +/* Copyright (C) 1992-2024 by Dianne Skoll */ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* */ +/***************************************************************/ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include +#include "types.h" +#include "globals.h" +#include "protos.h" +#include "err.h" + +/* The structure of a translation item */ +typedef struct xlat { + struct hash_link link; + char *orig; + char *translated; +} XlateItem; + +hash_table TranslationTable; + +/***************************************************************/ +/* */ +/* AllocateXlateItem - Allocate a new translation item */ +/* */ +/***************************************************************/ +static XlateItem * +AllocateXlateItem(char const *orig, char const *translated) +{ + XlateItem *item = NEW(XlateItem); + if (!item) { + return NULL; + } + + /* Allocate the string space in ONE go! */ + char *s = malloc(strlen(orig) + strlen(translated) + 2); + if (!s) { + free(item); + return NULL; + } + + item->orig = s; + item->translated = s + strlen(orig) + 1; + strcpy(item->orig, orig); + strcpy(item->translated, translated); + return item; +} + +/***************************************************************/ +/* */ +/* FreeXlateItem - Free a translation item */ +/* */ +/***************************************************************/ +static void +FreeXlateItem(XlateItem *item) +{ + if (!item) return; + + if (item->orig) { + free(item->orig); + } + free(item); +} + +static void +RemoveTranslation(XlateItem *item) +{ + hash_table_delete(&TranslationTable, item); + FreeXlateItem(item); +} + +/***************************************************************/ +/* */ +/* ClearTranslationTable - free all translation items */ +/* */ +/***************************************************************/ +static void +ClearTranslationTable(void) +{ + XlateItem *item; + XlateItem *next; + + item = hash_table_next(&TranslationTable, NULL); + while(item) { + next = hash_table_next(&TranslationTable, item); + RemoveTranslation(item); + item = next; + } +} + +static void +print_escaped_string(FILE *fp, char const *s) +{ + putc('"', fp); + while(*s) { + switch(*s) { + case '\a': putc('\\', fp); putc('a', fp); break; + case '\b': putc('\\', fp); putc('b', fp); break; + case '\f': putc('\\', fp); putc('f', fp); break; + case '\n': putc('\\', fp); putc('n', fp); break; + case '\r': putc('\\', fp); putc('r', fp); break; + case '\t': putc('\\', fp); putc('t', fp); break; + case '\v': putc('\\', fp); putc('v', fp); break; + case '"': putc('\\', fp); putc('"', fp); break; + case '\\': putc('\\', fp); putc('\\', fp); break; + default: + if (*s < 32) { + fprintf(fp, "\\x%02x", (unsigned int) *s); + } else { + putc(*s, fp); break; + } + } + s++; + } + putc('"', fp); +} + +/***************************************************************/ +/* */ +/* DumpTranslationTable - Dump the table to a file descriptor */ +/* */ +/***************************************************************/ +static void +DumpTranslationTable(FILE *fp) +{ + XlateItem *item; + + item = hash_table_next(&TranslationTable, NULL); + while(item) { + print_escaped_string(fp, item->orig); + fprintf(fp, " "); + print_escaped_string(fp, item->translated); + fprintf(fp, "\n"); + item = hash_table_next(&TranslationTable, item); + } +} + +static unsigned int +HashXlateItem(void *x) +{ + XlateItem *item = (XlateItem *) x; + return HashVal(item->orig); +} + +static int +CompareXlateItems(void *a, void *b) +{ + XlateItem *i = (XlateItem *) a; + XlateItem *j = (XlateItem *) b; + return strcmp(i->orig, j->orig); +} + +void +InitTranslationTable(void) +{ + hash_table_init(&TranslationTable, offsetof(XlateItem, link), + HashXlateItem, CompareXlateItems); +} + +static XlateItem * +FindTranslation(char const *orig) +{ + XlateItem *item; + XlateItem candidate; + candidate.orig = (char *) orig; + item = hash_table_find(&TranslationTable, &candidate); + return item; +} + +static int +InsertTranslation(char const *orig, char const *translated) +{ + XlateItem *item = FindTranslation(orig); + if (item) { + if (!strcmp(item->translated, translated)) { + /* Translation is the same; do nothing */ + return OK; + } + RemoveTranslation(item); + } + item = AllocateXlateItem(orig, translated); + if (!item) { + return E_NO_MEM; + } + hash_table_insert(&TranslationTable, item); + return OK; +} + +char const * +GetTranslatedString(char const *orig) +{ + XlateItem *item = FindTranslation(orig); + if (!item) return NULL; + return item->translated; +} + +int +DoTranslate(ParsePtr p) +{ + int r; + DynamicBuffer orig, translated; + DBufInit(&orig); + DBufInit(&translated); + int c; + + c = ParseNonSpaceChar(p, &r, 1); + if (r) return r; + if (c != '"') { + r = ParseToken(p, &orig); + if (r) return r; + if (!StrCmpi(DBufValue(&orig), "dump")) { + DumpTranslationTable(stdout); + return OK; + } + if (!StrCmpi(DBufValue(&orig), "clear")) { + ClearTranslationTable(); + return OK; + } + return E_PARSE_ERR; + } + + if ( (r=ParseQuotedString(p, &orig)) ) { + return r; + } + if ( (r=ParseQuotedString(p, &translated)) ) { + if (r == E_EOLN) { + XlateItem *item = FindTranslation(DBufValue(&orig)); + if (item) { + RemoveTranslation(item); + } + r = OK; + } + DBufFree(&orig); + return r; + } + + if ( (r=VerifyEoln(p)) ) { + DBufFree(&orig); + DBufFree(&translated); + return r; + } + r = InsertTranslation(DBufValue(&orig), DBufValue(&translated)); + DBufFree(&orig); + DBufFree(&translated); + return r; +} + +void +get_translation_hash_stats(int *total, int *maxlen, double *avglen) +{ + struct hash_table_stats s; + hash_table_get_stats(&TranslationTable, &s); + *total = s.num_entries; + *maxlen = s.max_len; + *avglen = s.avg_len; +} diff --git a/src/types.h b/src/types.h index 573b1e83..d1c3bcf9 100644 --- a/src/types.h +++ b/src/types.h @@ -218,7 +218,7 @@ enum TokTypes T_MaybeUncomputable, T_Month, T_NoQueue, T_Number, T_Omit, T_OmitFunc, T_Once, T_Ordinal, T_Pop, T_Preserve, T_Priority, T_Push,T_Rem, T_RemType, T_Rep, T_Scanfrom, T_Sched, T_Set, T_Skip, T_Tag, T_Through, - T_Time, T_UnSet, T_Until, T_Warn, T_WkDay, T_Year + T_Time, T_Translate, T_UnSet, T_Until, T_Warn, T_WkDay, T_Year }; /* The structure of a token */ diff --git a/tests/test.cmp b/tests/test.cmp index 82f21acd..1efeae50 100644 --- a/tests/test.cmp +++ b/tests/test.cmp @@ -5966,6 +5966,43 @@ Leaving UserFN msgsuffix(5000) => "\b on the same line" Hello on the same line +# Test TRANSLATE +set a _("Hello") +_("Hello") => "Hello" + +TRANSLATE "Hello" "Goodbye" +set a _("Hello") +_("Hello") => "Goodbye" + +TRANSLATE "Hello" "Hallo!!!!!" +set a _("Hello") +_("Hello") => "Hallo!!!!!" + +TRANSLATE DUMP +"Hello" "Hallo!!!!!" +TRANSLATE CLEAR +TRANSLATE DUMP +set a _("Hello") +_("Hello") => "Hello" + +TRANSLATE "Wookie" "Bar" +TRANSLATE "Bar" "Quux" +TRANSLATE "Quux" "OOOGHY" + +# Delete Bar translation +TRANSLATE "Bar" + +set a _("Wookie") +_("Wookie") => "Bar" +set a _("Bar") +_("Bar") => "Bar" +set a _("Quux") +_("Quux") => "OOOGHY" + +TRANSLATE DUMP +"Wookie" "Bar" +"Quux" "OOOGHY" + # Output expression-node stats DEBUG +s # Don't want Remind to queue reminders @@ -5973,6 +6010,7 @@ EXIT Var hash: total = 141; maxlen = 4; avglen = 1.785 Func hash: total = 17; maxlen = 3; avglen = 1.000 Dedup hash: total = 0; maxlen = 0; avglen = 0.000 +Trans hash: total = 2; maxlen = 1; avglen = 0.118 Expression nodes allocated: 128 Expression nodes high-water: 74 Expression nodes leaked: 0 @@ -13427,6 +13465,7 @@ No reminders. Var hash: total = 1; maxlen = 1; avglen = 0.059 Func hash: total = 0; maxlen = 0; avglen = 0.000 Dedup hash: total = 0; maxlen = 0; avglen = 0.000 +Trans hash: total = 0; maxlen = 0; avglen = 0.000 Expression nodes allocated: 512 Expression nodes high-water: 499 Expression nodes leaked: 0 @@ -13666,6 +13705,8 @@ special tag third through +trans +translate unset until warn @@ -13715,6 +13756,7 @@ wednesday # Built-in Functions +_ abs access adawn diff --git a/tests/test.rem b/tests/test.rem index 5c403766..55930902 100644 --- a/tests/test.rem +++ b/tests/test.rem @@ -1184,6 +1184,33 @@ REM MSG Hello FSET msgsuffix(x) char(8) + " on the same line" REM MSG Hello +# Test TRANSLATE +set a _("Hello") + +TRANSLATE "Hello" "Goodbye" +set a _("Hello") + +TRANSLATE "Hello" "Hallo!!!!!" +set a _("Hello") + +TRANSLATE DUMP +TRANSLATE CLEAR +TRANSLATE DUMP +set a _("Hello") + +TRANSLATE "Wookie" "Bar" +TRANSLATE "Bar" "Quux" +TRANSLATE "Quux" "OOOGHY" + +# Delete Bar translation +TRANSLATE "Bar" + +set a _("Wookie") +set a _("Bar") +set a _("Quux") + +TRANSLATE DUMP + # Output expression-node stats DEBUG +s # Don't want Remind to queue reminders