@cavuno/board 2.0.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/CONTRIBUTING.md +0 -11
- package/README.md +8 -7
- package/dist/{_spec-BKtVFcV9.d.mts → _spec-nkWAraS0.d.mts} +4 -18
- package/dist/{_spec-BKtVFcV9.d.ts → _spec-nkWAraS0.d.ts} +4 -18
- package/dist/bin.mjs +40 -57
- package/dist/{board-3Xj_Y-Tx.d.ts → board-NE98PlwN.d.ts} +2 -3
- package/dist/{board-pNN710Zc.d.mts → board-muu8BI8P.d.mts} +2 -3
- package/dist/doctor.js +21 -47
- package/dist/doctor.mjs +24 -50
- package/dist/filters.d.mts +2 -2
- package/dist/filters.d.ts +2 -2
- package/dist/format.d.mts +3 -3
- package/dist/format.d.ts +3 -3
- package/dist/index.d.mts +11 -27
- package/dist/index.d.ts +11 -27
- package/dist/index.js +3 -3
- package/dist/index.mjs +3 -3
- package/dist/{jobs-C4qRfCdP.d.ts → jobs-BiEvL9d7.d.ts} +1 -1
- package/dist/{jobs-k84C0v_8.d.mts → jobs-lVvKCBCU.d.mts} +1 -1
- package/dist/{salaries--J5mTylN.d.ts → salaries-BahBpxWT.d.ts} +2 -2
- package/dist/{salaries-C9YLO5NG.d.mts → salaries-ChSqDBAM.d.mts} +2 -2
- package/dist/{search-CE_qQiM7.d.mts → search-CCyFWPi3.d.mts} +1 -1
- package/dist/{search-CLRQ_El9.d.ts → search-CyRvVMUp.d.ts} +1 -1
- package/dist/seo.d.mts +4 -4
- package/dist/seo.d.ts +4 -4
- package/dist/server.d.mts +5 -5
- package/dist/server.d.ts +5 -5
- package/dist/sitemap.d.mts +5 -5
- package/dist/sitemap.d.ts +5 -5
- package/dist/skills.d.mts +15 -4
- package/dist/skills.d.ts +15 -4
- package/dist/skills.js +30 -7
- package/dist/skills.mjs +30 -8
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/package.json +3 -13
- package/skills/cavuno-board-account/SKILL.md +54 -66
- package/skills/cavuno-board-api-client/SKILL.md +74 -0
- package/skills/cavuno-board-applications/SKILL.md +38 -52
- package/skills/cavuno-board-auth/SKILL.md +36 -61
- package/skills/cavuno-board-blog/SKILL.md +60 -49
- package/skills/cavuno-board-companies/SKILL.md +71 -53
- package/skills/cavuno-board-errors/SKILL.md +29 -46
- package/skills/cavuno-board-filters/SKILL.md +64 -75
- package/skills/cavuno-board-format/SKILL.md +63 -68
- package/skills/cavuno-board-i18n/SKILL.md +33 -62
- package/skills/cavuno-board-job-alerts/SKILL.md +68 -57
- package/skills/cavuno-board-jobs/SKILL.md +63 -55
- package/skills/cavuno-board-messaging/SKILL.md +53 -59
- package/skills/cavuno-board-paywall/SKILL.md +48 -54
- package/skills/cavuno-board-post-a-job/SKILL.md +118 -0
- package/skills/cavuno-board-salaries/SKILL.md +73 -50
- package/skills/cavuno-board-search-suggestions/SKILL.md +82 -0
- package/skills/cavuno-board-seo/BLOG.md +40 -0
- package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +49 -0
- package/skills/cavuno-board-seo/LISTING.md +36 -0
- package/skills/cavuno-board-seo/SALARY.md +61 -0
- package/skills/cavuno-board-seo/SKILL.md +32 -163
- package/skills/cavuno-board-server-sessions/SKILL.md +143 -0
- package/skills/cavuno-board-setup/SKILL.md +43 -80
- package/skills/cavuno-board-sitemap/SKILL.md +76 -93
- package/skills/cavuno-board-smoke-test/SKILL.md +34 -69
- package/skills/manifest.json +43 -57
- package/dist/theme.d.mts +0 -115
- package/dist/theme.d.ts +0 -115
- package/dist/theme.js +0 -195
- package/dist/theme.mjs +0 -174
- package/skills/cavuno-board-client/SKILL.md +0 -89
- package/skills/cavuno-board-job-posting/SKILL.md +0 -116
- package/skills/cavuno-board-server/SKILL.md +0 -229
- package/skills/cavuno-board-suggest/SKILL.md +0 -119
- package/skills/cavuno-board-theme/SKILL.md +0 -57
- package/skills/flavors/tanstack-start/SKILL.md +0 -100
|
@@ -1,100 +1,89 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-filters
|
|
3
|
-
description:
|
|
3
|
+
description: Listing-filter contracts with @cavuno/board. Use for job filter controls, sort controls, listing URL validation, or taxonomy-backed filter options.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Listing filters
|
|
7
7
|
|
|
8
|
-
`@cavuno/board/filters` is the
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
`@cavuno/board/filters` is the shared vocabulary and parser for every job
|
|
9
|
+
listing route, including category, skill, and location pages. Display labels
|
|
10
|
+
live in `@cavuno/board/format`; search POST bodies use `jobs.search`; typeahead
|
|
11
|
+
UI behavior lives in `cavuno-board-search-suggestions`.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Parse public URL input
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- Turning values into card display text — `cavuno-board-format`.
|
|
22
|
-
- Search QUERIES (`jobs.search` bodies) — this module is about the listing
|
|
23
|
-
URL contract, not the search POST body.
|
|
24
|
-
- Search-dropdown typeahead — `cavuno-board-suggest`.
|
|
25
|
-
|
|
26
|
-
## Parse listing URLs
|
|
15
|
+
Run every listing URL through `parseListingFilters`. Public input is
|
|
16
|
+
permissive: unknown values are dropped and parsing does not throw. Seniority
|
|
17
|
+
and company accept repeated parameters or comma-separated strings, then trim,
|
|
18
|
+
lowercase, deduplicate, and preserve order. Company is an open set of public
|
|
19
|
+
slugs capped at the first 10 values.
|
|
27
20
|
|
|
28
21
|
```ts snippet
|
|
29
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_SORT,
|
|
24
|
+
parseListingFilters,
|
|
25
|
+
} from '@cavuno/board/filters';
|
|
30
26
|
|
|
31
27
|
const filters = parseListingFilters(rawSearchParams);
|
|
32
|
-
|
|
28
|
+
const selectedSort = filters.sort ?? DEFAULT_SORT;
|
|
33
29
|
|
|
34
30
|
const page = await board.jobs.list({
|
|
35
31
|
limit: 20,
|
|
36
32
|
seniority: filters.seniority,
|
|
37
33
|
companySlug: filters.company,
|
|
38
34
|
remoteOption: filters.remoteOption ? [filters.remoteOption] : undefined,
|
|
39
|
-
employmentType: filters.employmentType
|
|
35
|
+
employmentType: filters.employmentType
|
|
36
|
+
? [filters.employmentType]
|
|
37
|
+
: undefined,
|
|
40
38
|
});
|
|
41
39
|
```
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
the hosted rules: trim, lowercase, dedupe, keep order. `company` is an open
|
|
46
|
-
value set (public slugs) capped at 10 (wire max; first 10 kept).
|
|
41
|
+
Company slugs are the URL identity. Map `filters.company` directly to
|
|
42
|
+
`companySlug` in `jobs.list` queries or `jobs.search` filters.
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
`companySlug` (`jobs.list` query / `jobs.search` filters). The API accepts
|
|
50
|
-
slugs directly — never resolve slug→id client-side.
|
|
51
|
-
|
|
52
|
-
## Render the filter UI from the vocabulary
|
|
44
|
+
## Render controls from the vocabulary
|
|
53
45
|
|
|
54
46
|
```ts snippet
|
|
55
47
|
import {
|
|
56
|
-
REMOTE_OPTIONS,
|
|
57
48
|
EMPLOYMENT_TYPES,
|
|
58
|
-
SENIORITIES,
|
|
59
49
|
JOB_SORTS,
|
|
50
|
+
REMOTE_OPTIONS,
|
|
51
|
+
SENIORITIES,
|
|
60
52
|
seniorityLabels,
|
|
61
53
|
sortLabels,
|
|
62
54
|
} from '@cavuno/board/filters';
|
|
63
55
|
|
|
64
56
|
const { language } = await board.context();
|
|
65
|
-
const
|
|
66
|
-
const
|
|
57
|
+
const seniorityCopy = seniorityLabels(language);
|
|
58
|
+
const sortCopy = sortLabels(language);
|
|
67
59
|
```
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`
|
|
61
|
+
Render seniority as a multi-select with all eight `SENIORITIES`.
|
|
62
|
+
`EMPLOYMENT_TYPES` contains five listing options; `volunteer` and `other`
|
|
63
|
+
remain valid job wire values but are absent from the filter control.
|
|
64
|
+
`JOB_SORTS` contains exactly `relevance`, `newest`, and `salary_high`;
|
|
65
|
+
`relevance` is the featured-ranked default. Sort labels follow the board copy
|
|
66
|
+
catalog and operator overrides supplied to `sortLabels`.
|
|
73
67
|
|
|
74
|
-
## Load
|
|
68
|
+
## Load taxonomy options
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`canonicalSlug` for links.
|
|
70
|
+
Category and skill collections contain terms backed by published jobs. Each
|
|
71
|
+
term already carries a board-language `displayName`, immutable English
|
|
72
|
+
`sourceSlug` for filtering, and board-language `canonicalSlug` for links.
|
|
80
73
|
|
|
81
74
|
```ts snippet
|
|
82
75
|
const categories = await board.taxonomy.categories.list({ limit: 50 });
|
|
83
76
|
const skills = await board.taxonomy.skills.list({ limit: 50 });
|
|
84
|
-
const suggestions = await board.taxonomy.suggestions.list({
|
|
85
|
-
q: searchText,
|
|
86
|
-
limit: 10,
|
|
87
|
-
});
|
|
88
77
|
|
|
89
78
|
const categoryOptions = categories.data.map((term) => ({
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
label: term.displayName,
|
|
80
|
+
filterValue: term.sourceSlug,
|
|
81
|
+
href: `/jobs/${term.canonicalSlug}`,
|
|
93
82
|
}));
|
|
94
83
|
```
|
|
95
84
|
|
|
96
|
-
Category and skill lists accept `q`, `limit` (1–100), and
|
|
97
|
-
`
|
|
85
|
+
Category and skill lists accept `q`, `limit` (1–100), and opaque `cursor`.
|
|
86
|
+
Pass `nextCursor` unchanged to the next request.
|
|
98
87
|
|
|
99
88
|
```ts snippet
|
|
100
89
|
const first = await board.taxonomy.categories.list({ limit: 50 });
|
|
@@ -106,31 +95,31 @@ const second = first.nextCursor
|
|
|
106
95
|
: null;
|
|
107
96
|
```
|
|
108
97
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
## Anti-patterns
|
|
98
|
+
Keyword suggestions accept `q` and `limit`. A present query shorter than two
|
|
99
|
+
characters returns no results. Suggestions contain categories and skills only;
|
|
100
|
+
because both types may share a slug, key each option by
|
|
101
|
+
`type + canonicalSlug`.
|
|
115
102
|
|
|
116
|
-
```ts
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
board.jobs.list({ seniority: rawSearch.seniority });
|
|
103
|
+
```ts snippet
|
|
104
|
+
const suggestions = await board.taxonomy.suggestions.list({
|
|
105
|
+
q: searchText,
|
|
106
|
+
limit: 10,
|
|
107
|
+
});
|
|
122
108
|
```
|
|
123
109
|
|
|
124
|
-
|
|
110
|
+
The host router owns URL serialization and saved-filter persistence.
|
|
111
|
+
Locations come from `board.taxonomy.places` rather than a static filter export.
|
|
125
112
|
|
|
126
|
-
|
|
127
|
-
taxonomy vocabulary (locations come from `board.taxonomy.places`), no
|
|
128
|
-
saved-filter persistence.
|
|
113
|
+
## Completion gate
|
|
129
114
|
|
|
130
|
-
|
|
115
|
+
Finish only after every applicable check passes:
|
|
131
116
|
|
|
132
|
-
-
|
|
133
|
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
-
|
|
117
|
+
- `/jobs?seniority=Senior,%20lead&sort=oldest` selects senior and lead, falls
|
|
118
|
+
back to `DEFAULT_SORT`, and passes no invalid value to the SDK.
|
|
119
|
+
- The seniority control has all eight localized levels and supports multiple
|
|
120
|
+
selections.
|
|
121
|
+
- The employment control has five options; the sort control has exactly
|
|
122
|
+
relevance, newest, and salary high.
|
|
123
|
+
- Category and skill filtering sends `sourceSlug`, while links use
|
|
124
|
+
`canonicalSlug`.
|
|
125
|
+
- Every paged taxonomy request forwards the previous opaque `nextCursor`.
|
|
@@ -1,104 +1,99 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-format
|
|
3
|
-
description:
|
|
3
|
+
description: Board-language display formatting with @cavuno/board/format. Use for salary ranges, dates, job labels, location labels, card conversion, or salary metadata copy.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Display formatting
|
|
7
7
|
|
|
8
|
-
`@cavuno/board/format`
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
`@cavuno/board/format` matches the hosted board's display rules. Read the
|
|
9
|
+
board language once and pass it as the required first argument to every
|
|
10
|
+
label-producing helper.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Filter controls use `cavuno-board-filters`. Salary values remain in their wire
|
|
13
|
+
currency and retain their server-computed amounts.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
- Building salary-page metadata sentences (the lexicon frames).
|
|
16
|
-
|
|
17
|
-
## When not to use
|
|
18
|
-
|
|
19
|
-
- Filter vocabulary and URL parsing — `cavuno-board-filters`.
|
|
20
|
-
- Money math. Never compute or convert amounts client-side; format what the
|
|
21
|
-
wire provides.
|
|
22
|
-
|
|
23
|
-
## The board language is a required first argument
|
|
24
|
-
|
|
25
|
-
Every label-producing helper takes the board language — read it once from
|
|
26
|
-
context. There is no `en` default: a German-native board must never
|
|
27
|
-
silently render English.
|
|
15
|
+
## Format job and blog fields
|
|
28
16
|
|
|
29
17
|
```ts snippet
|
|
30
18
|
import {
|
|
31
|
-
formatSalaryRange,
|
|
32
|
-
formatPublishedRelativeDate,
|
|
33
|
-
formatDate,
|
|
34
19
|
fieldLabel,
|
|
20
|
+
formatDate,
|
|
21
|
+
formatPublishedRelativeDate,
|
|
22
|
+
formatSalaryRange,
|
|
35
23
|
} from '@cavuno/board/format';
|
|
36
24
|
|
|
37
25
|
const { language } = await board.context();
|
|
38
26
|
|
|
39
|
-
formatSalaryRange(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
formatSalaryRange(
|
|
28
|
+
language,
|
|
29
|
+
job.salaryMin,
|
|
30
|
+
job.salaryMax,
|
|
31
|
+
job.salaryTimeframe,
|
|
32
|
+
job.salaryCurrency);
|
|
33
|
+
formatPublishedRelativeDate(language, job.publishedAt);
|
|
34
|
+
formatDate(language, job.publishedAt);
|
|
35
|
+
fieldLabel(language, job.seniority);
|
|
44
36
|
```
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
`formatPublishedRelativeDate` produces the compact relative value used on job
|
|
39
|
+
cards and the job-detail header, such as `5d ago`. `formatDate` produces the
|
|
40
|
+
UTC-pinned medium date used in blog metadata and detail facts, such as
|
|
41
|
+
`Jun 24, 2026`. This format surface limits relative time to the compact
|
|
42
|
+
job-publication label.
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
Salary timeframe and seniority copy is localized for English and German;
|
|
45
|
+
other locales use English words with locale-correct number formatting.
|
|
46
|
+
Employment and remote labels remain English across board languages because
|
|
47
|
+
the hosted board currently has no translated vocabulary for them.
|
|
53
48
|
|
|
54
|
-
##
|
|
49
|
+
## Render locations and saved-job cards
|
|
55
50
|
|
|
56
|
-
Cards
|
|
57
|
-
the full
|
|
51
|
+
Cards already contain server-computed location labels. Full `PublicJob`
|
|
52
|
+
detail requires the full-job formatter. Convert a full saved job before
|
|
53
|
+
placing it in a card collection.
|
|
58
54
|
|
|
59
55
|
```ts snippet
|
|
60
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
cardLocationLabel,
|
|
58
|
+
fullJobToCard,
|
|
59
|
+
locationLabel,
|
|
60
|
+
} from '@cavuno/board/format';
|
|
61
61
|
|
|
62
|
-
cardLocationLabel(language, card);
|
|
63
|
-
locationLabel(language, job);
|
|
64
|
-
const
|
|
62
|
+
cardLocationLabel(language, card);
|
|
63
|
+
locationLabel(language, job);
|
|
64
|
+
const savedCard = fullJobToCard(language, job);
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
##
|
|
67
|
+
## Build salary metadata copy
|
|
68
68
|
|
|
69
|
-
`getSalaryLexicon
|
|
70
|
-
sentence frames the hosted salary pages use:
|
|
69
|
+
`getSalaryLexicon` exposes hosted salary terminology and sentence frames.
|
|
71
70
|
|
|
72
71
|
```ts snippet
|
|
73
72
|
import { getSalaryLexicon } from '@cavuno/board/format';
|
|
74
73
|
|
|
75
74
|
const lexicon = getSalaryLexicon(language);
|
|
76
|
-
lexicon.frames.entitySalariesTitle({
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
## Anti-patterns
|
|
82
|
-
|
|
83
|
-
```ts no-check
|
|
84
|
-
// NEVER hand-format money — symbol placement and words are locale rules:
|
|
85
|
-
`$${(job.salaryMin / 1000).toFixed(0)}k`; // wrong on de boards
|
|
86
|
-
// NEVER default the locale:
|
|
87
|
-
formatSalaryRange('en', ...); // hardcoded 'en' on a de board
|
|
88
|
-
// NEVER rebuild card location labels from scratch — the wire pre-computes them.
|
|
75
|
+
lexicon.frames.entitySalariesTitle({
|
|
76
|
+
entity: 'JavaScript',
|
|
77
|
+
range: '$70K – $90K',
|
|
78
|
+
});
|
|
79
|
+
lexicon.seniority.senior;
|
|
89
80
|
```
|
|
90
81
|
|
|
91
|
-
|
|
82
|
+
Use the helpers as the formatting boundary: pass the board language, wire
|
|
83
|
+
amounts, wire currency, and server location data in; render the returned text.
|
|
84
|
+
This keeps symbol placement, timeframe copy, number rules, and hosted labels
|
|
85
|
+
aligned. Sort-dropdown copy remains in `cavuno-board-filters`.
|
|
92
86
|
|
|
93
|
-
|
|
94
|
-
number-only formatter — the SDK formats exactly what the hosted board
|
|
95
|
-
formats. Sort-dropdown copy lives in `cavuno-board-filters`.
|
|
87
|
+
## Completion gate
|
|
96
88
|
|
|
97
|
-
|
|
89
|
+
Finish only after every applicable check passes:
|
|
98
90
|
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
91
|
+
- Every label-producing call receives `board.context().language`; no call site
|
|
92
|
+
substitutes a hardcoded locale.
|
|
93
|
+
- A German board renders `pro Jahr`, `ab`, and `Führungskraft` where the same
|
|
94
|
+
path renders `Yearly`, `From`, and `Executive` in English.
|
|
95
|
+
- English salary strings are byte-identical to the hosted board for the same
|
|
96
|
+
wire job.
|
|
97
|
+
- Cards use `cardLocationLabel`; full jobs use `locationLabel`; saved full jobs
|
|
98
|
+
are converted with `fullJobToCard` before card rendering.
|
|
99
|
+
- Formatting changes neither salary amounts nor currency.
|
|
@@ -1,81 +1,52 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-i18n
|
|
3
|
-
description:
|
|
3
|
+
description: One-seam board localization with @cavuno/board. Use when localizing UI chrome, adding locale routing, merging operator labels, or preserving board-language content.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# One-seam board localization
|
|
7
7
|
|
|
8
|
-
A Cavuno board has
|
|
9
|
-
Job titles, descriptions, and company text are that single language and
|
|
10
|
-
never translated by the frontend. What you localize is the **chrome** —
|
|
11
|
-
labels, headings, FAQ scaffolding, filter vocabulary — plus the entity
|
|
12
|
-
data the API already translates (taxonomy, places, salary names).
|
|
8
|
+
A Cavuno board has one content language: `board.context().language`. Jobs, companies, and blog content remain in that API-served language. Multi-locale frontends localize chrome—labels, headings, filters, and FAQ scaffolding—plus entity data the API already translates, such as taxonomy, places, and salary names.
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
- **Content** (jobs, companies, blog): board-language, API-served, single.
|
|
16
|
-
- **Chrome** (your authored UI strings): can be multi-locale, path-routed.
|
|
10
|
+
## Build the copy seam
|
|
17
11
|
|
|
18
|
-
|
|
12
|
+
All UI strings resolve through one application module. Layer copy in this order:
|
|
19
13
|
|
|
20
|
-
1.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
2. **`⊕ board.context().labels`** — the operator's per-board overrides,
|
|
24
|
-
API-served (e.g. `featuredLabel` → "Top Job").
|
|
25
|
-
3. **`⊕ generated code`** — per-board copy the AI builder ejects to native
|
|
26
|
-
code. Supersedes the catalog for that board.
|
|
27
|
-
|
|
28
|
-
Every surface resolves copy through **one seam module** (`src/copy.ts`),
|
|
29
|
-
never by calling the catalog inline:
|
|
14
|
+
1. `uiCopy(language)` provides the versioned `en`, `de`, and `fr` catalog, including functions for pluralized keys.
|
|
15
|
+
2. `board.context().labels` overlays operator-specific labels.
|
|
16
|
+
3. Generated application copy overlays both when the builder ejects native code.
|
|
30
17
|
|
|
31
18
|
```ts snippet
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
import {
|
|
20
|
+
uiCopy,
|
|
21
|
+
type BoardLabelOverrides,
|
|
22
|
+
} from '@cavuno/board/format';
|
|
23
|
+
|
|
24
|
+
export function boardCopy(
|
|
25
|
+
language: string | undefined,
|
|
26
|
+
labels?: BoardLabelOverrides) {
|
|
27
|
+
return uiCopy(language, labels);
|
|
35
28
|
}
|
|
36
29
|
```
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
today → Paraglide messages (below) → builder-generated code later. Components
|
|
40
|
-
never change; only the seam's backing source does.
|
|
31
|
+
Components import this seam rather than the catalog. That keeps the catalog-to-generated-code transition to one module.
|
|
41
32
|
|
|
42
|
-
## Add
|
|
33
|
+
## Add locales
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
**Paraglide JS** — compile-time (messages become tree-shakeable functions),
|
|
46
|
-
**no runtime provider** (matches the catalog's no-provider stance), with
|
|
47
|
-
built-in locale routing. The board's language is served unprefixed at `/`;
|
|
48
|
-
extra locales are prefixed (`/de/`, `/fr/`).
|
|
35
|
+
Use the installed framework and i18n guidance for routing, SSR locale detection, link rewriting, and compiled messages. Keep the board language unprefixed at `/` and prefix additional locales such as `/de/` or `/fr/`. Preserve these Cavuno invariants while doing so:
|
|
49
36
|
|
|
50
|
-
1.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
the source of truth. Emit `messages/{locale}.json` from
|
|
55
|
-
`uiCopy(locale)` so `de`/`fr` come from the reviewed SDK catalog, not a
|
|
56
|
-
second copy. (This generator is the `uiCopy → Paraglide` adapter.)
|
|
57
|
-
3. **Route.** Wire the router's `rewrite` with Paraglide's `deLocalizeUrl` /
|
|
58
|
-
`localizeUrl` so `/de/jobs` routes internally as `/jobs` and every `Link`
|
|
59
|
-
auto-prefixes the active locale. Default-locale-no-prefix: the board
|
|
60
|
-
language stays at `/`.
|
|
61
|
-
4. **SSR.** Add `paraglideMiddleware` in the server entry; set `<html lang>`
|
|
62
|
-
from `getLocale()` (it replaces the static `board.language` in the root).
|
|
63
|
-
5. **Seam.** Point `boardCopy` at the URL locale (`getLocale()`) instead of
|
|
64
|
-
only `board.language`, so `/de/` renders German chrome even on an English
|
|
65
|
-
board. This is the opt-in over the single-language model.
|
|
66
|
-
6. **SEO slice.** Per-locale `hreflang`, canonical, and sitemap entries are
|
|
67
|
-
part of completing multi-language — do them when you add real locales, not
|
|
68
|
-
in the foundation.
|
|
37
|
+
1. Generate locale messages from `uiCopy(locale)` so reviewed SDK copy remains the translation source. This step is complete when every supported locale's chrome keys are generated from the catalog.
|
|
38
|
+
2. Point the seam at the URL locale and merge the current board's label overrides. This step is complete when switching locale changes chrome while retaining operator labels.
|
|
39
|
+
3. Keep API content in `board.context().language`. This step is complete when locale switching leaves job, company, and blog fields unchanged.
|
|
40
|
+
4. Emit locale-aware `<html lang>`, canonical URLs, `hreflang`, and sitemap entries. This step is complete when each public locale route declares itself and all alternates consistently.
|
|
69
41
|
|
|
70
|
-
|
|
42
|
+
A compile-time i18n system can replace the seam's backing source. Keep the seam provider-free unless the chosen framework integration itself requires a provider.
|
|
71
43
|
|
|
72
|
-
|
|
73
|
-
listings is a data-model change, out of scope.
|
|
74
|
-
- **Do not hand-author translations** in the starter — generate messages from
|
|
75
|
-
`uiCopy` so there is one reviewed source.
|
|
76
|
-
- **Do not add a runtime i18n provider** (e.g. use-intl's context). Paraglide
|
|
77
|
-
is compile-time; keep the no-provider property.
|
|
78
|
-
- **Do not scatter catalog calls** through markup — everything imports the one
|
|
79
|
-
seam, so the catalog→code migration is a single-file change.
|
|
44
|
+
## Completion gate
|
|
80
45
|
|
|
81
|
-
|
|
46
|
+
- Every authored chrome string enters components through the copy seam.
|
|
47
|
+
- `uiCopy` plus `board.context().labels` resolves the expected operator override.
|
|
48
|
+
- Each supported locale renders translated chrome at its public URL.
|
|
49
|
+
- Switching locale preserves API-served job, company, and blog content.
|
|
50
|
+
- Server HTML uses the active locale in `<html lang>` with no hydration mismatch.
|
|
51
|
+
- Canonical, `hreflang`, and sitemap URLs agree for every locale route.
|
|
52
|
+
- Missing translation keys fail generation or tests rather than appearing as raw keys.
|