Skip to content

Commit 3145f40

Browse files
committed
fix(i18n,widgets): a translated reference card printed every fact twice, and the copy-paste snippet pinned every embedder to English
Two defects that only appear once the page is not English. `<roxy-reference-card>` builds its whole output from `Object.entries`, so the day the API began echoing `nameLocalized` beside `name` the card began drawing one fact as two rows, each under its own heading, on every non-English lookup and on a page whose owner changed nothing. That is a regression rather than a missing feature: the card was correct until we shipped the additive field. `foldLocalized` now runs before anything reads a key, at all three levels that read one (the record, a nested object, and the join in `objectLabel`), so the reader gets their value under the canonical name and the twin is gone. The generated snippet emitted `lang="en"`. A component with no `lang` resolves its language from the element, then the nearest ancestor carrying one, then `<html lang>`, which is what lets a snippet pasted into a Spanish CMS speak Spanish with no edit. The attribute won that chain at the first link, so every copy-paste embedder was pinned to English including the ones whose page already said otherwise. Dropped from the source, so it clears the catalogue, the registry and the docs manifest in one edit. Also: - `<roxy-data>`, `<roxy-bodygraph>`, `<roxy-hd-type-card>`, `<roxy-hd-variables>` and `<roxy-reference-card>` now carry catalogue entries in all seven languages. `<roxy-hd-connection>` and `<roxy-hd-penta>` are deliberately left English: what they write is doctrine prose rather than labels, and a half-translated card is worse than a consistent one. A build guard now makes the half state uncommittable. - Centre names are longer once translated, and an SVG clips at its viewport rather than wrapping, so a Russian bodygraph would have lost text silently: our layout e2e skips every node inside an SVG. `labelFit` sizes them; English charts stay byte-identical. - The truncation guard was anchored on `??`, which was only the half of the class that had bitten us. A bare `modality.slice(0, 3)` in the element-modality header turned `Cardinal` into `Car` on purpose and every translated word into debris, splitting a matra off its consonant in Devanagari. The guard is now any short literal slice, comments excluded so the why-notes quoting the bug do not trip it. - Spanish read `Configuraciones de la carta` for chart patterns, which is settings, not aspect figures. `Figuras planetarias`, sourced. - Spec refreshed from live: `transitInterpretation` is now required on a transit aspect, which the wheel fixture was missing.
1 parent c4f6a6c commit 3145f40

26 files changed

Lines changed: 2760 additions & 399 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ The language comes from `lang` on the element, else the nearest ancestor carryin
553553

554554
English is the default and needs no catalogue. Load only the languages your site serves; one catalogue covers every Roxy component on the page.
555555

556-
A catalogue ships for every language the API serves: `de`, `es`, `fr`, `hi`, `pt`, `ru`, `tr`. Each covers `<roxy-natal-chart>`, `<roxy-transit-wheel>` and the shared labels every component inherits; the remaining components write English labels until their entries land. **The values the API returns are never rewritten**: a planet, sign, aspect, element or house-system name renders exactly as the response carries it, so the words on the chart and the words in the response can never disagree. Ask for a language and the response carries the reader-facing name beside the English one, so the chart prints the translated name while the English one stays available to compare against in your own code.
556+
A catalogue ships for every language the API serves: `de`, `es`, `fr`, `hi`, `pt`, `ru`, `tr`. Each covers `<roxy-natal-chart>`, `<roxy-transit-wheel>`, `<roxy-data>`, `<roxy-bodygraph>`, `<roxy-hd-type-card>`, `<roxy-hd-variables>`, `<roxy-reference-card>` and the shared labels every component inherits; the remaining components write English labels until their entries land. Every term is sourced from named astrology references in that language rather than translated word for word. **The values the API returns are never rewritten**: a planet, sign, aspect or house-system name renders exactly as the response carries it, so the words on the chart and the words in the response can never disagree. Ask for a language and the response carries the reader-facing name beside the English one, so the chart prints the translated name while the English one stays available to compare against in your own code.
557557

558558
Dates, times and numbers need no catalogue at all. They follow the same page language and are written the way that language writes them, hour cycle included, so a German page reads `15. Jan. 1990, 14:30` and an Argentine one `15 de ene de 1990, 2:30 p. m.`.
559559

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,7 @@ Two things are separate on purpose. The catalogue is the wording this library wr
788788

