Adding gender to the config
build / build (push) Successful in 19s

This commit is contained in:
ml
2026-07-16 22:49:06 +02:00
parent 1ebbfebe31
commit 7a77c4a07d
26 changed files with 470 additions and 109 deletions
+40 -12
View File
@@ -71,25 +71,53 @@ aspect.square=Quadrat
aspect.trine=Trigon
aspect.opposition=Opposition
# --- Positionen des Keltischen Kreuzes (tarot_data.c) ---
# --- Positionen des Keltischen Kreuzes (tarot_data.c). Waite spricht die
# fragende Person durchgehend mit "ihn/ihm/sein" an; der unsuffigierte
# Schlüssel ist eine geschlechtsneutrale Umschreibung (über "die fragende
# Person"/"die eigene", ohne Pronomen), die bei nicht angegebenem
# Geschlecht verwendet wird, ".male" ist Waites eigener Wortlaut
# unverändert, ".female" eine für dieses Projekt geschriebene Umschreibung -
# siehe tarot_data.c's Kommentar zu Gender. ---
position.present.name=Die Gegenwart
position.present.desc=Dies bedeckt ihn: der allgemeine Einfluss, der die Angelegenheit betrifft.
position.present.desc=Dies bedeckt die fragende Person: der allgemeine Einfluss, der die Angelegenheit betrifft.
position.present.desc.male=Dies bedeckt ihn: der allgemeine Einfluss, der die Angelegenheit betrifft.
position.present.desc.female=Dies bedeckt sie: der allgemeine Einfluss, der die Angelegenheit betrifft.
position.challenge.name=Die Herausforderung
position.challenge.desc=Dies kreuzt ihn: die Art des Hindernisses in der Angelegenheit.
position.challenge.desc=Dies kreuzt die fragende Person: die Art des Hindernisses in der Angelegenheit.
position.challenge.desc.male=Dies kreuzt ihn: die Art des Hindernisses in der Angelegenheit.
position.challenge.desc.female=Dies kreuzt sie: die Art des Hindernisses in der Angelegenheit.
position.crown.name=Die Krone
position.crown.desc=Dies krönt ihn: das Ziel oder Ideal, das Beste, was erreicht werden kann.
position.crown.desc=Dies krönt die fragende Person: das Ziel oder Ideal, das Beste, was erreicht werden kann.
position.crown.desc.male=Dies krönt ihn: das Ziel oder Ideal, das Beste, was erreicht werden kann.
position.crown.desc.female=Dies krönt sie: das Ziel oder Ideal, das Beste, was erreicht werden kann.
position.foundation.name=Das Fundament
position.foundation.desc=Dies liegt unter ihm: die Grundlage der Angelegenheit, bereits Wirklichkeit.
position.foundation.desc=Dies liegt unter der fragenden Person: die Grundlage der Angelegenheit, bereits Wirklichkeit.
position.foundation.desc.male=Dies liegt unter ihm: die Grundlage der Angelegenheit, bereits Wirklichkeit.
position.foundation.desc.female=Dies liegt unter ihr: die Grundlage der Angelegenheit, bereits Wirklichkeit.
position.recent_past.name=Die jüngste Vergangenheit
position.recent_past.desc=Dies liegt hinter ihm: der Einfluss, der gerade vergeht.
position.recent_past.desc=Dies liegt hinter der fragenden Person: der Einfluss, der gerade vergeht.
position.recent_past.desc.male=Dies liegt hinter ihm: der Einfluss, der gerade vergeht.
position.recent_past.desc.female=Dies liegt hinter ihr: der Einfluss, der gerade vergeht.
position.near_future.name=Die nahe Zukunft
position.near_future.desc=Dies liegt vor ihm: der Einfluss, der jetzt wirksam wird.
position.attitude.name=Er selbst
position.attitude.desc=Seine Haltung oder Einstellung in den gegebenen Umständen.
position.environment.name=Sein Haus
position.environment.desc=Sein Umfeld und die darin wirkenden Tendenzen.
position.near_future.desc=Dies liegt vor der fragenden Person: der Einfluss, der jetzt wirksam wird.
position.near_future.desc.male=Dies liegt vor ihm: der Einfluss, der jetzt wirksam wird.
position.near_future.desc.female=Dies liegt vor ihr: der Einfluss, der jetzt wirksam wird.
position.attitude.name=Die fragende Person selbst
position.attitude.name.male=Er selbst
position.attitude.name.female=Sie selbst
position.attitude.desc=Die eigene Haltung oder Einstellung in den gegebenen Umständen.
position.attitude.desc.male=Seine Haltung oder Einstellung in den gegebenen Umständen.
position.attitude.desc.female=Ihre Haltung oder Einstellung in den gegebenen Umständen.
position.environment.name=Das eigene Haus
position.environment.name.male=Sein Haus
position.environment.name.female=Ihr Haus
position.environment.desc=Das eigene Umfeld und die darin wirkenden Tendenzen.
position.environment.desc.male=Sein Umfeld und die darin wirkenden Tendenzen.
position.environment.desc.female=Ihr Umfeld und die darin wirkenden Tendenzen.
position.hopes_and_fears.name=Hoffnungen und Ängste
position.hopes_and_fears.desc=Seine Hoffnungen oder Ängste in der Angelegenheit.
position.hopes_and_fears.desc=Die eigenen Hoffnungen oder Ängste in der Angelegenheit.
position.hopes_and_fears.desc.male=Seine Hoffnungen oder Ängste in der Angelegenheit.
position.hopes_and_fears.desc.female=Ihre Hoffnungen oder Ängste in der Angelegenheit.
position.outcome.name=Das Ergebnis
position.outcome.desc=Was kommen wird: das endgültige Ergebnis der Angelegenheit.
+39 -12
View File
@@ -79,25 +79,52 @@ aspect.trine=Trine
aspect.opposition=Opposition
# --- Celtic Cross position names/descriptions (tarot_data.c), Waite's
# own wording from "An Ancient Celtic Method of Divination" ---
# own wording from "An Ancient Celtic Method of Divination". Waite
# addresses the querent as "him" throughout; the unsuffixed key is a
# pronoun-neutral paraphrase (singular "they") used when no gender is
# configured, the ".male" key is Waite's own wording verbatim, and the
# ".female" key is an original paraphrase - see tarot_data.c's own
# comment on Gender. ---
position.present.name=The Present
position.present.desc=This covers him: the general influence affecting the matter.
position.present.desc=This covers them: the general influence affecting the matter.
position.present.desc.male=This covers him: the general influence affecting the matter.
position.present.desc.female=This covers her: the general influence affecting the matter.
position.challenge.name=The Challenge
position.challenge.desc=This crosses him: the nature of the obstacle in the matter.
position.challenge.desc=This crosses them: the nature of the obstacle in the matter.
position.challenge.desc.male=This crosses him: the nature of the obstacle in the matter.
position.challenge.desc.female=This crosses her: the nature of the obstacle in the matter.
position.crown.name=The Crown
position.crown.desc=This crowns him: the aim or ideal, the best that can be achieved.
position.crown.desc=This crowns them: the aim or ideal, the best that can be achieved.
position.crown.desc.male=This crowns him: the aim or ideal, the best that can be achieved.
position.crown.desc.female=This crowns her: the aim or ideal, the best that can be achieved.
position.foundation.name=The Foundation
position.foundation.desc=This is beneath him: the basis of the matter, already actual.
position.foundation.desc=This is beneath them: the basis of the matter, already actual.
position.foundation.desc.male=This is beneath him: the basis of the matter, already actual.
position.foundation.desc.female=This is beneath her: the basis of the matter, already actual.
position.recent_past.name=The Recent Past
position.recent_past.desc=This is behind him: the influence that is just passing away.
position.recent_past.desc=This is behind them: the influence that is just passing away.
position.recent_past.desc.male=This is behind him: the influence that is just passing away.
position.recent_past.desc.female=This is behind her: the influence that is just passing away.
position.near_future.name=The Near Future
position.near_future.desc=This is before him: the influence now coming into action.
position.attitude.name=Himself
position.attitude.desc=His position or attitude in the circumstances.
position.environment.name=His House
position.environment.desc=His environment and the tendencies at work therein.
position.near_future.desc=This is before them: the influence now coming into action.
position.near_future.desc.male=This is before him: the influence now coming into action.
position.near_future.desc.female=This is before her: the influence now coming into action.
position.attitude.name=Themself
position.attitude.name.male=Himself
position.attitude.name.female=Herself
position.attitude.desc=Their position or attitude in the circumstances.
position.attitude.desc.male=His position or attitude in the circumstances.
position.attitude.desc.female=Her position or attitude in the circumstances.
position.environment.name=Their House
position.environment.name.male=His House
position.environment.name.female=Her House
position.environment.desc=Their environment and the tendencies at work therein.
position.environment.desc.male=His environment and the tendencies at work therein.
position.environment.desc.female=Her environment and the tendencies at work therein.
position.hopes_and_fears.name=Hopes and Fears
position.hopes_and_fears.desc=His hopes or fears in the matter.
position.hopes_and_fears.desc=Their hopes or fears in the matter.
position.hopes_and_fears.desc.male=His hopes or fears in the matter.
position.hopes_and_fears.desc.female=Her hopes or fears in the matter.
position.outcome.name=The Outcome
position.outcome.desc=What will come: the final result of the matter.
+3
View File
@@ -29,6 +29,7 @@ birth_time=""
birth_utc_offset=""
birth_lat=""
birth_lon=""
gender="unspecified"
lang="en"
while IFS='=' read -r key value || [ -n "$key" ]; do
@@ -43,6 +44,7 @@ while IFS='=' read -r key value || [ -n "$key" ]; do
birth_utc_offset) birth_utc_offset="$value" ;;
birth_lat) birth_lat="$value" ;;
birth_lon) birth_lon="$value" ;;
gender) [ -n "$value" ] && gender="$value" ;;
lang) [ -n "$value" ] && lang="$value" ;;
esac
done <"$PROPERTIES_FILE"
@@ -77,5 +79,6 @@ exec "$BINARY" \
--birth-lon "$birth_lon" \
--date "$transit_date" \
--format "$format" \
--gender "$gender" \
--lang "$lang" \
--i18n-dir "$SCRIPT_DIR/i18n"
+3
View File
@@ -41,6 +41,7 @@ birth_time=""
birth_utc_offset=""
birth_lat=""
birth_lon=""
gender="unspecified"
lang="en"
while IFS='=' read -r key value || [ -n "$key" ]; do
@@ -55,6 +56,7 @@ while IFS='=' read -r key value || [ -n "$key" ]; do
birth_utc_offset) birth_utc_offset="$value" ;;
birth_lat) birth_lat="$value" ;;
birth_lon) birth_lon="$value" ;;
gender) [ -n "$value" ] && gender="$value" ;;
lang) [ -n "$value" ] && lang="$value" ;;
esac
done <"$PROPERTIES_FILE"
@@ -92,4 +94,5 @@ lang="${2:-$lang}"
--birth-lon "$birth_lon" \
--date "$transit_date" \
--format json \
--gender "$gender" \
| exec "$INTERPRETER_BINARY" --format "$format" --lang "$lang" --i18n-dir "$SCRIPT_DIR/i18n"
+7
View File
@@ -11,6 +11,12 @@
# (e.g. 2 for Central European Summer Time)
# birth_lat Birth location latitude, decimal degrees, north positive
# birth_lon Birth location longitude, decimal degrees, east positive
# gender Who the reading is for - male, female, or unspecified.
# Only affects the Celtic Cross position text's
# pronouns (e.g. "His House" vs. "Her House" vs.
# "Their House"), never the chart/draw itself.
# Optional, defaults to "unspecified"; unrecognized
# values fall back to "unspecified" too.
# lang Reading language code - matches a file named
# <code>.lang in dist/i18n/ (see engine/i18n/ for the
# source files, currently "en" and "de"). Optional,
@@ -22,4 +28,5 @@ birth_time=YOUR_BIRTH_TIME_HERE
birth_utc_offset=YOUR_UTC_OFFSET_HERE
birth_lat=YOUR_LATITUDE_HERE
birth_lon=YOUR_LONGITUDE_HERE
gender=unspecified
lang=en
+16 -1
View File
@@ -15,12 +15,16 @@ static void print_usage(const char *prog) {
"Usage: %s --seed <string> --birth-date YYYY-MM-DD --birth-time HH:MM\n"
" --birth-utc-offset <hours> --birth-lat <deg> --birth-lon <deg>\n"
" [--date YYYY-MM-DDTHH:MM] [--format text|html|json]\n"
" [--gender male|female|unspecified]\n"
" [--lang <code>] [--i18n-dir <path>]\n\n"
" --seed Tarot seed string; same seed -> same Celtic Cross spread.\n"
" --birth-date/time Birth date/time in local clock time.\n"
" --birth-utc-offset Hours to subtract from local birth time to get UTC (e.g. 2 for CEST).\n"
" --birth-lat/lon Birth location in decimal degrees (north/east positive).\n"
" --date UTC moment for today's transits/tarot draw. Defaults to now.\n"
" --gender Who the reading is for - only affects the Celtic Cross\n"
" position text's pronouns, never the chart/draw itself.\n"
" Defaults to unspecified (pronoun-neutral phrasing).\n"
" --format Output format, defaults to text. html links card art via\n"
" img/<file>, i.e. it expects to be saved next to the img/\n"
" directory that `make images` copies into dist/. json uses\n"
@@ -63,6 +67,7 @@ int main(int argc, char **argv) {
const char *birth_utc_offset = NULL, *birth_lat = NULL, *birth_lon = NULL;
const char *date_str = NULL, *format_str = "text";
const char *lang = "en", *i18n_dir_arg = NULL;
const char *gender_str = "unspecified";
for (int i = 1; i < argc; i++) {
const char *arg = argv[i];
@@ -75,6 +80,7 @@ int main(int argc, char **argv) {
else if (strcmp(arg, "--birth-lon") == 0) rc = require_arg(argc, argv, &i, arg, &birth_lon);
else if (strcmp(arg, "--date") == 0) rc = require_arg(argc, argv, &i, arg, &date_str);
else if (strcmp(arg, "--format") == 0) rc = require_arg(argc, argv, &i, arg, &format_str);
else if (strcmp(arg, "--gender") == 0) rc = require_arg(argc, argv, &i, arg, &gender_str);
else if (strcmp(arg, "--lang") == 0) rc = require_arg(argc, argv, &i, arg, &lang);
else if (strcmp(arg, "--i18n-dir") == 0) rc = require_arg(argc, argv, &i, arg, &i18n_dir_arg);
else if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
@@ -136,6 +142,15 @@ int main(int argc, char **argv) {
return 1;
}
Gender gender;
if (strcmp(gender_str, "unspecified") == 0) gender = GENDER_UNSPECIFIED;
else if (strcmp(gender_str, "male") == 0) gender = GENDER_MALE;
else if (strcmp(gender_str, "female") == 0) gender = GENDER_FEMALE;
else {
fprintf(stderr, "Invalid --gender, expected male, female, or unspecified\n");
return 1;
}
char i18n_path[512];
if (i18n_dir_arg) {
snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_dir_arg, lang);
@@ -148,7 +163,7 @@ int main(int argc, char **argv) {
}
DailyReading reading;
reading_generate(seed, &birth, utc_moment, &reading);
reading_generate(seed, &birth, gender, utc_moment, &reading);
if (format == FORMAT_HTML) {
reading_print_html(&reading, stdout);
+7 -5
View File
@@ -3,9 +3,10 @@
#include <math.h>
void reading_generate(const char *tarot_seed, const BirthData *birth,
void reading_generate(const char *tarot_seed, const BirthData *birth, Gender gender,
time_t utc_moment, DailyReading *out) {
out->utc_moment = utc_moment;
out->gender = gender;
astro_compute_natal_chart(birth, &out->natal);
astro_compute_daily_transits(utc_moment, &out->natal, &out->transits);
tarot_draw_celtic_cross(tarot_seed, &out->spread);
@@ -66,10 +67,10 @@ void reading_print_text(const DailyReading *r, FILE *out) {
fprintf(out, "===== %s =====\n", ui("ui.celtic_cross", "Celtic Cross"));
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
const TarotDraw *draw = &r->spread.positions[i];
fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticCrossPosition)i),
fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticCrossPosition)i, r->gender),
tarot_card_name(draw->card), draw->reversed ? " " : "",
draw->reversed ? ui("ui.reversed", "(Reversed)") : "");
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i));
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i, r->gender));
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
}
}
@@ -138,12 +139,12 @@ void reading_print_html(const DailyReading *r, FILE *out) {
" <div class=\"desc\">%s</div>\n"
" <div class=\"meaning\">%s</div>\n"
"</div>\n",
tarot_position_name((CelticCrossPosition)i),
tarot_position_name((CelticCrossPosition)i, r->gender),
draw->reversed ? "reversed" : "",
tarot_card_image_file(draw->card), tarot_card_name(draw->card),
tarot_card_name(draw->card), draw->reversed ? " " : "",
draw->reversed ? ui("ui.reversed", "(Reversed)") : "",
tarot_position_description((CelticCrossPosition)i),
tarot_position_description((CelticCrossPosition)i, r->gender),
tarot_card_meaning(draw->card, draw->reversed));
}
fprintf(out, "</div>\n</body></html>\n");
@@ -163,6 +164,7 @@ void reading_print_json(const DailyReading *r, FILE *out) {
struct tm *utc = gmtime(&r->utc_moment);
fprintf(out, " \"date\": \"%04d-%02d-%02d\",\n", utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday);
fprintf(out, " \"gender\": \"%s\",\n", tarot_gender_slug(r->gender));
fprintf(out, " \"natal\": {\n \"bodies\": [\n");
for (int b = 0; b < NUM_BODIES; b++) {
+5 -1
View File
@@ -21,6 +21,10 @@ typedef struct {
* interpreter side) gets it for free without threading it through
* separately. */
time_t utc_moment;
/* Who the reading is for - only affects the pronouns in the Celtic
* Cross position text (see tarot.h's Gender), set by reading_generate()
* the same way utc_moment is, so every caller gets it for free. */
Gender gender;
NatalChart natal;
DailyTransits transits;
CelticCrossSpread spread;
@@ -28,7 +32,7 @@ typedef struct {
/* The real API: this is the only function the watchapp needs to call.
* It walks the returned struct directly to lay out its own screens. */
void reading_generate(const char *tarot_seed, const BirthData *birth,
void reading_generate(const char *tarot_seed, const BirthData *birth, Gender gender,
time_t utc_moment, DailyReading *out);
/* Desktop-only output formats for inspecting a reading before any watch
+14 -2
View File
@@ -52,6 +52,17 @@ typedef struct {
bool reversed;
} TarotDraw;
/* Who the reading is for - affects only the pronouns in the Celtic
* Cross position text below (Waite's original wording addresses the
* querent as "him"), never anything about the tarot draw or astrology
* math itself. GENDER_UNSPECIFIED is the default and renders
* pronoun-neutral phrasing (singular "they" in English). */
typedef enum {
GENDER_UNSPECIFIED = 0,
GENDER_MALE,
GENDER_FEMALE
} Gender;
typedef struct {
/* Indexed by CelticCrossPosition. */
TarotDraw positions[TAROT_SPREAD_SIZE];
@@ -67,8 +78,8 @@ const char *tarot_card_name(TarotCard card);
const char *tarot_card_image_file(TarotCard card); /* matches res/img/ */
const char *tarot_card_meaning(TarotCard card, bool reversed);
const char *tarot_position_name(CelticCrossPosition position);
const char *tarot_position_description(CelticCrossPosition position);
const char *tarot_position_name(CelticCrossPosition position, Gender gender);
const char *tarot_position_description(CelticCrossPosition position, Gender gender);
/* Stable, language-independent identifiers (e.g. "high_priestess",
* "wheel_of_fortune", "recent_past") - the same strings i18n.c's lookup
@@ -76,5 +87,6 @@ const char *tarot_position_description(CelticCrossPosition position);
* (reading_print_json) that must stay identical regardless of --lang. */
const char *tarot_card_slug(TarotCard card);
const char *tarot_position_slug(CelticCrossPosition position);
const char *tarot_gender_slug(Gender gender); /* "unspecified"/"male"/"female" */
#endif
+112 -34
View File
@@ -130,52 +130,97 @@ static const TarotCardInfo k_cards[TAROT_DECK_SIZE] = {
/* Position names and one-line descriptions follow Waite's own account
* of "An Ancient Celtic Method of Divination" (Part III §7), in his
* original drawing order. */
* original drawing order. Waite addresses the querent as "him"
* throughout; that wording is kept verbatim as the GENDER_MALE variant
* below, and GENDER_FEMALE/GENDER_UNSPECIFIED are original paraphrases
* written for this project (not Waite's own text) so the reading can
* address the person it's actually for. Each array is indexed by
* Gender (tarot.h): [GENDER_UNSPECIFIED], [GENDER_MALE], [GENDER_FEMALE]. */
typedef struct {
const char *name;
const char *description;
const char *name[3];
const char *description[3];
} PositionInfo;
static const PositionInfo k_positions[TAROT_SPREAD_SIZE] = {
[POSITION_PRESENT] = {
"The Present",
"This covers him: the general influence affecting the matter."
{ "The Present", "The Present", "The Present" },
{
"This covers them: the general influence affecting the matter.",
"This covers him: the general influence affecting the matter.",
"This covers her: the general influence affecting the matter.",
}
},
[POSITION_CHALLENGE] = {
"The Challenge",
"This crosses him: the nature of the obstacle in the matter."
{ "The Challenge", "The Challenge", "The Challenge" },
{
"This crosses them: the nature of the obstacle in the matter.",
"This crosses him: the nature of the obstacle in the matter.",
"This crosses her: the nature of the obstacle in the matter.",
}
},
[POSITION_CROWN] = {
"The Crown",
"This crowns him: the aim or ideal, the best that can be achieved."
{ "The Crown", "The Crown", "The Crown" },
{
"This crowns them: the aim or ideal, the best that can be achieved.",
"This crowns him: the aim or ideal, the best that can be achieved.",
"This crowns her: the aim or ideal, the best that can be achieved.",
}
},
[POSITION_FOUNDATION] = {
"The Foundation",
"This is beneath him: the basis of the matter, already actual."
{ "The Foundation", "The Foundation", "The Foundation" },
{
"This is beneath them: the basis of the matter, already actual.",
"This is beneath him: the basis of the matter, already actual.",
"This is beneath her: the basis of the matter, already actual.",
}
},
[POSITION_RECENT_PAST] = {
"The Recent Past",
"This is behind him: the influence that is just passing away."
{ "The Recent Past", "The Recent Past", "The Recent Past" },
{
"This is behind them: the influence that is just passing away.",
"This is behind him: the influence that is just passing away.",
"This is behind her: the influence that is just passing away.",
}
},
[POSITION_NEAR_FUTURE] = {
"The Near Future",
"This is before him: the influence now coming into action."
{ "The Near Future", "The Near Future", "The Near Future" },
{
"This is before them: the influence now coming into action.",
"This is before him: the influence now coming into action.",
"This is before her: the influence now coming into action.",
}
},
[POSITION_ATTITUDE] = {
"Himself",
"His position or attitude in the circumstances."
{ "Themself", "Himself", "Herself" },
{
"Their position or attitude in the circumstances.",
"His position or attitude in the circumstances.",
"Her position or attitude in the circumstances.",
}
},
[POSITION_ENVIRONMENT] = {
"His House",
"His environment and the tendencies at work therein."
{ "Their House", "His House", "Her House" },
{
"Their environment and the tendencies at work therein.",
"His environment and the tendencies at work therein.",
"Her environment and the tendencies at work therein.",
}
},
[POSITION_HOPES_AND_FEARS] = {
"Hopes and Fears",
"His hopes or fears in the matter."
{ "Hopes and Fears", "Hopes and Fears", "Hopes and Fears" },
{
"Their hopes or fears in the matter.",
"His hopes or fears in the matter.",
"Her hopes or fears in the matter.",
}
},
[POSITION_OUTCOME] = {
"The Outcome",
"What will come: the final result of the matter."
{ "The Outcome", "The Outcome", "The Outcome" },
{
"What will come: the final result of the matter.",
"What will come: the final result of the matter.",
"What will come: the final result of the matter.",
}
},
};
@@ -229,13 +274,44 @@ static const char *card_field(const char *slug, const char *field, const char *f
return i18n_get(key, fallback);
}
static const char *position_field(const char *slug, const char *field, const char *fallback) {
char key[64];
strcpy(key, "position.");
strcat(key, slug);
strcat(key, ".");
strcat(key, field);
return i18n_get(key, fallback);
/* Suffix appended to the i18n key for the male/female variants - the
* unspecified/neutral variant uses the plain, unsuffixed key so it
* matches the field name callers already know (e.g. "position.
* attitude.name"), same convention as card_field()'s upright/reversed
* field names above. */
static const char *const k_gender_suffix[3] = {
[GENDER_UNSPECIFIED] = "",
[GENDER_MALE] = ".male",
[GENDER_FEMALE] = ".female",
};
static const char *const k_gender_slug[3] = {
[GENDER_UNSPECIFIED] = "unspecified",
[GENDER_MALE] = "male",
[GENDER_FEMALE] = "female",
};
static const char *position_field(const char *slug, const char *field, Gender gender,
const char *fallback) {
char neutral_key[80];
strcpy(neutral_key, "position.");
strcat(neutral_key, slug);
strcat(neutral_key, ".");
strcat(neutral_key, field);
if (gender == GENDER_UNSPECIFIED) return i18n_get(neutral_key, fallback);
/* Not every position's text actually varies by gender (e.g. "The
* Outcome" has no pronoun) - the .lang files only define a ".male"/
* ".female" key where the wording differs. So look up the neutral key
* first and use *that* (translated, if a catalog is loaded) as the
* fallback for the gendered key, rather than jumping straight past a
* loaded translation to the hardcoded English `fallback` whenever the
* gendered key happens to be absent. */
const char *neutral_text = i18n_get(neutral_key, fallback);
char key[88];
strcpy(key, neutral_key);
strcat(key, k_gender_suffix[gender]);
return i18n_get(key, neutral_text);
}
const char *tarot_card_name(TarotCard card) {
@@ -252,13 +328,15 @@ const char *tarot_card_meaning(TarotCard card, bool reversed) {
reversed ? k_cards[card].reversed : k_cards[card].upright);
}
const char *tarot_position_name(CelticCrossPosition position) {
return position_field(k_position_slug[position], "name", k_positions[position].name);
const char *tarot_position_name(CelticCrossPosition position, Gender gender) {
return position_field(k_position_slug[position], "name", gender, k_positions[position].name[gender]);
}
const char *tarot_position_description(CelticCrossPosition position) {
return position_field(k_position_slug[position], "desc", k_positions[position].description);
const char *tarot_position_description(CelticCrossPosition position, Gender gender) {
return position_field(k_position_slug[position], "desc", gender,
k_positions[position].description[gender]);
}
const char *tarot_card_slug(TarotCard card) { return k_card_slug[card]; }
const char *tarot_position_slug(CelticCrossPosition position) { return k_position_slug[position]; }
const char *tarot_gender_slug(Gender gender) { return k_gender_slug[gender]; }
+38 -4
View File
@@ -72,8 +72,8 @@ static void test_reading_generate_smoke(void) {
};
DailyReading r1, r2;
reading_generate("smoke-test-seed", &birth, 1751500000, &r1);
reading_generate("smoke-test-seed", &birth, 1751500000, &r2);
reading_generate("smoke-test-seed", &birth, GENDER_UNSPECIFIED, 1751500000, &r1);
reading_generate("smoke-test-seed", &birth, GENDER_UNSPECIFIED, 1751500000, &r2);
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
assert(r1.spread.positions[i].card == r2.spread.positions[i].card);
@@ -103,7 +103,7 @@ static void test_reading_print_json_shape(void) {
};
DailyReading r;
reading_generate("smoke-test-seed", &birth, 1751500000, &r);
reading_generate("smoke-test-seed", &birth, GENDER_FEMALE, 1751500000, &r);
FILE *tmp = tmpfile();
assert(tmp);
@@ -121,6 +121,7 @@ static void test_reading_print_json_shape(void) {
* into what must stay language-independent regardless of --lang). */
assert(buf[0] == '{');
assert(strstr(buf, "\"date\": \"2025-07-02\"") != NULL); /* UTC date of the fixed 1751500000 seed */
assert(strstr(buf, "\"gender\": \"female\"") != NULL);
assert(strstr(buf, "\"natal\"") != NULL);
assert(strstr(buf, "\"transits\"") != NULL);
assert(strstr(buf, "\"spread\"") != NULL);
@@ -155,7 +156,7 @@ static void test_i18n_fallback_and_translation(void) {
assert(strcmp(astro_moon_phase_name(MOON_FULL), "Vollmond") == 0);
assert(strcmp(astro_aspect_name(ASPECT_TRINE), "Trigon") == 0);
assert(strcmp(tarot_card_name(CARD_WORLD), "Die Welt") == 0);
assert(strcmp(tarot_position_name(POSITION_OUTCOME), "Das Ergebnis") == 0);
assert(strcmp(tarot_position_name(POSITION_OUTCOME, GENDER_UNSPECIFIED), "Das Ergebnis") == 0);
/* Loading a file that doesn't exist fails and leaves the previously
* loaded catalog in place, rather than silently clearing it. */
@@ -170,12 +171,45 @@ static void test_i18n_fallback_and_translation(void) {
printf("PASS test_i18n_fallback_and_translation\n");
}
static void test_gendered_position_text(void) {
/* English catalog (loaded by the previous test): "Attitude" is the one
* position whose name itself carries a pronoun. */
assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_UNSPECIFIED), "Themself") == 0);
assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_MALE), "Himself") == 0);
assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_FEMALE), "Herself") == 0);
assert(strstr(tarot_position_description(POSITION_PRESENT, GENDER_UNSPECIFIED), "them") != NULL);
assert(strstr(tarot_position_description(POSITION_PRESENT, GENDER_MALE), "him") != NULL);
assert(strstr(tarot_position_description(POSITION_PRESENT, GENDER_FEMALE), "her") != NULL);
/* The Outcome position has no pronoun in Waite's own text - identical
* across all three genders. */
assert(strcmp(tarot_position_description(POSITION_OUTCOME, GENDER_MALE),
tarot_position_description(POSITION_OUTCOME, GENDER_FEMALE)) == 0);
assert(strcmp(tarot_position_description(POSITION_OUTCOME, GENDER_MALE),
tarot_position_description(POSITION_OUTCOME, GENDER_UNSPECIFIED)) == 0);
bool loaded = i18n_load("engine/i18n/de.lang");
assert(loaded);
assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_MALE), "Er selbst") == 0);
assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_FEMALE), "Sie selbst") == 0);
assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_UNSPECIFIED),
"Die fragende Person selbst") == 0);
assert(strcmp(tarot_position_description(POSITION_ENVIRONMENT, GENDER_MALE),
"Sein Umfeld und die darin wirkenden Tendenzen.") == 0);
loaded = i18n_load("engine/i18n/en.lang"); /* leave the catalog in a known state */
assert(loaded);
printf("PASS test_gendered_position_text\n");
}
int main(void) {
test_tarot_determinism();
test_natal_sun_sign();
test_reading_generate_smoke();
test_reading_print_json_shape();
test_i18n_fallback_and_translation();
test_gendered_position_text();
printf("All smoke tests passed.\n");
return 0;
}