Compare commits
1 Commits
v0.2.0
...
7a77c4a07d
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a77c4a07d |
@@ -200,6 +200,23 @@ more common (but less original) ordering; don't use it as a reference for
|
|||||||
the position enum order. Card and position text in `tarot_data.c` is
|
the position enum order. Card and position text in `tarot_data.c` is
|
||||||
condensed from Waite's own wording in Part III §3 (public domain).
|
condensed from Waite's own wording in Part III §3 (public domain).
|
||||||
|
|
||||||
|
**Gender** (`Gender` in `tarot.h`: `GENDER_UNSPECIFIED`/`GENDER_MALE`/
|
||||||
|
`GENDER_FEMALE`) is who the reading is for — it only affects the
|
||||||
|
pronouns in the 9 Celtic Cross position names/descriptions that have one
|
||||||
|
(Waite addresses the querent as "him" throughout; every other position
|
||||||
|
text in this codebase — card meanings, narrative, guidance — is already
|
||||||
|
pronoun-neutral or direct "you" address, so nothing else varies with
|
||||||
|
it). `DailyReading.gender` is set by `reading_generate()` the same way
|
||||||
|
`utc_moment` is, `deck-engine`'s `--gender male|female|unspecified`
|
||||||
|
flag/`dist/user.properties`' `gender=` field/the watch config page's
|
||||||
|
Gender field all feed it, and `reading_print_json`/`reading_load_json`
|
||||||
|
carry it across the `deck-engine` → `interpreter-cli` JSON boundary
|
||||||
|
(`tarot_gender_slug()`) the same way `date` does. `GENDER_UNSPECIFIED`
|
||||||
|
is the default and renders pronoun-neutral phrasing (singular "they" in
|
||||||
|
English; German rephrases around a noun like "die fragende Person"
|
||||||
|
rather than a pronoun, since German has no equivalent singular-they
|
||||||
|
construction in common use).
|
||||||
|
|
||||||
### Translations (`i18n.c`, `engine/i18n/*.lang`)
|
### Translations (`i18n.c`, `engine/i18n/*.lang`)
|
||||||
|
|
||||||
All display text (planet/sign/moon-phase/aspect names, tarot card names
|
All display text (planet/sign/moon-phase/aspect names, tarot card names
|
||||||
@@ -217,7 +234,12 @@ Keys are namespaced by the *slug* tables in `tarot_data.c`/`astro.c`
|
|||||||
`moonphase.<slug>`, `aspect.<slug>`), plus `ui.*` keys for `reading.c`'s
|
`moonphase.<slug>`, `aspect.<slug>`), plus `ui.*` keys for `reading.c`'s
|
||||||
own labels — deliberately keyed off separate slug strings rather than
|
own labels — deliberately keyed off separate slug strings rather than
|
||||||
the C enum names, so renaming an enumerator can never silently break a
|
the C enum names, so renaming an enumerator can never silently break a
|
||||||
`.lang` file. **To add a language**: copy `engine/i18n/en.lang` to
|
`.lang` file. `position.<slug>.name/desc` additionally take an optional
|
||||||
|
`.male`/`.female` suffix (e.g. `position.attitude.name.male`) for the
|
||||||
|
`Gender`-varying wording (see "Tarot" above); the unsuffixed key is
|
||||||
|
looked up first as the fallback for a missing gendered key, so a
|
||||||
|
position whose wording doesn't vary by gender only needs the one
|
||||||
|
unsuffixed key. **To add a language**: copy `engine/i18n/en.lang` to
|
||||||
`<code>.lang` in the same directory (keys must match exactly) and
|
`<code>.lang` in the same directory (keys must match exactly) and
|
||||||
translate the values — no source changes needed, `make` picks up any
|
translate the values — no source changes needed, `make` picks up any
|
||||||
`*.lang` file there automatically.
|
`*.lang` file there automatically.
|
||||||
@@ -444,10 +466,16 @@ built once and linked into *both* binaries.
|
|||||||
`VENDORED.md` for the pinned upstream commit.
|
`VENDORED.md` for the pinned upstream commit.
|
||||||
- Card/spread text in `tarot_data.c`/`engine/i18n/en.lang`: condensed
|
- Card/spread text in `tarot_data.c`/`engine/i18n/en.lang`: condensed
|
||||||
from A. E. Waite's *The Pictorial Key to the Tarot* (1911), public
|
from A. E. Waite's *The Pictorial Key to the Tarot* (1911), public
|
||||||
domain.
|
domain. The `GENDER_MALE` variant of the Celtic Cross position text
|
||||||
|
keeps Waite's own "him"/"his" wording verbatim; the
|
||||||
|
`GENDER_UNSPECIFIED`/`GENDER_FEMALE` variants are original paraphrases
|
||||||
|
written for this project, not Waite's own text (see "Tarot" above).
|
||||||
- `engine/i18n/de.lang`'s card/spread text is an original translation of
|
- `engine/i18n/de.lang`'s card/spread text is an original translation of
|
||||||
that same public-domain English text, made for this project — not
|
that same public-domain English text, made for this project — not
|
||||||
taken from any specific published German edition.
|
taken from any specific published German edition. Its `Gender`-varying
|
||||||
|
position text follows the same policy as the English original: `.male`
|
||||||
|
is a direct translation of Waite's own wording, `.female`/unsuffixed
|
||||||
|
are original paraphrases for this project.
|
||||||
- Transit narrative text in `interpreter/src/narrative.c`: condensed from
|
- Transit narrative text in `interpreter/src/narrative.c`: condensed from
|
||||||
Sepharial's *Transits and Planetary Periods* (1920, public domain —
|
Sepharial's *Transits and Planetary Periods* (1920, public domain —
|
||||||
`res/Transits_and_Planetary_Periods.pdf`), except the Moon and Pluto
|
`res/Transits_and_Planetary_Periods.pdf`), except the Moon and Pluto
|
||||||
|
|||||||
+27
-11
@@ -17,6 +17,7 @@ same `--lang`) for its own report, independently of `deck-engine`.
|
|||||||
deck-engine --seed <string> --birth-date YYYY-MM-DD --birth-time HH:MM
|
deck-engine --seed <string> --birth-date YYYY-MM-DD --birth-time HH:MM
|
||||||
--birth-utc-offset <hours> --birth-lat <deg> --birth-lon <deg>
|
--birth-utc-offset <hours> --birth-lat <deg> --birth-lon <deg>
|
||||||
[--date YYYY-MM-DDTHH:MM] [--format text|html|json]
|
[--date YYYY-MM-DDTHH:MM] [--format text|html|json]
|
||||||
|
[--gender male|female|unspecified]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Required | Format | Meaning |
|
| Flag | Required | Format | Meaning |
|
||||||
@@ -29,6 +30,7 @@ deck-engine --seed <string> --birth-date YYYY-MM-DD --birth-time HH:MM
|
|||||||
| `--birth-lon` | yes | decimal degrees | Birth longitude, east positive. |
|
| `--birth-lon` | yes | decimal degrees | Birth longitude, east positive. |
|
||||||
| `--date` | no | `YYYY-MM-DDTHH:MM[:SS]` | UTC moment used for today's transits and as the moment the tarot seed is drawn against. Defaults to the current system time. |
|
| `--date` | no | `YYYY-MM-DDTHH:MM[:SS]` | UTC moment used for today's transits and as the moment the tarot seed is drawn against. Defaults to the current system time. |
|
||||||
| `--format` | no | `text` \| `html` \| `json` | Output format, defaults to `text`. `json` always uses language-independent identifiers, ignoring `--lang`. |
|
| `--format` | no | `text` \| `html` \| `json` | Output format, defaults to `text`. `json` always uses language-independent identifiers, ignoring `--lang`. |
|
||||||
|
| `--gender` | no | `male` \| `female` \| `unspecified` | Who the reading is for. Only affects the Celtic Cross position text's pronouns (e.g. "His House" vs. "Her House" vs. "Their House") - never the chart/draw itself. Defaults to `unspecified` (pronoun-neutral phrasing). |
|
||||||
| `--lang` | no | language code, e.g. `en`, `de` | Reading language, defaults to `en`. Matches a file named `<code>.lang` in `--i18n-dir` (see "Translations" below). Unknown codes or missing files fall back to English with a warning on stderr. |
|
| `--lang` | no | language code, e.g. `en`, `de` | Reading language, defaults to `en`. Matches a file named `<code>.lang` in `--i18n-dir` (see "Translations" below). Unknown codes or missing files fall back to English with a warning on stderr. |
|
||||||
| `--i18n-dir` | no | path | Directory to look for `<lang>.lang` in. Defaults to an `i18n/` directory next to the binary itself (resolved from `argv[0]`, so `dist/deck-engine` finds `dist/i18n/` regardless of the caller's working directory). |
|
| `--i18n-dir` | no | path | Directory to look for `<lang>.lang` in. Defaults to an `i18n/` directory next to the binary itself (resolved from `argv[0]`, so `dist/deck-engine` finds `dist/i18n/` regardless of the caller's working directory). |
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ use:
|
|||||||
birth_utc_offset=2
|
birth_utc_offset=2
|
||||||
birth_lat=52.5200
|
birth_lat=52.5200
|
||||||
birth_lon=13.4050
|
birth_lon=13.4050
|
||||||
|
gender=unspecified
|
||||||
lang=en
|
lang=en
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -59,8 +62,8 @@ use:
|
|||||||
with placeholder values (`YOUR_BIRTH_DATE_HERE`, etc.). `run-engine.sh`
|
with placeholder values (`YOUR_BIRTH_DATE_HERE`, etc.). `run-engine.sh`
|
||||||
checks every birth field for an empty value or a leftover `YOUR_`
|
checks every birth field for an empty value or a leftover `YOUR_`
|
||||||
placeholder and refuses to run with a clear error until the file is
|
placeholder and refuses to run with a clear error until the file is
|
||||||
filled in; `lang` has no such check since it always has a usable
|
filled in; `gender` and `lang` have no such check since they always
|
||||||
default (`en`). Since `dist/` itself can be wiped (`make clean`, or
|
have a usable default (`unspecified`/`en`). Since `dist/` itself can be wiped (`make clean`, or
|
||||||
deleting the directory), the build also keeps a durable backup at
|
deleting the directory), the build also keeps a durable backup at
|
||||||
`engine/scripts/user.properties.local` (gitignored) once the file
|
`engine/scripts/user.properties.local` (gitignored) once the file
|
||||||
looks filled in, and restores from it if `dist/user.properties` is
|
looks filled in, and restores from it if `dist/user.properties` is
|
||||||
@@ -81,6 +84,14 @@ catalog with the built-in English text as the fallback for any key the
|
|||||||
catalog doesn't have. See `CLAUDE.md`'s "Translations" section for the
|
catalog doesn't have. See `CLAUDE.md`'s "Translations" section for the
|
||||||
key-naming convention and how to add a new language.
|
key-naming convention and how to add a new language.
|
||||||
|
|
||||||
|
Celtic Cross position text additionally varies with `--gender`: the
|
||||||
|
plain key (e.g. `position.present.desc`) is the pronoun-neutral wording
|
||||||
|
used for `unspecified`, and `.male`/`.female`-suffixed keys (e.g.
|
||||||
|
`position.present.desc.male`) hold the gendered wording where it
|
||||||
|
differs. A position whose wording doesn't vary by gender (e.g. "The
|
||||||
|
Outcome") simply has no `.male`/`.female` keys - the neutral key's
|
||||||
|
translation is used for every `--gender` value in that case.
|
||||||
|
|
||||||
`engine/i18n/en.lang` and `de.lang` are the shipped translation files;
|
`engine/i18n/en.lang` and `de.lang` are the shipped translation files;
|
||||||
`make` copies `engine/i18n/*.lang` to `dist/i18n/` (refreshed on every
|
`make` copies `engine/i18n/*.lang` to `dist/i18n/` (refreshed on every
|
||||||
build, like `dist/img/`). Adding a language is just dropping another
|
build, like `dist/img/`). Adding a language is just dropping another
|
||||||
@@ -146,7 +157,7 @@ Transiting Sun Opposition natal Moon (orb 3.7°)
|
|||||||
|
|
||||||
===== Celtic Cross =====
|
===== Celtic Cross =====
|
||||||
The Present The High Priestess (Reversed)
|
The Present The High Priestess (Reversed)
|
||||||
This covers him: the general influence affecting the matter.
|
This covers them: the general influence affecting the matter.
|
||||||
Passion, moral or physical ardour, conceit, surface knowledge.
|
Passion, moral or physical ardour, conceit, surface knowledge.
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
@@ -171,8 +182,8 @@ cards rendered as an image:
|
|||||||
|
|
||||||
### `--format json`
|
### `--format json`
|
||||||
|
|
||||||
A single JSON object mirroring `DailyReading` exactly — `date`, `natal`,
|
A single JSON object mirroring `DailyReading` exactly — `date`, `gender`,
|
||||||
`transits`, `spread` — using the **slug** accessors
|
`natal`, `transits`, `spread` — using the **slug** accessors
|
||||||
(`astro_body_slug()`, `astro_sign_slug()`, `astro_moon_phase_slug()`,
|
(`astro_body_slug()`, `astro_sign_slug()`, `astro_moon_phase_slug()`,
|
||||||
`astro_aspect_slug()`, `tarot_card_slug()`, `tarot_position_slug()`)
|
`astro_aspect_slug()`, `tarot_card_slug()`, `tarot_position_slug()`)
|
||||||
rather than the display-name ones, so the output is identical regardless
|
rather than the display-name ones, so the output is identical regardless
|
||||||
@@ -183,6 +194,7 @@ read.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"date": "2026-07-16",
|
"date": "2026-07-16",
|
||||||
|
"gender": "unspecified",
|
||||||
"natal": {
|
"natal": {
|
||||||
"bodies": [
|
"bodies": [
|
||||||
{"body": "sun", "sign": "taurus", "degree_in_sign": 23.4926, "ecliptic_longitude": 53.4926, "house": 3},
|
{"body": "sun", "sign": "taurus", "degree_in_sign": 23.4926, "ecliptic_longitude": 53.4926, "house": 3},
|
||||||
@@ -218,6 +230,10 @@ read.
|
|||||||
serialized here since that's the only thing the rest of this format
|
serialized here since that's the only thing the rest of this format
|
||||||
(and the interpreter's own reports, which read this field back — see
|
(and the interpreter's own reports, which read this field back — see
|
||||||
"The interpreter" below) ever display.
|
"The interpreter" below) ever display.
|
||||||
|
- `gender` is `--gender`'s value (`male`/`female`/`unspecified`,
|
||||||
|
`tarot_gender_slug()`) — who the reading is for. Only affects the
|
||||||
|
Celtic Cross position text's pronouns, nothing else in this document.
|
||||||
|
The interpreter reads this field back the same way it reads `date`.
|
||||||
- `bodies` is always `NUM_BODIES` (10) entries, Sun..Pluto in `Body` enum
|
- `bodies` is always `NUM_BODIES` (10) entries, Sun..Pluto in `Body` enum
|
||||||
order; `spread.positions` is always `TAROT_SPREAD_SIZE` (10) entries in
|
order; `spread.positions` is always `TAROT_SPREAD_SIZE` (10) entries in
|
||||||
`CelticCrossPosition` enum order (Present, Challenge, Crown,
|
`CelticCrossPosition` enum order (Present, Challenge, Crown,
|
||||||
@@ -333,14 +349,14 @@ Top 5 significant transits:
|
|||||||
|
|
||||||
Celtic Cross:
|
Celtic Cross:
|
||||||
1. The Present: The Emperor (Reversed)
|
1. The Present: The Emperor (Reversed)
|
||||||
This covers him: the general influence affecting the matter.
|
This covers them: the general influence affecting the matter.
|
||||||
Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.
|
Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.
|
||||||
2. The Challenge: The Devil
|
2. The Challenge: The Devil
|
||||||
This crosses him: the nature of the obstacle in the matter.
|
This crosses them: the nature of the obstacle in the matter.
|
||||||
Ravage, violence, vehemence, extraordinary efforts, force, fatality.
|
Ravage, violence, vehemence, extraordinary efforts, force, fatality.
|
||||||
...
|
...
|
||||||
7. Himself: The Magician
|
7. Themself: The Magician
|
||||||
His position or attitude in the circumstances.
|
Their position or attitude in the circumstances.
|
||||||
Skill, diplomacy, address, subtlety; self-confidence, will.
|
Skill, diplomacy, address, subtlety; self-confidence, will.
|
||||||
...
|
...
|
||||||
10. The Outcome: Strength
|
10. The Outcome: Strength
|
||||||
@@ -381,7 +397,7 @@ Top 5 wichtige Transits:
|
|||||||
|
|
||||||
Keltisches Kreuz:
|
Keltisches Kreuz:
|
||||||
1. Die Gegenwart: Der Herrscher (Umgekehrt)
|
1. Die Gegenwart: Der Herrscher (Umgekehrt)
|
||||||
Dies bedeckt ihn: der allgemeine Einfluss, der die Angelegenheit betrifft.
|
Dies bedeckt die fragende Person: der allgemeine Einfluss, der die Angelegenheit betrifft.
|
||||||
Wohlwollen, Mitgefühl, Ansehen; auch Verwirrung der Feinde, Behinderung, Unreife.
|
Wohlwollen, Mitgefühl, Ansehen; auch Verwirrung der Feinde, Behinderung, Unreife.
|
||||||
...
|
...
|
||||||
|
|
||||||
@@ -407,7 +423,7 @@ HTML output:
|
|||||||
<div class="position">The Present</div>
|
<div class="position">The Present</div>
|
||||||
<img class="reversed" src="img/RWS_Tarot_04_Emperor.jpeg" alt="The Emperor">
|
<img class="reversed" src="img/RWS_Tarot_04_Emperor.jpeg" alt="The Emperor">
|
||||||
<div class="name">The Emperor (Reversed)</div>
|
<div class="name">The Emperor (Reversed)</div>
|
||||||
<div class="desc">This covers him: the general influence affecting the matter.</div>
|
<div class="desc">This covers them: the general influence affecting the matter.</div>
|
||||||
<div class="meaning">Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.</div>
|
<div class="meaning">Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.</div>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|||||||
+40
-12
@@ -71,25 +71,53 @@ aspect.square=Quadrat
|
|||||||
aspect.trine=Trigon
|
aspect.trine=Trigon
|
||||||
aspect.opposition=Opposition
|
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.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.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.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.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.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.name=Die nahe Zukunft
|
||||||
position.near_future.desc=Dies liegt vor ihm: der Einfluss, der jetzt wirksam wird.
|
position.near_future.desc=Dies liegt vor der fragenden Person: der Einfluss, der jetzt wirksam wird.
|
||||||
position.attitude.name=Er selbst
|
position.near_future.desc.male=Dies liegt vor ihm: der Einfluss, der jetzt wirksam wird.
|
||||||
position.attitude.desc=Seine Haltung oder Einstellung in den gegebenen Umständen.
|
position.near_future.desc.female=Dies liegt vor ihr: der Einfluss, der jetzt wirksam wird.
|
||||||
position.environment.name=Sein Haus
|
position.attitude.name=Die fragende Person selbst
|
||||||
position.environment.desc=Sein Umfeld und die darin wirkenden Tendenzen.
|
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.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.name=Das Ergebnis
|
||||||
position.outcome.desc=Was kommen wird: das endgültige Ergebnis der Angelegenheit.
|
position.outcome.desc=Was kommen wird: das endgültige Ergebnis der Angelegenheit.
|
||||||
|
|
||||||
|
|||||||
+39
-12
@@ -79,25 +79,52 @@ aspect.trine=Trine
|
|||||||
aspect.opposition=Opposition
|
aspect.opposition=Opposition
|
||||||
|
|
||||||
# --- Celtic Cross position names/descriptions (tarot_data.c), Waite's
|
# --- 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.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.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.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.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.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.name=The Near Future
|
||||||
position.near_future.desc=This is before him: the influence now coming into action.
|
position.near_future.desc=This is before them: the influence now coming into action.
|
||||||
position.attitude.name=Himself
|
position.near_future.desc.male=This is before him: the influence now coming into action.
|
||||||
position.attitude.desc=His position or attitude in the circumstances.
|
position.near_future.desc.female=This is before her: the influence now coming into action.
|
||||||
position.environment.name=His House
|
position.attitude.name=Themself
|
||||||
position.environment.desc=His environment and the tendencies at work therein.
|
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.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.name=The Outcome
|
||||||
position.outcome.desc=What will come: the final result of the matter.
|
position.outcome.desc=What will come: the final result of the matter.
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ birth_time=""
|
|||||||
birth_utc_offset=""
|
birth_utc_offset=""
|
||||||
birth_lat=""
|
birth_lat=""
|
||||||
birth_lon=""
|
birth_lon=""
|
||||||
|
gender="unspecified"
|
||||||
lang="en"
|
lang="en"
|
||||||
|
|
||||||
while IFS='=' read -r key value || [ -n "$key" ]; do
|
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_utc_offset) birth_utc_offset="$value" ;;
|
||||||
birth_lat) birth_lat="$value" ;;
|
birth_lat) birth_lat="$value" ;;
|
||||||
birth_lon) birth_lon="$value" ;;
|
birth_lon) birth_lon="$value" ;;
|
||||||
|
gender) [ -n "$value" ] && gender="$value" ;;
|
||||||
lang) [ -n "$value" ] && lang="$value" ;;
|
lang) [ -n "$value" ] && lang="$value" ;;
|
||||||
esac
|
esac
|
||||||
done <"$PROPERTIES_FILE"
|
done <"$PROPERTIES_FILE"
|
||||||
@@ -77,5 +79,6 @@ exec "$BINARY" \
|
|||||||
--birth-lon "$birth_lon" \
|
--birth-lon "$birth_lon" \
|
||||||
--date "$transit_date" \
|
--date "$transit_date" \
|
||||||
--format "$format" \
|
--format "$format" \
|
||||||
|
--gender "$gender" \
|
||||||
--lang "$lang" \
|
--lang "$lang" \
|
||||||
--i18n-dir "$SCRIPT_DIR/i18n"
|
--i18n-dir "$SCRIPT_DIR/i18n"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ birth_time=""
|
|||||||
birth_utc_offset=""
|
birth_utc_offset=""
|
||||||
birth_lat=""
|
birth_lat=""
|
||||||
birth_lon=""
|
birth_lon=""
|
||||||
|
gender="unspecified"
|
||||||
lang="en"
|
lang="en"
|
||||||
|
|
||||||
while IFS='=' read -r key value || [ -n "$key" ]; do
|
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_utc_offset) birth_utc_offset="$value" ;;
|
||||||
birth_lat) birth_lat="$value" ;;
|
birth_lat) birth_lat="$value" ;;
|
||||||
birth_lon) birth_lon="$value" ;;
|
birth_lon) birth_lon="$value" ;;
|
||||||
|
gender) [ -n "$value" ] && gender="$value" ;;
|
||||||
lang) [ -n "$value" ] && lang="$value" ;;
|
lang) [ -n "$value" ] && lang="$value" ;;
|
||||||
esac
|
esac
|
||||||
done <"$PROPERTIES_FILE"
|
done <"$PROPERTIES_FILE"
|
||||||
@@ -92,4 +94,5 @@ lang="${2:-$lang}"
|
|||||||
--birth-lon "$birth_lon" \
|
--birth-lon "$birth_lon" \
|
||||||
--date "$transit_date" \
|
--date "$transit_date" \
|
||||||
--format json \
|
--format json \
|
||||||
|
--gender "$gender" \
|
||||||
| exec "$INTERPRETER_BINARY" --format "$format" --lang "$lang" --i18n-dir "$SCRIPT_DIR/i18n"
|
| exec "$INTERPRETER_BINARY" --format "$format" --lang "$lang" --i18n-dir "$SCRIPT_DIR/i18n"
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
# (e.g. 2 for Central European Summer Time)
|
# (e.g. 2 for Central European Summer Time)
|
||||||
# birth_lat Birth location latitude, decimal degrees, north positive
|
# birth_lat Birth location latitude, decimal degrees, north positive
|
||||||
# birth_lon Birth location longitude, decimal degrees, east 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
|
# lang Reading language code - matches a file named
|
||||||
# <code>.lang in dist/i18n/ (see engine/i18n/ for the
|
# <code>.lang in dist/i18n/ (see engine/i18n/ for the
|
||||||
# source files, currently "en" and "de"). Optional,
|
# 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_utc_offset=YOUR_UTC_OFFSET_HERE
|
||||||
birth_lat=YOUR_LATITUDE_HERE
|
birth_lat=YOUR_LATITUDE_HERE
|
||||||
birth_lon=YOUR_LONGITUDE_HERE
|
birth_lon=YOUR_LONGITUDE_HERE
|
||||||
|
gender=unspecified
|
||||||
lang=en
|
lang=en
|
||||||
|
|||||||
+16
-1
@@ -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"
|
"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"
|
" --birth-utc-offset <hours> --birth-lat <deg> --birth-lon <deg>\n"
|
||||||
" [--date YYYY-MM-DDTHH:MM] [--format text|html|json]\n"
|
" [--date YYYY-MM-DDTHH:MM] [--format text|html|json]\n"
|
||||||
|
" [--gender male|female|unspecified]\n"
|
||||||
" [--lang <code>] [--i18n-dir <path>]\n\n"
|
" [--lang <code>] [--i18n-dir <path>]\n\n"
|
||||||
" --seed Tarot seed string; same seed -> same Celtic Cross spread.\n"
|
" --seed Tarot seed string; same seed -> same Celtic Cross spread.\n"
|
||||||
" --birth-date/time Birth date/time in local clock time.\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-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"
|
" --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"
|
" --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"
|
" --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"
|
" img/<file>, i.e. it expects to be saved next to the img/\n"
|
||||||
" directory that `make images` copies into dist/. json uses\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 *birth_utc_offset = NULL, *birth_lat = NULL, *birth_lon = NULL;
|
||||||
const char *date_str = NULL, *format_str = "text";
|
const char *date_str = NULL, *format_str = "text";
|
||||||
const char *lang = "en", *i18n_dir_arg = NULL;
|
const char *lang = "en", *i18n_dir_arg = NULL;
|
||||||
|
const char *gender_str = "unspecified";
|
||||||
|
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
const char *arg = argv[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, "--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, "--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, "--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, "--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, "--i18n-dir") == 0) rc = require_arg(argc, argv, &i, arg, &i18n_dir_arg);
|
||||||
else if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
|
else if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
|
||||||
@@ -136,6 +142,15 @@ int main(int argc, char **argv) {
|
|||||||
return 1;
|
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];
|
char i18n_path[512];
|
||||||
if (i18n_dir_arg) {
|
if (i18n_dir_arg) {
|
||||||
snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_dir_arg, lang);
|
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;
|
DailyReading reading;
|
||||||
reading_generate(seed, &birth, utc_moment, &reading);
|
reading_generate(seed, &birth, gender, utc_moment, &reading);
|
||||||
|
|
||||||
if (format == FORMAT_HTML) {
|
if (format == FORMAT_HTML) {
|
||||||
reading_print_html(&reading, stdout);
|
reading_print_html(&reading, stdout);
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
#include <math.h>
|
#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) {
|
time_t utc_moment, DailyReading *out) {
|
||||||
out->utc_moment = utc_moment;
|
out->utc_moment = utc_moment;
|
||||||
|
out->gender = gender;
|
||||||
astro_compute_natal_chart(birth, &out->natal);
|
astro_compute_natal_chart(birth, &out->natal);
|
||||||
astro_compute_daily_transits(utc_moment, &out->natal, &out->transits);
|
astro_compute_daily_transits(utc_moment, &out->natal, &out->transits);
|
||||||
tarot_draw_celtic_cross(tarot_seed, &out->spread);
|
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"));
|
fprintf(out, "===== %s =====\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
const TarotDraw *draw = &r->spread.positions[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 ? " " : "",
|
tarot_card_name(draw->card), draw->reversed ? " " : "",
|
||||||
draw->reversed ? ui("ui.reversed", "(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));
|
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=\"desc\">%s</div>\n"
|
||||||
" <div class=\"meaning\">%s</div>\n"
|
" <div class=\"meaning\">%s</div>\n"
|
||||||
"</div>\n",
|
"</div>\n",
|
||||||
tarot_position_name((CelticCrossPosition)i),
|
tarot_position_name((CelticCrossPosition)i, r->gender),
|
||||||
draw->reversed ? "reversed" : "",
|
draw->reversed ? "reversed" : "",
|
||||||
tarot_card_image_file(draw->card), tarot_card_name(draw->card),
|
tarot_card_image_file(draw->card), tarot_card_name(draw->card),
|
||||||
tarot_card_name(draw->card), draw->reversed ? " " : "",
|
tarot_card_name(draw->card), draw->reversed ? " " : "",
|
||||||
draw->reversed ? ui("ui.reversed", "(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));
|
tarot_card_meaning(draw->card, draw->reversed));
|
||||||
}
|
}
|
||||||
fprintf(out, "</div>\n</body></html>\n");
|
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);
|
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, " \"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");
|
fprintf(out, " \"natal\": {\n \"bodies\": [\n");
|
||||||
for (int b = 0; b < NUM_BODIES; b++) {
|
for (int b = 0; b < NUM_BODIES; b++) {
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ typedef struct {
|
|||||||
* interpreter side) gets it for free without threading it through
|
* interpreter side) gets it for free without threading it through
|
||||||
* separately. */
|
* separately. */
|
||||||
time_t utc_moment;
|
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;
|
NatalChart natal;
|
||||||
DailyTransits transits;
|
DailyTransits transits;
|
||||||
CelticCrossSpread spread;
|
CelticCrossSpread spread;
|
||||||
@@ -28,7 +32,7 @@ typedef struct {
|
|||||||
|
|
||||||
/* The real API: this is the only function the watchapp needs to call.
|
/* 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. */
|
* 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);
|
time_t utc_moment, DailyReading *out);
|
||||||
|
|
||||||
/* Desktop-only output formats for inspecting a reading before any watch
|
/* Desktop-only output formats for inspecting a reading before any watch
|
||||||
|
|||||||
+14
-2
@@ -52,6 +52,17 @@ typedef struct {
|
|||||||
bool reversed;
|
bool reversed;
|
||||||
} TarotDraw;
|
} 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 {
|
typedef struct {
|
||||||
/* Indexed by CelticCrossPosition. */
|
/* Indexed by CelticCrossPosition. */
|
||||||
TarotDraw positions[TAROT_SPREAD_SIZE];
|
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_image_file(TarotCard card); /* matches res/img/ */
|
||||||
const char *tarot_card_meaning(TarotCard card, bool reversed);
|
const char *tarot_card_meaning(TarotCard card, bool reversed);
|
||||||
|
|
||||||
const char *tarot_position_name(CelticCrossPosition position);
|
const char *tarot_position_name(CelticCrossPosition position, Gender gender);
|
||||||
const char *tarot_position_description(CelticCrossPosition position);
|
const char *tarot_position_description(CelticCrossPosition position, Gender gender);
|
||||||
|
|
||||||
/* Stable, language-independent identifiers (e.g. "high_priestess",
|
/* Stable, language-independent identifiers (e.g. "high_priestess",
|
||||||
* "wheel_of_fortune", "recent_past") - the same strings i18n.c's lookup
|
* "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. */
|
* (reading_print_json) that must stay identical regardless of --lang. */
|
||||||
const char *tarot_card_slug(TarotCard card);
|
const char *tarot_card_slug(TarotCard card);
|
||||||
const char *tarot_position_slug(CelticCrossPosition position);
|
const char *tarot_position_slug(CelticCrossPosition position);
|
||||||
|
const char *tarot_gender_slug(Gender gender); /* "unspecified"/"male"/"female" */
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+112
-34
@@ -130,52 +130,97 @@ static const TarotCardInfo k_cards[TAROT_DECK_SIZE] = {
|
|||||||
|
|
||||||
/* Position names and one-line descriptions follow Waite's own account
|
/* Position names and one-line descriptions follow Waite's own account
|
||||||
* of "An Ancient Celtic Method of Divination" (Part III §7), in his
|
* 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 {
|
typedef struct {
|
||||||
const char *name;
|
const char *name[3];
|
||||||
const char *description;
|
const char *description[3];
|
||||||
} PositionInfo;
|
} PositionInfo;
|
||||||
|
|
||||||
static const PositionInfo k_positions[TAROT_SPREAD_SIZE] = {
|
static const PositionInfo k_positions[TAROT_SPREAD_SIZE] = {
|
||||||
[POSITION_PRESENT] = {
|
[POSITION_PRESENT] = {
|
||||||
"The Present",
|
{ "The Present", "The Present", "The Present" },
|
||||||
"This covers him: the general influence affecting the matter."
|
{
|
||||||
|
"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] = {
|
[POSITION_CHALLENGE] = {
|
||||||
"The Challenge",
|
{ "The Challenge", "The Challenge", "The Challenge" },
|
||||||
"This crosses him: the nature of the obstacle in the matter."
|
{
|
||||||
|
"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] = {
|
[POSITION_CROWN] = {
|
||||||
"The Crown",
|
{ "The Crown", "The Crown", "The Crown" },
|
||||||
"This crowns him: the aim or ideal, the best that can be achieved."
|
{
|
||||||
|
"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] = {
|
[POSITION_FOUNDATION] = {
|
||||||
"The Foundation",
|
{ "The Foundation", "The Foundation", "The Foundation" },
|
||||||
"This is beneath him: the basis of the matter, already actual."
|
{
|
||||||
|
"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] = {
|
[POSITION_RECENT_PAST] = {
|
||||||
"The Recent Past",
|
{ "The Recent Past", "The Recent Past", "The Recent Past" },
|
||||||
"This is behind him: the influence that is just passing away."
|
{
|
||||||
|
"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] = {
|
[POSITION_NEAR_FUTURE] = {
|
||||||
"The Near Future",
|
{ "The Near Future", "The Near Future", "The Near Future" },
|
||||||
"This is before him: the influence now coming into action."
|
{
|
||||||
|
"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] = {
|
[POSITION_ATTITUDE] = {
|
||||||
"Himself",
|
{ "Themself", "Himself", "Herself" },
|
||||||
"His position or attitude in the circumstances."
|
{
|
||||||
|
"Their position or attitude in the circumstances.",
|
||||||
|
"His position or attitude in the circumstances.",
|
||||||
|
"Her position or attitude in the circumstances.",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[POSITION_ENVIRONMENT] = {
|
[POSITION_ENVIRONMENT] = {
|
||||||
"His House",
|
{ "Their House", "His House", "Her House" },
|
||||||
"His environment and the tendencies at work therein."
|
{
|
||||||
|
"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] = {
|
[POSITION_HOPES_AND_FEARS] = {
|
||||||
"Hopes and Fears",
|
{ "Hopes and Fears", "Hopes and Fears", "Hopes and Fears" },
|
||||||
"His hopes or fears in the matter."
|
{
|
||||||
|
"Their hopes or fears in the matter.",
|
||||||
|
"His hopes or fears in the matter.",
|
||||||
|
"Her hopes or fears in the matter.",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[POSITION_OUTCOME] = {
|
[POSITION_OUTCOME] = {
|
||||||
"The Outcome",
|
{ "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.",
|
||||||
|
"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);
|
return i18n_get(key, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *position_field(const char *slug, const char *field, const char *fallback) {
|
/* Suffix appended to the i18n key for the male/female variants - the
|
||||||
char key[64];
|
* unspecified/neutral variant uses the plain, unsuffixed key so it
|
||||||
strcpy(key, "position.");
|
* matches the field name callers already know (e.g. "position.
|
||||||
strcat(key, slug);
|
* attitude.name"), same convention as card_field()'s upright/reversed
|
||||||
strcat(key, ".");
|
* field names above. */
|
||||||
strcat(key, field);
|
static const char *const k_gender_suffix[3] = {
|
||||||
return i18n_get(key, fallback);
|
[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) {
|
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);
|
reversed ? k_cards[card].reversed : k_cards[card].upright);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *tarot_position_name(CelticCrossPosition position) {
|
const char *tarot_position_name(CelticCrossPosition position, Gender gender) {
|
||||||
return position_field(k_position_slug[position], "name", k_positions[position].name);
|
return position_field(k_position_slug[position], "name", gender, k_positions[position].name[gender]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *tarot_position_description(CelticCrossPosition position) {
|
const char *tarot_position_description(CelticCrossPosition position, Gender gender) {
|
||||||
return position_field(k_position_slug[position], "desc", k_positions[position].description);
|
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_card_slug(TarotCard card) { return k_card_slug[card]; }
|
||||||
const char *tarot_position_slug(CelticCrossPosition position) { return k_position_slug[position]; }
|
const char *tarot_position_slug(CelticCrossPosition position) { return k_position_slug[position]; }
|
||||||
|
const char *tarot_gender_slug(Gender gender) { return k_gender_slug[gender]; }
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ static void test_reading_generate_smoke(void) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
DailyReading r1, r2;
|
DailyReading r1, r2;
|
||||||
reading_generate("smoke-test-seed", &birth, 1751500000, &r1);
|
reading_generate("smoke-test-seed", &birth, GENDER_UNSPECIFIED, 1751500000, &r1);
|
||||||
reading_generate("smoke-test-seed", &birth, 1751500000, &r2);
|
reading_generate("smoke-test-seed", &birth, GENDER_UNSPECIFIED, 1751500000, &r2);
|
||||||
|
|
||||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
assert(r1.spread.positions[i].card == r2.spread.positions[i].card);
|
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;
|
DailyReading r;
|
||||||
reading_generate("smoke-test-seed", &birth, 1751500000, &r);
|
reading_generate("smoke-test-seed", &birth, GENDER_FEMALE, 1751500000, &r);
|
||||||
|
|
||||||
FILE *tmp = tmpfile();
|
FILE *tmp = tmpfile();
|
||||||
assert(tmp);
|
assert(tmp);
|
||||||
@@ -121,6 +121,7 @@ static void test_reading_print_json_shape(void) {
|
|||||||
* into what must stay language-independent regardless of --lang). */
|
* into what must stay language-independent regardless of --lang). */
|
||||||
assert(buf[0] == '{');
|
assert(buf[0] == '{');
|
||||||
assert(strstr(buf, "\"date\": \"2025-07-02\"") != NULL); /* UTC date of the fixed 1751500000 seed */
|
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, "\"natal\"") != NULL);
|
||||||
assert(strstr(buf, "\"transits\"") != NULL);
|
assert(strstr(buf, "\"transits\"") != NULL);
|
||||||
assert(strstr(buf, "\"spread\"") != 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_moon_phase_name(MOON_FULL), "Vollmond") == 0);
|
||||||
assert(strcmp(astro_aspect_name(ASPECT_TRINE), "Trigon") == 0);
|
assert(strcmp(astro_aspect_name(ASPECT_TRINE), "Trigon") == 0);
|
||||||
assert(strcmp(tarot_card_name(CARD_WORLD), "Die Welt") == 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
|
/* Loading a file that doesn't exist fails and leaves the previously
|
||||||
* loaded catalog in place, rather than silently clearing it. */
|
* 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");
|
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) {
|
int main(void) {
|
||||||
test_tarot_determinism();
|
test_tarot_determinism();
|
||||||
test_natal_sun_sign();
|
test_natal_sun_sign();
|
||||||
test_reading_generate_smoke();
|
test_reading_generate_smoke();
|
||||||
test_reading_print_json_shape();
|
test_reading_print_json_shape();
|
||||||
test_i18n_fallback_and_translation();
|
test_i18n_fallback_and_translation();
|
||||||
|
test_gendered_position_text();
|
||||||
printf("All smoke tests passed.\n");
|
printf("All smoke tests passed.\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-10
@@ -226,13 +226,13 @@ static void print_top_transits_header(FILE *out, int count) {
|
|||||||
|
|
||||||
/* ===== --format text ===== */
|
/* ===== --format text ===== */
|
||||||
|
|
||||||
static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread) {
|
static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread, Gender gender) {
|
||||||
fprintf(out, "%s:\n", ui("ui.celtic_cross", "Celtic Cross"));
|
fprintf(out, "%s:\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
const TarotDraw *draw = &spread->positions[i];
|
const TarotDraw *draw = &spread->positions[i];
|
||||||
fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_name((CelticCrossPosition)i),
|
fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_name((CelticCrossPosition)i, gender),
|
||||||
tarot_card_name(draw->card), reversed_marker(draw->reversed));
|
tarot_card_name(draw->card), reversed_marker(draw->reversed));
|
||||||
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i));
|
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i, gender));
|
||||||
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
|
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,7 +269,7 @@ static void interp_print_text(FILE *out, const DailyReading *reading, const Dail
|
|||||||
}
|
}
|
||||||
fprintf(out, "\n");
|
fprintf(out, "\n");
|
||||||
|
|
||||||
print_celtic_cross_text(out, &reading->spread);
|
print_celtic_cross_text(out, &reading->spread, reading->gender);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== --format html ===== */
|
/* ===== --format html ===== */
|
||||||
@@ -297,7 +297,8 @@ static void print_transit_item_html(FILE *out, const DailyReading *reading,
|
|||||||
/* One Celtic Cross card as a ".card" div - shared between the
|
/* One Celtic Cross card as a ".card" div - shared between the
|
||||||
* Attitude/Outcome preview (right after guidance) and the full spread
|
* Attitude/Outcome preview (right after guidance) and the full spread
|
||||||
* further down, so both render identically. */
|
* further down, so both render identically. */
|
||||||
static void print_card_html(FILE *out, CelticCrossPosition position, const TarotDraw *draw) {
|
static void print_card_html(FILE *out, CelticCrossPosition position, const TarotDraw *draw,
|
||||||
|
Gender gender) {
|
||||||
fprintf(out,
|
fprintf(out,
|
||||||
"<div class=\"card\">\n"
|
"<div class=\"card\">\n"
|
||||||
" <div class=\"position\">%s</div>\n"
|
" <div class=\"position\">%s</div>\n"
|
||||||
@@ -306,9 +307,9 @@ static void print_card_html(FILE *out, CelticCrossPosition position, const Tarot
|
|||||||
" <div class=\"desc\">%s</div>\n"
|
" <div class=\"desc\">%s</div>\n"
|
||||||
" <div class=\"meaning\">%s</div>\n"
|
" <div class=\"meaning\">%s</div>\n"
|
||||||
"</div>\n",
|
"</div>\n",
|
||||||
tarot_position_name(position), draw->reversed ? "reversed" : "",
|
tarot_position_name(position, gender), draw->reversed ? "reversed" : "",
|
||||||
tarot_card_image_file(draw->card), tarot_card_name(draw->card), tarot_card_name(draw->card),
|
tarot_card_image_file(draw->card), tarot_card_name(draw->card), tarot_card_name(draw->card),
|
||||||
reversed_marker(draw->reversed), tarot_position_description(position),
|
reversed_marker(draw->reversed), tarot_position_description(position, gender),
|
||||||
tarot_card_meaning(draw->card, draw->reversed));
|
tarot_card_meaning(draw->card, draw->reversed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,8 +353,8 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
|
|||||||
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_guidance", "Guidance"));
|
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_guidance", "Guidance"));
|
||||||
|
|
||||||
fprintf(out, "<div class=\"spread attitude-outcome\">\n");
|
fprintf(out, "<div class=\"spread attitude-outcome\">\n");
|
||||||
print_card_html(out, POSITION_ATTITUDE, &reading->spread.positions[POSITION_ATTITUDE]);
|
print_card_html(out, POSITION_ATTITUDE, &reading->spread.positions[POSITION_ATTITUDE], reading->gender);
|
||||||
print_card_html(out, POSITION_OUTCOME, &reading->spread.positions[POSITION_OUTCOME]);
|
print_card_html(out, POSITION_OUTCOME, &reading->spread.positions[POSITION_OUTCOME], reading->gender);
|
||||||
fprintf(out, "</div>\n");
|
fprintf(out, "</div>\n");
|
||||||
|
|
||||||
fprintf(out, "<div class=\"guidance\">\n");
|
fprintf(out, "<div class=\"guidance\">\n");
|
||||||
@@ -386,7 +387,7 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
|
|||||||
|
|
||||||
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
|
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
print_card_html(out, (CelticCrossPosition)i, &reading->spread.positions[i]);
|
print_card_html(out, (CelticCrossPosition)i, &reading->spread.positions[i], reading->gender);
|
||||||
}
|
}
|
||||||
fprintf(out, "</div>\n");
|
fprintf(out, "</div>\n");
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ static const char *const k_position_slug[TAROT_SPREAD_SIZE] = {
|
|||||||
"near_future", "attitude", "environment", "hopes_and_fears", "outcome",
|
"near_future", "attitude", "environment", "hopes_and_fears", "outcome",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Mirrors tarot_data.c's own k_gender_slug, same duplication policy as
|
||||||
|
* k_card_slug/k_position_slug above (no reverse lookup exposed there). */
|
||||||
|
static const char *const k_gender_slug[3] = {
|
||||||
|
"unspecified", "male", "female",
|
||||||
|
};
|
||||||
|
|
||||||
static bool body_from_slug(const char *slug, Body *out) {
|
static bool body_from_slug(const char *slug, Body *out) {
|
||||||
for (int i = 0; i < NUM_BODIES; i++) {
|
for (int i = 0; i < NUM_BODIES; i++) {
|
||||||
if (strcmp(slug, k_body_slug[i]) == 0) {
|
if (strcmp(slug, k_body_slug[i]) == 0) {
|
||||||
@@ -104,6 +110,16 @@ static bool card_from_slug(const char *slug, TarotCard *out) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool gender_from_slug(const char *slug, Gender *out) {
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (strcmp(slug, k_gender_slug[i]) == 0) {
|
||||||
|
*out = (Gender)i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static bool position_from_slug(const char *slug, CelticCrossPosition *out) {
|
static bool position_from_slug(const char *slug, CelticCrossPosition *out) {
|
||||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
if (strcmp(slug, k_position_slug[i]) == 0) {
|
if (strcmp(slug, k_position_slug[i]) == 0) {
|
||||||
@@ -155,6 +171,18 @@ static void parse_date(const JsonValue *date, DailyReading *out) {
|
|||||||
out->utc_moment = timegm(&tm_date);
|
out->utc_moment = timegm(&tm_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Parses the top-level "gender" field (see reading_print_json in
|
||||||
|
* engine/src/reading.c) into out->gender. Lenient like parse_date(): a
|
||||||
|
* missing or unrecognized value just leaves out->gender at
|
||||||
|
* GENDER_UNSPECIFIED (0, from reading_load_json's own memset), not a
|
||||||
|
* load failure - it only affects Celtic Cross position text pronouns,
|
||||||
|
* nothing scoring depends on it. */
|
||||||
|
static void parse_gender(const JsonValue *gender, DailyReading *out) {
|
||||||
|
const char *gender_str = gender ? json_as_string(gender) : NULL;
|
||||||
|
if (!gender_str) return;
|
||||||
|
gender_from_slug(gender_str, &out->gender);
|
||||||
|
}
|
||||||
|
|
||||||
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
|
|
||||||
@@ -197,6 +225,7 @@ bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
|||||||
out->transits.aspect_count = filled;
|
out->transits.aspect_count = filled;
|
||||||
|
|
||||||
parse_date(json_object_get(root, "date"), out);
|
parse_date(json_object_get(root, "date"), out);
|
||||||
|
parse_gender(json_object_get(root, "gender"), out);
|
||||||
|
|
||||||
const JsonValue *natal = json_object_get(root, "natal");
|
const JsonValue *natal = json_object_get(root, "natal");
|
||||||
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
||||||
|
|||||||
@@ -16,10 +16,13 @@
|
|||||||
* significance.c), out->natal.bodies[]/out->transits.bodies[] (sign +
|
* significance.c), out->natal.bodies[]/out->transits.bodies[] (sign +
|
||||||
* house per body, used only for display), out->spread.positions[]
|
* house per body, used only for display), out->spread.positions[]
|
||||||
* (card + reversed per Celtic Cross position, used only by
|
* (card + reversed per Celtic Cross position, used only by
|
||||||
* guidance.c), and out->utc_moment (the top-level "date" field, parsed
|
* guidance.c), out->utc_moment (the top-level "date" field, parsed
|
||||||
* back to midnight UTC of that date - used only for display, e.g. main.c
|
* back to midnight UTC of that date - used only for display, e.g. main.c
|
||||||
* printing which day a reading is for). out->natal.ascendant_longitude/
|
* printing which day a reading is for), and out->gender (the top-level
|
||||||
* houses[] are left zeroed - nothing reads them yet.
|
* "gender" field - used only for the Celtic Cross position text's
|
||||||
|
* pronouns, e.g. tarot_position_name()/tarot_position_description()).
|
||||||
|
* out->natal.ascendant_longitude/houses[] are left zeroed - nothing
|
||||||
|
* reads them yet.
|
||||||
*
|
*
|
||||||
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
||||||
* present as an array (an empty array is fine - that's a real "no
|
* present as an array (an empty array is fine - that's a real "no
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ static void test_json_parse_rejects_malformed(void) {
|
|||||||
static const char *k_fixture =
|
static const char *k_fixture =
|
||||||
"{"
|
"{"
|
||||||
" \"date\": \"2026-07-16\","
|
" \"date\": \"2026-07-16\","
|
||||||
|
" \"gender\": \"female\","
|
||||||
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
|
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
|
||||||
" \"transits\": {"
|
" \"transits\": {"
|
||||||
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \"house\": 5}],"
|
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \"house\": 5}],"
|
||||||
@@ -106,6 +107,38 @@ static void test_reading_load_json_missing_date_defaults_to_zero(void) {
|
|||||||
printf("PASS test_reading_load_json_missing_date_defaults_to_zero\n");
|
printf("PASS test_reading_load_json_missing_date_defaults_to_zero\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_reading_load_json_extracts_gender(void) {
|
||||||
|
DailyReading reading;
|
||||||
|
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||||
|
|
||||||
|
assert(ok);
|
||||||
|
assert(reading.gender == GENDER_FEMALE);
|
||||||
|
|
||||||
|
printf("PASS test_reading_load_json_extracts_gender\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_reading_load_json_missing_gender_defaults_to_unspecified(void) {
|
||||||
|
const char *text = "{\"transits\": {\"aspects\": []}}";
|
||||||
|
DailyReading reading;
|
||||||
|
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||||
|
|
||||||
|
assert(ok);
|
||||||
|
assert(reading.gender == GENDER_UNSPECIFIED);
|
||||||
|
|
||||||
|
printf("PASS test_reading_load_json_missing_gender_defaults_to_unspecified\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_reading_load_json_unrecognized_gender_defaults_to_unspecified(void) {
|
||||||
|
const char *text = "{\"gender\": \"xenu\", \"transits\": {\"aspects\": []}}";
|
||||||
|
DailyReading reading;
|
||||||
|
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||||
|
|
||||||
|
assert(ok);
|
||||||
|
assert(reading.gender == GENDER_UNSPECIFIED);
|
||||||
|
|
||||||
|
printf("PASS test_reading_load_json_unrecognized_gender_defaults_to_unspecified\n");
|
||||||
|
}
|
||||||
|
|
||||||
static void test_reading_load_json_extracts_body_sign_and_house(void) {
|
static void test_reading_load_json_extracts_body_sign_and_house(void) {
|
||||||
DailyReading reading;
|
DailyReading reading;
|
||||||
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||||
@@ -206,6 +239,9 @@ int main(void) {
|
|||||||
test_reading_load_json_extracts_aspects();
|
test_reading_load_json_extracts_aspects();
|
||||||
test_reading_load_json_extracts_date();
|
test_reading_load_json_extracts_date();
|
||||||
test_reading_load_json_missing_date_defaults_to_zero();
|
test_reading_load_json_missing_date_defaults_to_zero();
|
||||||
|
test_reading_load_json_extracts_gender();
|
||||||
|
test_reading_load_json_missing_gender_defaults_to_unspecified();
|
||||||
|
test_reading_load_json_unrecognized_gender_defaults_to_unspecified();
|
||||||
test_reading_load_json_extracts_body_sign_and_house();
|
test_reading_load_json_extracts_body_sign_and_house();
|
||||||
test_reading_load_json_extracts_spread_positions();
|
test_reading_load_json_extracts_spread_positions();
|
||||||
test_reading_load_json_missing_spread_is_not_fatal();
|
test_reading_load_json_missing_spread_is_not_fatal();
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"BIRTH_UTC_OFFSET_MINUTES",
|
"BIRTH_UTC_OFFSET_MINUTES",
|
||||||
"BIRTH_LAT_MICRODEG",
|
"BIRTH_LAT_MICRODEG",
|
||||||
"BIRTH_LON_MICRODEG",
|
"BIRTH_LON_MICRODEG",
|
||||||
|
"GENDER",
|
||||||
"LANG",
|
"LANG",
|
||||||
"NOTIFY_ENABLED",
|
"NOTIFY_ENABLED",
|
||||||
"NOTIFY_HOUR",
|
"NOTIFY_HOUR",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ static void inbox_received_handler(DictionaryIterator *iter, void *context) {
|
|||||||
cfg.birth.longitude = v / 1000000.0;
|
cfg.birth.longitude = v / 1000000.0;
|
||||||
got_birth = true;
|
got_birth = true;
|
||||||
}
|
}
|
||||||
|
if (read_int(iter, MESSAGE_KEY_GENDER, &v)) cfg.gender = (Gender)v;
|
||||||
|
|
||||||
Tuple *lang_tuple = dict_find(iter, MESSAGE_KEY_LANG);
|
Tuple *lang_tuple = dict_find(iter, MESSAGE_KEY_LANG);
|
||||||
if (lang_tuple) {
|
if (lang_tuple) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ enum {
|
|||||||
PKEY_BIRTH_UTC_OFFSET_MINUTES,
|
PKEY_BIRTH_UTC_OFFSET_MINUTES,
|
||||||
PKEY_BIRTH_LAT_MICRODEG,
|
PKEY_BIRTH_LAT_MICRODEG,
|
||||||
PKEY_BIRTH_LON_MICRODEG,
|
PKEY_BIRTH_LON_MICRODEG,
|
||||||
|
PKEY_GENDER,
|
||||||
PKEY_LANG,
|
PKEY_LANG,
|
||||||
PKEY_NOTIFY_ENABLED,
|
PKEY_NOTIFY_ENABLED,
|
||||||
PKEY_NOTIFY_HOUR,
|
PKEY_NOTIFY_HOUR,
|
||||||
@@ -40,6 +41,7 @@ void config_load(WatchConfig *out) {
|
|||||||
out->birth.utc_offset_hours = (double)(int32_t)persist_read_int(PKEY_BIRTH_UTC_OFFSET_MINUTES) / 60.0;
|
out->birth.utc_offset_hours = (double)(int32_t)persist_read_int(PKEY_BIRTH_UTC_OFFSET_MINUTES) / 60.0;
|
||||||
out->birth.latitude = (double)(int32_t)persist_read_int(PKEY_BIRTH_LAT_MICRODEG) / 1000000.0;
|
out->birth.latitude = (double)(int32_t)persist_read_int(PKEY_BIRTH_LAT_MICRODEG) / 1000000.0;
|
||||||
out->birth.longitude = (double)(int32_t)persist_read_int(PKEY_BIRTH_LON_MICRODEG) / 1000000.0;
|
out->birth.longitude = (double)(int32_t)persist_read_int(PKEY_BIRTH_LON_MICRODEG) / 1000000.0;
|
||||||
|
out->gender = (Gender)persist_read_int(PKEY_GENDER); /* defaults to 0 = GENDER_UNSPECIFIED */
|
||||||
|
|
||||||
if (persist_read_string(PKEY_LANG, out->lang, sizeof out->lang) <= 0) {
|
if (persist_read_string(PKEY_LANG, out->lang, sizeof out->lang) <= 0) {
|
||||||
strcpy(out->lang, "en");
|
strcpy(out->lang, "en");
|
||||||
@@ -61,6 +63,7 @@ void config_save(const WatchConfig *cfg) {
|
|||||||
persist_write_int(PKEY_BIRTH_UTC_OFFSET_MINUTES, (int32_t)(cfg->birth.utc_offset_hours * 60.0));
|
persist_write_int(PKEY_BIRTH_UTC_OFFSET_MINUTES, (int32_t)(cfg->birth.utc_offset_hours * 60.0));
|
||||||
persist_write_int(PKEY_BIRTH_LAT_MICRODEG, (int32_t)(cfg->birth.latitude * 1000000.0));
|
persist_write_int(PKEY_BIRTH_LAT_MICRODEG, (int32_t)(cfg->birth.latitude * 1000000.0));
|
||||||
persist_write_int(PKEY_BIRTH_LON_MICRODEG, (int32_t)(cfg->birth.longitude * 1000000.0));
|
persist_write_int(PKEY_BIRTH_LON_MICRODEG, (int32_t)(cfg->birth.longitude * 1000000.0));
|
||||||
|
persist_write_int(PKEY_GENDER, (int32_t)cfg->gender);
|
||||||
|
|
||||||
persist_write_string(PKEY_LANG, cfg->lang);
|
persist_write_string(PKEY_LANG, cfg->lang);
|
||||||
|
|
||||||
@@ -79,8 +82,9 @@ void config_log(const WatchConfig *cfg) {
|
|||||||
APP_LOG(APP_LOG_LEVEL_INFO, "config: birth %04d-%02d-%02d %02d:%02d UTC offset %dmin",
|
APP_LOG(APP_LOG_LEVEL_INFO, "config: birth %04d-%02d-%02d %02d:%02d UTC offset %dmin",
|
||||||
cfg->birth.year, cfg->birth.month, cfg->birth.day, cfg->birth.hour, cfg->birth.minute,
|
cfg->birth.year, cfg->birth.month, cfg->birth.day, cfg->birth.hour, cfg->birth.minute,
|
||||||
(int)(cfg->birth.utc_offset_hours * 60));
|
(int)(cfg->birth.utc_offset_hours * 60));
|
||||||
APP_LOG(APP_LOG_LEVEL_INFO, "config: lat=%ld/1e6 lon=%ld/1e6 lang=%s",
|
APP_LOG(APP_LOG_LEVEL_INFO, "config: lat=%ld/1e6 lon=%ld/1e6 gender=%d lang=%s",
|
||||||
(long)(cfg->birth.latitude * 1000000), (long)(cfg->birth.longitude * 1000000), cfg->lang);
|
(long)(cfg->birth.latitude * 1000000), (long)(cfg->birth.longitude * 1000000),
|
||||||
|
(int)cfg->gender, cfg->lang);
|
||||||
APP_LOG(APP_LOG_LEVEL_INFO, "config: notify_enabled=%d at %02d:%02d",
|
APP_LOG(APP_LOG_LEVEL_INFO, "config: notify_enabled=%d at %02d:%02d",
|
||||||
cfg->notify_enabled, cfg->notify_hour, cfg->notify_minute);
|
cfg->notify_enabled, cfg->notify_hour, cfg->notify_minute);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <pebble.h>
|
#include <pebble.h>
|
||||||
|
|
||||||
#include "../../../engine/src/astro.h"
|
#include "../../../engine/src/astro.h"
|
||||||
|
#include "../../../engine/src/tarot.h" /* Gender */
|
||||||
|
|
||||||
/* Everything the watch needs that isn't computed fresh each day: birth
|
/* Everything the watch needs that isn't computed fresh each day: birth
|
||||||
* data (the same fields as dist/user.properties), display language, and
|
* data (the same fields as dist/user.properties), display language, and
|
||||||
@@ -13,6 +14,9 @@
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
bool configured; /* false until the config page has been submitted once */
|
bool configured; /* false until the config page has been submitted once */
|
||||||
BirthData birth;
|
BirthData birth;
|
||||||
|
Gender gender; /* who the reading is for - only affects Celtic Cross
|
||||||
|
* position text pronouns, see engine/src/tarot.h.
|
||||||
|
* Defaults to GENDER_UNSPECIFIED (0). */
|
||||||
char lang[8]; /* e.g. "en", "de" - matches engine/i18n/<lang>.lang's own code */
|
char lang[8]; /* e.g. "en", "de" - matches engine/i18n/<lang>.lang's own code */
|
||||||
bool notify_enabled;
|
bool notify_enabled;
|
||||||
int notify_hour; /* 0-23, the watch's own local wall-clock time (i.e.
|
int notify_hour; /* 0-23, the watch's own local wall-clock time (i.e.
|
||||||
|
|||||||
+2
-2
@@ -25,7 +25,7 @@ static void on_config_updated(const WatchConfig *cfg) {
|
|||||||
if (!s_config.configured) return;
|
if (!s_config.configured) return;
|
||||||
|
|
||||||
config_log(&s_config); /* the values this newly-recomputed reading is based on */
|
config_log(&s_config); /* the values this newly-recomputed reading is based on */
|
||||||
reading_state_recompute(&s_config.birth);
|
reading_state_recompute(&s_config.birth, s_config.gender);
|
||||||
if (was_configured) {
|
if (was_configured) {
|
||||||
report_window_reload();
|
report_window_reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -41,7 +41,7 @@ int main(void) {
|
|||||||
config_log(&s_config); /* the values this session's reading (if any) is based on */
|
config_log(&s_config); /* the values this session's reading (if any) is based on */
|
||||||
|
|
||||||
if (s_config.configured) {
|
if (s_config.configured) {
|
||||||
reading_state_recompute(&s_config.birth);
|
reading_state_recompute(&s_config.birth, s_config.gender);
|
||||||
report_window_push();
|
report_window_push();
|
||||||
} else {
|
} else {
|
||||||
show_setup_required();
|
show_setup_required();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
static DailyReading s_reading;
|
static DailyReading s_reading;
|
||||||
static DailyInterpretation s_interp;
|
static DailyInterpretation s_interp;
|
||||||
|
|
||||||
void reading_state_recompute(const BirthData *birth) {
|
void reading_state_recompute(const BirthData *birth, Gender gender) {
|
||||||
time_t now = time(NULL);
|
time_t now = time(NULL);
|
||||||
/* Plain gmtime() (not the POSIX-only gmtime_r()), used immediately -
|
/* Plain gmtime() (not the POSIX-only gmtime_r()), used immediately -
|
||||||
* same justification as astro.c's own time_from_unix(). */
|
* same justification as astro.c's own time_from_unix(). */
|
||||||
@@ -14,7 +14,7 @@ void reading_state_recompute(const BirthData *birth) {
|
|||||||
char seed[16];
|
char seed[16];
|
||||||
snprintf(seed, sizeof seed, "%04d-%02d-%02d", utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday);
|
snprintf(seed, sizeof seed, "%04d-%02d-%02d", utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday);
|
||||||
|
|
||||||
reading_generate(seed, birth, now, &s_reading);
|
reading_generate(seed, birth, gender, now, &s_reading);
|
||||||
interpret_daily_reading(&s_reading, &s_interp);
|
interpret_daily_reading(&s_reading, &s_interp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
#include "../../../interpreter/src/significance.h"
|
#include "../../../interpreter/src/significance.h"
|
||||||
|
|
||||||
/* Recomputes today's reading (astro + tarot + interpretation) from
|
/* Recomputes today's reading (astro + tarot + interpretation) from
|
||||||
* `birth`, using the watch's current clock: today's UTC date as the
|
* `birth`/`gender`, using the watch's current clock: today's UTC date as
|
||||||
* tarot seed (same convention as dist/run-engine.sh - see CLAUDE.md's
|
* the tarot seed (same convention as dist/run-engine.sh - see CLAUDE.md's
|
||||||
* "Build & run") and the current moment for the transit snapshot. Safe
|
* "Build & run") and the current moment for the transit snapshot. Safe
|
||||||
* to call again later (e.g. the day rolled over while the app stayed
|
* to call again later (e.g. the day rolled over while the app stayed
|
||||||
* open) - overwrites the previous result in place. */
|
* open) - overwrites the previous result in place. */
|
||||||
void reading_state_recompute(const BirthData *birth);
|
void reading_state_recompute(const BirthData *birth, Gender gender);
|
||||||
|
|
||||||
/* Valid only after at least one reading_state_recompute() call. */
|
/* Valid only after at least one reading_state_recompute() call. */
|
||||||
const DailyReading *reading_state_get_reading(void);
|
const DailyReading *reading_state_get_reading(void);
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ static void build_content(int16_t width) {
|
|||||||
const TarotDraw *draw = &reading->spread.positions[i];
|
const TarotDraw *draw = &reading->spread.positions[i];
|
||||||
|
|
||||||
snprintf(s_position_titles[i], sizeof s_position_titles[i], "%s: %s %s",
|
snprintf(s_position_titles[i], sizeof s_position_titles[i], "%s: %s %s",
|
||||||
tarot_position_name((CelticCrossPosition)i), tarot_card_name(draw->card),
|
tarot_position_name((CelticCrossPosition)i, reading->gender), tarot_card_name(draw->card),
|
||||||
reversed_marker(draw->reversed));
|
reversed_marker(draw->reversed));
|
||||||
y = add_text(width, y, s_position_titles[i], s_font_subhead);
|
y = add_text(width, y, s_position_titles[i], s_font_subhead);
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ static void build_content(int16_t width) {
|
|||||||
scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_slots[i].bitmap_layer));
|
scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_slots[i].bitmap_layer));
|
||||||
y += s_slots[i].height + MARGIN;
|
y += s_slots[i].height + MARGIN;
|
||||||
|
|
||||||
y = add_text(width, y, tarot_position_description((CelticCrossPosition)i), s_font_body);
|
y = add_text(width, y, tarot_position_description((CelticCrossPosition)i, reading->gender), s_font_body);
|
||||||
y = add_text(width, y, tarot_card_meaning(draw->card, draw->reversed), s_font_body);
|
y = add_text(width, y, tarot_card_meaning(draw->card, draw->reversed), s_font_body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ var LANGUAGES = [
|
|||||||
{ code: 'en', label: 'English' },
|
{ code: 'en', label: 'English' },
|
||||||
{ code: 'de', label: 'Deutsch' }
|
{ code: 'de', label: 'Deutsch' }
|
||||||
];
|
];
|
||||||
|
// Matches engine/src/tarot.h's Gender enum (GENDER_UNSPECIFIED = 0,
|
||||||
|
// GENDER_MALE = 1, GENDER_FEMALE = 2) - only affects the Celtic Cross
|
||||||
|
// position text's pronouns, never the chart/draw itself.
|
||||||
|
var GENDERS = [
|
||||||
|
{ code: 0, label: 'Prefer not to say' },
|
||||||
|
{ code: 1, label: 'Male' },
|
||||||
|
{ code: 2, label: 'Female' }
|
||||||
|
];
|
||||||
|
|
||||||
// 120x120 PNG of res/logo-small.png, inlined as base64 - this page is a
|
// 120x120 PNG of res/logo-small.png, inlined as base64 - this page is a
|
||||||
// bare `data:` URI with no server, so there's nowhere else to load an
|
// bare `data:` URI with no server, so there's nowhere else to load an
|
||||||
@@ -65,6 +73,16 @@ function buildLanguageOptions(selected) {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGenderOptions(selected) {
|
||||||
|
var html = '';
|
||||||
|
for (var i = 0; i < GENDERS.length; i++) {
|
||||||
|
var gender = GENDERS[i];
|
||||||
|
var isSelected = gender.code === selected ? ' selected' : '';
|
||||||
|
html += '<option value="' + gender.code + '"' + isSelected + '>' + gender.label + '</option>';
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
// Runs *inside* the webview (embedded as a <script> string below), not
|
// Runs *inside* the webview (embedded as a <script> string below), not
|
||||||
// in pkjs's own context - reads the form, validates it, and hands the
|
// in pkjs's own context - reads the form, validates it, and hands the
|
||||||
// result back to pkjs via the standard Pebble config-page convention:
|
// result back to pkjs via the standard Pebble config-page convention:
|
||||||
@@ -80,6 +98,7 @@ var FORM_SCRIPT =
|
|||||||
' var utcOffset = parseFloat(document.getElementById("birth_utc_offset").value);' +
|
' var utcOffset = parseFloat(document.getElementById("birth_utc_offset").value);' +
|
||||||
' var lat = parseFloat(document.getElementById("birth_lat").value);' +
|
' var lat = parseFloat(document.getElementById("birth_lat").value);' +
|
||||||
' var lon = parseFloat(document.getElementById("birth_lon").value);' +
|
' var lon = parseFloat(document.getElementById("birth_lon").value);' +
|
||||||
|
' var gender = parseInt(document.getElementById("gender").value, 10);' +
|
||||||
' var lang = document.getElementById("lang").value;' +
|
' var lang = document.getElementById("lang").value;' +
|
||||||
' var notifyEnabled = document.getElementById("notify_enabled").checked;' +
|
' var notifyEnabled = document.getElementById("notify_enabled").checked;' +
|
||||||
' var notifyTime = document.getElementById("notify_time").value || "08:00";' +
|
' var notifyTime = document.getElementById("notify_time").value || "08:00";' +
|
||||||
@@ -105,6 +124,7 @@ var FORM_SCRIPT =
|
|||||||
' birth_utc_offset_minutes: Math.round(utcOffset * 60),' +
|
' birth_utc_offset_minutes: Math.round(utcOffset * 60),' +
|
||||||
' birth_lat_microdeg: Math.round(lat * 1000000),' +
|
' birth_lat_microdeg: Math.round(lat * 1000000),' +
|
||||||
' birth_lon_microdeg: Math.round(lon * 1000000),' +
|
' birth_lon_microdeg: Math.round(lon * 1000000),' +
|
||||||
|
' gender: gender,' +
|
||||||
' lang: lang,' +
|
' lang: lang,' +
|
||||||
' notify_enabled: notifyEnabled ? 1 : 0,' +
|
' notify_enabled: notifyEnabled ? 1 : 0,' +
|
||||||
' notify_hour: parseInt(notifyParts[0], 10),' +
|
' notify_hour: parseInt(notifyParts[0], 10),' +
|
||||||
@@ -121,6 +141,7 @@ var FORM_SCRIPT =
|
|||||||
|
|
||||||
function buildConfigHtml(saved) {
|
function buildConfigHtml(saved) {
|
||||||
var lang = saved.lang || 'en';
|
var lang = saved.lang || 'en';
|
||||||
|
var gender = typeof saved.gender === 'number' ? saved.gender : 0;
|
||||||
var notifyChecked = saved.notify_enabled ? ' checked' : '';
|
var notifyChecked = saved.notify_enabled ? ' checked' : '';
|
||||||
return '<!doctype html>' +
|
return '<!doctype html>' +
|
||||||
'<html><head><meta charset="utf-8">' +
|
'<html><head><meta charset="utf-8">' +
|
||||||
@@ -162,6 +183,8 @@ function buildConfigHtml(saved) {
|
|||||||
'<input type="number" id="birth_lon" step="0.0001" min="-180" max="180" value="' +
|
'<input type="number" id="birth_lon" step="0.0001" min="-180" max="180" value="' +
|
||||||
escapeHtml(saved.birth_lon) + '" required></div>' +
|
escapeHtml(saved.birth_lon) + '" required></div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
'<label>Gender (used for reading pronouns)</label>' +
|
||||||
|
'<select id="gender">' + buildGenderOptions(gender) + '</select>' +
|
||||||
'<h2>Display</h2>' +
|
'<h2>Display</h2>' +
|
||||||
'<label>Language</label>' +
|
'<label>Language</label>' +
|
||||||
'<select id="lang">' + buildLanguageOptions(lang) + '</select>' +
|
'<select id="lang">' + buildLanguageOptions(lang) + '</select>' +
|
||||||
@@ -205,6 +228,7 @@ Pebble.addEventListener('webviewclosed', function (e) {
|
|||||||
birth_utc_offset: result.saved_birth_utc_offset,
|
birth_utc_offset: result.saved_birth_utc_offset,
|
||||||
birth_lat: result.saved_birth_lat,
|
birth_lat: result.saved_birth_lat,
|
||||||
birth_lon: result.saved_birth_lon,
|
birth_lon: result.saved_birth_lon,
|
||||||
|
gender: result.gender,
|
||||||
lang: result.lang,
|
lang: result.lang,
|
||||||
notify_enabled: result.notify_enabled === 1,
|
notify_enabled: result.notify_enabled === 1,
|
||||||
notify_time: result.saved_notify_time
|
notify_time: result.saved_notify_time
|
||||||
@@ -219,6 +243,7 @@ Pebble.addEventListener('webviewclosed', function (e) {
|
|||||||
BIRTH_UTC_OFFSET_MINUTES: result.birth_utc_offset_minutes,
|
BIRTH_UTC_OFFSET_MINUTES: result.birth_utc_offset_minutes,
|
||||||
BIRTH_LAT_MICRODEG: result.birth_lat_microdeg,
|
BIRTH_LAT_MICRODEG: result.birth_lat_microdeg,
|
||||||
BIRTH_LON_MICRODEG: result.birth_lon_microdeg,
|
BIRTH_LON_MICRODEG: result.birth_lon_microdeg,
|
||||||
|
GENDER: result.gender,
|
||||||
LANG: result.lang,
|
LANG: result.lang,
|
||||||
NOTIFY_ENABLED: result.notify_enabled,
|
NOTIFY_ENABLED: result.notify_enabled,
|
||||||
NOTIFY_HOUR: result.notify_hour,
|
NOTIFY_HOUR: result.notify_hour,
|
||||||
|
|||||||
Reference in New Issue
Block a user