789789
Dates, times and numbers follow the page as well, in the conventions that language actually uses: a Spanish page reads `15 ene 1990`, an Argentine one `15 de ene de 1990`, a German one `15. Jan. 1990, 14:30`. The hour cycle comes from the language, not from us, so nobody gets a 12-hour clock on a page that writes 24. This needs no catalogue and no attribute: it follows the same `lang` your page already declares.
790790

791-
A catalogue ships for every language the API serves: `de`, `es`, `fr`, `hi`, `pt`, `ru`, `tr`. Each covers `<roxy-natal-chart>`, `<roxy-transit-wheel>` and the shared chrome every component inherits; the remaining components render English labels until their catalogue entries land.
791+
A catalogue ships for every language the API serves: `de`, `es`, `fr`, `hi`, `pt`, `ru`, `tr`. Each covers `<roxy-natal-chart>`, `<roxy-transit-wheel>`, `<roxy-data>`, `<roxy-bodygraph>`, `<roxy-hd-type-card>`, `<roxy-hd-variables>`, `<roxy-reference-card>` and the shared chrome every component inherits; the remaining components render English labels until their catalogue entries land. Every term in every catalogue is sourced from named astrology references in that language rather than translated word for word, because the words a practitioner reads are the product.
792792

793793
## Reliability
794794

apps/docs/manifest.js

Lines changed: 59 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ui/components-catalog.json

Lines changed: 59 additions & 59 deletions
Large diffs are not rendered by default.

packages/ui/src/components/bodygraph.ts

Lines changed: 149 additions & 66 deletions
Large diffs are not rendered by default.

packages/ui/src/components/data.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ import { humanize } from '../utils/string.js';
2525
* round to 2 decimals, booleans read Yes/No, ISO dates and datetimes format
2626
* for the locale, SCREAMING_SNAKE enums humanize, http(s) strings link out.
2727
*
28+
* Every word this component writes goes through `this.t()`, which is not
29+
* optional politeness: `suppress()` already folds the API's localized values
30+
* into their canonical columns, so before the catalogue landed a Spanish page
31+
* read `Sol` and `Piscis` under `Yes`, `No` and `31 rows` in English. What a
32+
* catalogue CANNOT reach is the column headings, which come from the wire field
33+
* name through `humanize()` and are therefore computed per response; those stay
34+
* English on both this path and the PHP one, which at least keeps them
35+
* consistent with each other. The PHP twin has none of these strings yet, so
36+
* the JS and no-JS paths currently disagree on the chrome as well as the fold
37+
* (`docs/todo.md`, and lesson 6 for why that matters).
38+
*
2839
* When a schema declares an `x-roxy-ui` hint, a future dispatcher can opt
2940
* into a hand-tuned component instead of this fallback.
3041
*/
@@ -306,10 +317,10 @@ export class RoxyData extends RoxyDataElement<Json> {
306317
protected renderData(data: Json) {
307318
if (this.depth >= MAX_DEPTH) {
308319
return html`<div class="roxy-empty" role="status">
309-
Nested data omitted
320+
${this.t('Nested data omitted')}
310321
</div>`;
311322
}
312-
return html`<div class="roxy-card" aria-label="Generic data display">
323+
return html`<div class="roxy-card" aria-label=${this.t('Generic data display')}>
313324
${this.renderValue(data)}
314325
</div>`;
315326
}
@@ -323,7 +334,7 @@ export class RoxyData extends RoxyDataElement<Json> {
323334

324335
private renderArray(arr: Json[]): TemplateResult {
325336
if (arr.length === 0) {
326-
return html`<div class="roxy-empty" role="status">Empty list</div>`;
337+
return html`<div class="roxy-empty" role="status">${this.t('Empty list')}</div>`;
327338
}
328339
if (arr.every(isPrimitive)) {
329340
return this.renderChips(arr as (Scalar | null)[]);
@@ -367,7 +378,7 @@ export class RoxyData extends RoxyDataElement<Json> {
367378
const table = html`<div
368379
class="roxy-table-wrap"
369380
role="group"
370-
aria-label="Data table"
381+
aria-label=${this.t('Data table')}
371382
tabindex="0"
372383
>
373384
<table class="roxy-table" role="table">
@@ -396,7 +407,7 @@ export class RoxyData extends RoxyDataElement<Json> {
396407
// inline. Past the threshold it folds away behind its own row count.
397408
if (clean.length > DETAILS_ROWS) {
398409
return html`<details class="roxy-table-details">
399-
<summary>${clean.length} rows</summary>
410+
<summary>${this.t('{{count}} rows', { count: clean.length })}</summary>
400411
${table}
401412
</details>`;
402413
}
@@ -458,7 +469,7 @@ export class RoxyData extends RoxyDataElement<Json> {
458469
? html`<img
459470
class="roxy-image"
460471
src=${String(obj[imageKey])}
461-
alt=${titleKey ? String(obj[titleKey]) : 'illustration'}
472+
alt=${titleKey ? String(obj[titleKey]) : this.t('illustration')}
462473
loading="lazy"
463474
/>`
464475
: nothing
@@ -512,7 +523,7 @@ export class RoxyData extends RoxyDataElement<Json> {
512523
private renderField(value: Json, key?: string): TemplateResult | string {
513524
if (value === null || value === undefined) return '';
514525
if (value === true && key !== undefined && isBadgeKey(key)) {
515-
return html`<span class="roxy-badge">Yes</span>`;
526+
return html`<span class="roxy-badge">${this.t('Yes')}</span>`;
516527
}
517528
if (isPrimitive(value)) return html`${this.scalarTemplate(value)}`;
518529
if (Array.isArray(value) && value.every(isPrimitive)) {
@@ -535,7 +546,7 @@ export class RoxyData extends RoxyDataElement<Json> {
535546
): TemplateResult | string {
536547
if (value === null || value === undefined) return '';
537548
if (value === true && key !== undefined && isBadgeKey(key)) {
538-
return html`<span class="roxy-badge">Yes</span>`;
549+
return html`<span class="roxy-badge">${this.t('Yes')}</span>`;
539550
}
540551
if (isPrimitive(value)) return this.scalarTemplate(value);
541552
if (Array.isArray(value) && value.every(isPrimitive)) {
@@ -568,7 +579,7 @@ export class RoxyData extends RoxyDataElement<Json> {
568579
if (typeof value === 'number') {
569580
return formatNumber(value, 2) || String(value);
570581
}
571-
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
582+
if (typeof value === 'boolean') return value ? this.t('Yes') : this.t('No');
572583
if (ISO_DATE.test(value)) {
573584
const time = formatTime(this.effectiveLang(), value);
574585
const date = formatDate(this.effectiveLang(), value);

packages/ui/src/components/hd-type-card.ts

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
renderHdThemes,
1515
} from '../utils/hd-reading.js';
1616
import { interpAccordionStyles } from '../utils/interp-accordion.js';
17+
import { display } from '../utils/localized.js';
1718

1819
type HdIdentity = CalculateTypeResponse | CalculateProfileResponse;
1920

@@ -24,6 +25,8 @@ type HdIdentity = CalculateTypeResponse | CalculateProfileResponse;
2425
* Both endpoints answer "who is this person" rather than "how is the chart wired", so one card serves both and detects which it was given: the type response leads with the type and carries the strategy, authority, and aura readings; the profile response carries the two lines and their keynotes. This is the same shape-detecting pattern `roxy-positions-table` uses across the five Western point-list endpoints.
2526
*
2627
* The interpretation surface (fact tiles, lead paragraph, signature and not-self pills, the exclusive reading accordion, the line keynotes) is shared with `roxy-bodygraph` through `utils/hd-reading.ts`, because `/human-design/type` returns a strict subset of the bodygraph interpretation fields. Reach for `roxy-bodygraph` when the chart itself is wanted; reach for this when only the reading is.
28+
*
29+
* The type, strategy, authority, signature and not-self values are read through `display()`, so a translated response prints the display half and the canonical English stays available to anything that compares. `/human-design/profile` carries no vocabulary at all: its profile is `5/1` and its two keynotes are prose the API translates in place.
2730
*/
2831
@customElement('roxy-hd-type-card')
2932
export class RoxyHdTypeCard extends RoxyDataElement<HdIdentity> {
@@ -67,7 +70,7 @@ export class RoxyHdTypeCard extends RoxyDataElement<HdIdentity> {
6770
];
6871

6972
protected renderEmpty() {
70-
return html`<div class="roxy-empty" role="status">No Human Design data</div>`;
73+
return html`<div class="roxy-empty" role="status">${this.t('No Human Design data')}</div>`;
7174
}
7275

7376
protected renderData(d: HdIdentity) {
@@ -80,37 +83,52 @@ export class RoxyHdTypeCard extends RoxyDataElement<HdIdentity> {
8083
private renderType(d: CalculateTypeResponse) {
8184
return html`<div class="wrap" part="card">
8285
<header class="head" part="header">
83-
<h2 class="title">Type</h2>
86+
<h2 class="title">${this.t('Type')}</h2>
8487
${
8588
d.type || d.profile
8689
? html`<div class="type-line">
87-
${[d.type, d.profile ? `Profile ${d.profile}` : ''].filter(Boolean).join(' · ')}
90+
${[
91+
display(d, 'type'),
92+
d.profile
93+
? this.t('Profile {{profile}}', { profile: d.profile })
94+
: '',
95+
]
96+
.filter(Boolean)
97+
.join(' · ')}
8898
</div>`
8999
: nothing
90100
}
91101
</header>
92102
${renderHdFacts([
93-
{ label: 'Type', value: d.type },
94-
{ label: 'Strategy', value: d.strategy },
95-
{ label: 'Authority', value: d.authority },
96-
{ label: 'Profile', value: d.profile },
103+
{ label: this.t('Type'), value: display(d, 'type') },
104+
{ label: this.t('Strategy'), value: display(d, 'strategy') },
105+
{ label: this.t('Authority'), value: display(d, 'authority') },
106+
{ label: this.t('Profile'), value: d.profile },
97107
])}
98108
${
99109
// The tiles above name the type; this paragraph explains it.
100110
d.typeDescription && !this.hideReadings
101111
? html`<p class="lead">${d.typeDescription}</p>`
102112
: nothing
103113
}
104-
${renderHdThemes(d.signature, d.notSelf)}
114+
${renderHdThemes(
115+
d.signature ? display(d, 'signature') : undefined,
116+
d.notSelf ? display(d, 'notSelf') : undefined,
117+
this.translator,
118+
)}
105119
${this.renderInterpretation(
106120
[
107-
{ label: 'Strategy', aside: d.strategy, body: d.strategyDescription },
108121
{
109-
label: 'Authority',
110-
aside: d.authority,
122+
label: this.t('Strategy'),
123+
aside: display(d, 'strategy'),
124+
body: d.strategyDescription,
125+
},
126+
{
127+
label: this.t('Authority'),
128+
aside: display(d, 'authority'),
111129
body: d.authorityDescription,
112130
},
113-
{ label: 'Aura', body: d.aura },
131+
{ label: this.t('Aura'), body: d.aura },
114132
],
115133
'hd-type-reading',
116134
)}
@@ -123,27 +141,33 @@ export class RoxyHdTypeCard extends RoxyDataElement<HdIdentity> {
123141
private renderProfile(d: CalculateProfileResponse) {
124142
return html`<div class="wrap" part="card">
125143
<header class="head" part="header">
126-
<h2 class="title">Profile</h2>
144+
<h2 class="title">${this.t('Profile')}</h2>
127145
${d.profile ? html`<div class="type-line">${d.profile}</div>` : nothing}
128146
</header>
129147
${renderHdFacts([
130-
{ label: 'Profile', value: d.profile },
131-
{ label: 'Personality line', value: d.personalityLine?.toString() },
132-
{ label: 'Design line', value: d.designLine?.toString() },
148+
{ label: this.t('Profile'), value: d.profile },
149+
{
150+
label: this.t('Personality line'),
151+
value: d.personalityLine?.toString(),
152+
},
153+
{ label: this.t('Design line'), value: d.designLine?.toString() },
133154
])}
134155
${
135156
// The section holds nothing but the two keynote sentences, and the line
136157
// numbers are already tiles above it, so it goes whole.
137158
this.hideReadings
138159
? nothing
139160
: html`<section class="block" part="section lines">
140-
<h3>Lines</h3>
141-
${renderHdKeynotes({
142-
personality: d.personalityKeynote,
143-
personalityLine: d.personalityLine,
144-
design: d.designKeynote,
145-
designLine: d.designLine,
146-
})}
161+
<h3>${this.t('Lines')}</h3>
162+
${renderHdKeynotes(
163+
{
164+
personality: d.personalityKeynote,
165+
personalityLine: d.personalityLine,
166+
design: d.designKeynote,
167+
designLine: d.designLine,
168+
},
169+
this.translator,
170+
)}
147171
</section>`
148172
}
149173
</div>`;

0 commit comments

Comments
 (0)