@cavuno/board 1.25.0 → 1.26.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.
Files changed (37) hide show
  1. package/README.md +88 -0
  2. package/dist/bin.mjs +67 -34
  3. package/dist/filters.d.mts +55 -0
  4. package/dist/filters.d.ts +55 -0
  5. package/dist/filters.js +193 -0
  6. package/dist/filters.mjs +170 -0
  7. package/dist/format.d.mts +240 -0
  8. package/dist/format.d.ts +240 -0
  9. package/dist/format.js +453 -0
  10. package/dist/format.mjs +430 -0
  11. package/dist/index.d.mts +79 -3838
  12. package/dist/index.d.ts +79 -3838
  13. package/dist/index.js +154 -7
  14. package/dist/index.mjs +151 -4
  15. package/dist/jobs-CM67_J6H.d.mts +3836 -0
  16. package/dist/jobs-CM67_J6H.d.ts +3836 -0
  17. package/dist/theme.d.mts +63 -0
  18. package/dist/theme.d.ts +63 -0
  19. package/dist/theme.js +149 -0
  20. package/dist/theme.mjs +128 -0
  21. package/package.json +37 -2
  22. package/skills/cavuno-board-account/SKILL.md +147 -0
  23. package/skills/cavuno-board-applications/SKILL.md +110 -0
  24. package/skills/cavuno-board-blog/SKILL.md +99 -0
  25. package/skills/cavuno-board-companies/SKILL.md +99 -0
  26. package/skills/cavuno-board-filters/SKILL.md +89 -0
  27. package/skills/cavuno-board-format/SKILL.md +104 -0
  28. package/skills/cavuno-board-job-alerts/SKILL.md +115 -0
  29. package/skills/cavuno-board-job-posting/SKILL.md +130 -0
  30. package/skills/cavuno-board-jobs/SKILL.md +15 -0
  31. package/skills/cavuno-board-messaging/SKILL.md +121 -0
  32. package/skills/cavuno-board-paywall/SKILL.md +108 -0
  33. package/skills/cavuno-board-salaries/SKILL.md +87 -0
  34. package/skills/cavuno-board-setup/SKILL.md +26 -2
  35. package/skills/cavuno-board-smoke-test/SKILL.md +84 -0
  36. package/skills/cavuno-board-theme/SKILL.md +69 -0
  37. package/skills/manifest.json +92 -1
@@ -0,0 +1,170 @@
1
+ // src/format/salary-lexicon.ts
2
+ var EN = {
3
+ timeframe: {
4
+ per_year: "Yearly",
5
+ per_month: "Monthly",
6
+ per_week: "Weekly",
7
+ per_day: "Daily",
8
+ per_hour: "Hourly"
9
+ },
10
+ rangePrefix: { from: "From", upTo: "Up to" },
11
+ seniority: {
12
+ entry_level: "Entry level",
13
+ associate: "Associate",
14
+ mid_level: "Mid-level",
15
+ senior: "Senior",
16
+ lead: "Lead",
17
+ principal: "Principal",
18
+ director: "Director",
19
+ executive: "Executive"
20
+ },
21
+ frames: {
22
+ entitySalariesTitle: ({ entity, range }) => `${entity} Salaries (${range}/yr)`,
23
+ salariesInPlaceTitle: ({ place, range }) => `Salaries in ${place} (${range}/yr)`,
24
+ salariesInPlaceTitleNoRange: ({ place }) => `Salaries in ${place}`,
25
+ entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Salaries in ${place} (${range}/yr)`,
26
+ companySalariesTitle: ({ company, range }) => `${company} Salaries${range ? ` (${range}/yr)` : ""}`,
27
+ companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Salaries${range ? ` (${range}/yr)` : ""}`,
28
+ professionalsEarn: ({ entity, range, count }) => `${entity} professionals earn ${range}/yr on average across ${count} job postings.`,
29
+ rolesPay: ({ entity, range, count }) => `${entity} roles pay ${range}/yr on average across ${count} job postings.`,
30
+ professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity} professionals in ${place} earn ${range}/yr on average across ${count} job postings.`,
31
+ seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} roles start at ${lowAmount}, ${highLabel} roles reach ${highAmount}.`,
32
+ seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} salaries start at ${lowAmount}, while ${highLabel} roles reach ${highAmount}.`,
33
+ compareAcrossTopPaying: ({ count }) => `Compare across ${count} top-paying ${count === 1 ? "company" : "companies"}.`,
34
+ compareAcrossTop: ({ count }) => `Compare across ${count} top ${count === 1 ? "company" : "companies"}.`,
35
+ boardWideAverage: ({ entity, range }) => `Board-wide ${entity} average: ${range}/yr.`,
36
+ averageSalaryInCity: ({ place, range, count }) => `Average salary in ${place} is ${range}/yr across ${count} jobs. Browse salary data by title and skill.`,
37
+ browseLocationsInRegion: ({ count, place }) => `Browse salary data across ${count} locations in ${place}. Compare salaries by title, skill, and company.`,
38
+ companyPays: ({ company, range, count }) => `${company} pays ${range}/yr on average across ${count} job postings.`,
39
+ breakdownAcrossCategories: ({ count }) => `Breakdown across ${count} job ${count === 1 ? "category" : "categories"}.`,
40
+ compareWithSimilar: ({ count }) => `Compare with ${count} similar ${count === 1 ? "company" : "companies"}.`,
41
+ salaryInfoFor: ({ company, board }) => `Salary information for ${company} on ${board}.`,
42
+ categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category} roles at ${company} pay ${range}/yr based on ${count} job postings.`,
43
+ categorySalaryDataFor: ({ category, company, board }) => `${category} salary data for ${company} on ${board}.`
44
+ }
45
+ };
46
+ var DE = {
47
+ timeframe: {
48
+ per_year: "pro Jahr",
49
+ per_month: "pro Monat",
50
+ per_week: "pro Woche",
51
+ per_day: "pro Tag",
52
+ per_hour: "pro Stunde"
53
+ },
54
+ rangePrefix: { from: "ab", upTo: "bis zu" },
55
+ seniority: {
56
+ entry_level: "Entry-Level",
57
+ associate: "Associate",
58
+ mid_level: "Mid-Level",
59
+ senior: "Senior",
60
+ lead: "Lead",
61
+ principal: "Principal",
62
+ director: "Director",
63
+ executive: "F\xFChrungskraft"
64
+ },
65
+ frames: {
66
+ entitySalariesTitle: ({ entity, range }) => `${entity} Geh\xE4lter (${range}/Jahr)`,
67
+ salariesInPlaceTitle: ({ place, range }) => `Geh\xE4lter in ${place} (${range}/Jahr)`,
68
+ salariesInPlaceTitleNoRange: ({ place }) => `Geh\xE4lter in ${place}`,
69
+ entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Geh\xE4lter in ${place} (${range}/Jahr)`,
70
+ companySalariesTitle: ({ company, range }) => `${company} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
71
+ companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
72
+ professionalsEarn: ({ entity, range, count }) => `${entity}-Fachkr\xE4fte verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
73
+ rolesPay: ({ entity, range, count }) => `${entity}-Positionen werden mit durchschnittlich ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
74
+ professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity}-Fachkr\xE4fte in ${place} verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
75
+ seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Positionen beginnen bei ${lowAmount}, ${highLabel}-Positionen erreichen ${highAmount}.`,
76
+ seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Geh\xE4lter beginnen bei ${lowAmount}, w\xE4hrend ${highLabel}-Positionen ${highAmount} erreichen.`,
77
+ compareAcrossTopPaying: ({ count }) => `Vergleichen Sie ${count} ${count === 1 ? "bestzahlendes" : "bestzahlende"} Unternehmen.`,
78
+ compareAcrossTop: ({ count }) => `Vergleichen Sie ${count} Top-Unternehmen.`,
79
+ boardWideAverage: ({ entity, range }) => `Gesamtdurchschnitt ${entity}: ${range}/Jahr.`,
80
+ averageSalaryInCity: ({ place, range, count }) => `Das Durchschnittsgehalt in ${place} betr\xE4gt ${range}/Jahr, basierend auf ${count} Stellen. Gehaltsdaten nach Titel und F\xE4higkeiten durchsuchen.`,
81
+ browseLocationsInRegion: ({ count, place }) => `Gehaltsdaten f\xFCr ${count} Standorte in ${place} durchsuchen. Geh\xE4lter nach Titel, F\xE4higkeiten und Unternehmen vergleichen.`,
82
+ companyPays: ({ company, range, count }) => `${company} zahlt durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
83
+ breakdownAcrossCategories: ({ count }) => `Aufschl\xFCsselung \xFCber ${count} Job-${count === 1 ? "Kategorie" : "Kategorien"}.`,
84
+ compareWithSimilar: ({ count }) => `Vergleichen Sie mit ${count} \xE4hnlichen Unternehmen.`,
85
+ salaryInfoFor: ({ company, board }) => `Gehaltsinformationen f\xFCr ${company} auf ${board}.`,
86
+ categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category}-Positionen bei ${company} werden mit ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
87
+ categorySalaryDataFor: ({ category, company, board }) => `${category}-Gehaltsdaten f\xFCr ${company} auf ${board}.`
88
+ }
89
+ };
90
+ var LEXICONS = { en: EN, de: DE };
91
+ function getSalaryLexicon(locale) {
92
+ return locale && LEXICONS[locale] || EN;
93
+ }
94
+
95
+ // src/filters/index.ts
96
+ var REMOTE_OPTIONS = [
97
+ "on_site",
98
+ "hybrid",
99
+ "remote"
100
+ ];
101
+ var EMPLOYMENT_TYPES = [
102
+ "full_time",
103
+ "part_time",
104
+ "contract",
105
+ "internship",
106
+ "temporary"
107
+ ];
108
+ var SENIORITIES = [
109
+ "entry_level",
110
+ "associate",
111
+ "mid_level",
112
+ "senior",
113
+ "lead",
114
+ "principal",
115
+ "director",
116
+ "executive"
117
+ ];
118
+ var JOB_SORTS = [
119
+ "relevance",
120
+ "newest",
121
+ "salary_high"
122
+ ];
123
+ var DEFAULT_SORT = "relevance";
124
+ function seniorityLabels(locale) {
125
+ return getSalaryLexicon(locale).seniority;
126
+ }
127
+ function sortLabels(_locale) {
128
+ return {
129
+ relevance: "AI-ranked",
130
+ newest: "Most recent",
131
+ salary_high: "Salary: high to low"
132
+ };
133
+ }
134
+ var SENIORITY_LOOKUP = new Set(SENIORITIES);
135
+ function parseSeniority(raw) {
136
+ const values = Array.isArray(raw) ? raw : typeof raw === "string" && raw ? raw.split(",") : [];
137
+ const result = [];
138
+ const seen = /* @__PURE__ */ new Set();
139
+ for (const value of values) {
140
+ if (typeof value !== "string") continue;
141
+ const normalized = value.trim().toLowerCase();
142
+ if (!normalized || seen.has(normalized)) continue;
143
+ if (!SENIORITY_LOOKUP.has(normalized)) continue;
144
+ seen.add(normalized);
145
+ result.push(normalized);
146
+ }
147
+ return result.length > 0 ? result : void 0;
148
+ }
149
+ function parseListingFilters(search) {
150
+ return {
151
+ q: typeof search.q === "string" && search.q ? search.q : void 0,
152
+ remoteOption: REMOTE_OPTIONS.includes(search.remoteOption) ? search.remoteOption : void 0,
153
+ employmentType: EMPLOYMENT_TYPES.includes(
154
+ search.employmentType
155
+ ) ? search.employmentType : void 0,
156
+ seniority: parseSeniority(search.seniority),
157
+ sort: JOB_SORTS.includes(search.sort) ? search.sort : void 0
158
+ };
159
+ }
160
+ export {
161
+ DEFAULT_SORT,
162
+ EMPLOYMENT_TYPES,
163
+ JOB_SORTS,
164
+ REMOTE_OPTIONS,
165
+ SENIORITIES,
166
+ parseListingFilters,
167
+ parseSeniority,
168
+ seniorityLabels,
169
+ sortLabels
170
+ };
@@ -0,0 +1,240 @@
1
+ import { P as PublicJob, m as PublicJobCard } from './jobs-CM67_J6H.mjs';
2
+
3
+ /**
4
+ * Date display helpers in the board language (required leading parameter,
5
+ * ADR-0057) — transcribed from the hosted board's `date-labels.ts` and
6
+ * golden-tested against it in-monorepo.
7
+ *
8
+ * Job cards and the job-detail header render the RELATIVE form
9
+ * (`formatPublishedRelativeDate` — "5d ago"); the absolute medium date
10
+ * (`formatDate`) is what hosted uses for blog metadata and detail facts.
11
+ */
12
+ /**
13
+ * Absolute medium date in the board language, UTC-pinned — e.g.
14
+ * "Jun 24, 2026" (`en`) / "24.06.2026"-style per locale. What hosted uses
15
+ * for blog metadata and detail facts; job CARDS use the relative form.
16
+ * Returns `null` for missing/unparseable input.
17
+ */
18
+ declare function formatDate(locale: string, value: string | number | null): string | null;
19
+ /**
20
+ * Relative published label — "5d ago" (en) / "vor 5 T." (de) — the form the
21
+ * hosted board renders on job cards and the job-detail header. Under one
22
+ * minute renders "now". `referenceNowMs` exists for deterministic tests.
23
+ */
24
+ declare function formatPublishedRelativeDate(locale: string, value: string | number | null, referenceNowMs?: number): string | null;
25
+ /**
26
+ * Month-year label, e.g. "Jun 2023" — transcribed from the hosted board's
27
+ * `formatMonthYear` (golden-tested in-monorepo). The `T00:00:00` suffix
28
+ * prevents timezone-related off-by-one-month issues on date-only strings.
29
+ */
30
+ declare function formatMonthYear(locale: string, dateStr: string): string;
31
+
32
+ /**
33
+ * Adapt a full `PublicJob` (e.g. an embedded saved job) to the slim
34
+ * `PublicJobCard` shape a list card renders — computing the card's
35
+ * pre-resolved `locationLabel`/`remoteLocationLabel` from the full job's
36
+ * fields. Board language is required because the computed location label is
37
+ * display text (ADR-0057).
38
+ */
39
+ declare function fullJobToCard(locale: string, job: PublicJob): PublicJobCard;
40
+
41
+ /**
42
+ * Display label for a job enum value (`remoteOption`, `employmentType`,
43
+ * `seniority`) in the board language. Unknown values pass through unchanged
44
+ * (enums grow additively within v1).
45
+ *
46
+ * @example
47
+ * fieldLabel('en', 'mid_level'); // "Mid-level"
48
+ * fieldLabel('de', 'executive'); // "Führungskraft"
49
+ * fieldLabel('de', 'full_time'); // "Full-time" (hosted parity: en-only vocab)
50
+ */
51
+ declare function fieldLabel(locale: string, value: string | null): string | null;
52
+
53
+ /**
54
+ * Location display labels for job cards and detail pages, in the board
55
+ * language (required leading parameter, ADR-0057). The wrapper words
56
+ * ("Remote", "(hybrid)", "Worldwide") follow the hosted board's current
57
+ * English-only rendering; place names arrive already board-language from the
58
+ * wire (the API serves canonical, localized labels).
59
+ */
60
+ /**
61
+ * Location label for the full `PublicJob` (detail pages, embedded saved
62
+ * jobs): first office location's display name, remote/hybrid wrapping per
63
+ * `remoteOption`.
64
+ */
65
+ declare function locationLabel(locale: string, job: PublicJob): string;
66
+ /**
67
+ * Location label for a list CARD. The card read-model pre-computes
68
+ * `locationLabel` and (for remote jobs) `remoteLocationLabel` server-side —
69
+ * the slim card carries no `officeLocations`/`remoteWorldwide` — so those
70
+ * wire fields are used directly, matching what the hosted card renders.
71
+ */
72
+ declare function cardLocationLabel(locale: string, job: Pick<PublicJobCard, 'remoteOption' | 'remoteLocationLabel' | 'locationLabel'>): string;
73
+
74
+ /**
75
+ * The closed, board-language salary LEXICON — the fixed salary words:
76
+ * timeframe labels + range prefixes, seniority labels, and metadata
77
+ * `<title>`/`<description>` sentence FRAMES, keyed by the board language
78
+ * (`board.context().language`), NOT by any visitor locale.
79
+ *
80
+ * Transcribed from the hosted board's `salary-lexicon.ts` (CAV-352 /
81
+ * ADR-0012) and golden-tested against it in-monorepo (ADR-0057): the two
82
+ * copies cannot drift silently. The `de` strings are native-reviewed.
83
+ * Unseeded locales fall back to the English source WORDS while number and
84
+ * currency formatting stays locale-correct.
85
+ */
86
+ /** Wire enum for `salaryTimeframe`. */
87
+ type SalaryTimeframeValue = 'per_year' | 'per_month' | 'per_week' | 'per_day' | 'per_hour';
88
+ type SeniorityKey = 'entry_level' | 'associate' | 'mid_level' | 'senior' | 'lead' | 'principal' | 'director' | 'executive';
89
+ /**
90
+ * Board-language builders for the salary-page metadata sentence frames. Each
91
+ * receives already-formatted parts (entity name, board-language `range`/amount
92
+ * strings, counts) and returns one localized sentence. The numbers are
93
+ * formatted upstream (`formatSalaryRange`); the FRAME is the words.
94
+ */
95
+ interface SalaryFrames {
96
+ entitySalariesTitle(a: {
97
+ entity: string;
98
+ range: string;
99
+ }): string;
100
+ salariesInPlaceTitle(a: {
101
+ place: string;
102
+ range: string;
103
+ }): string;
104
+ salariesInPlaceTitleNoRange(a: {
105
+ place: string;
106
+ }): string;
107
+ entitySalariesInPlaceTitle(a: {
108
+ entity: string;
109
+ place: string;
110
+ range: string;
111
+ }): string;
112
+ companySalariesTitle(a: {
113
+ company: string;
114
+ range: string | null;
115
+ }): string;
116
+ companyCategorySalariesTitle(a: {
117
+ company: string;
118
+ category: string;
119
+ range: string | null;
120
+ }): string;
121
+ professionalsEarn(a: {
122
+ entity: string;
123
+ range: string;
124
+ count: number;
125
+ }): string;
126
+ rolesPay(a: {
127
+ entity: string;
128
+ range: string;
129
+ count: number;
130
+ }): string;
131
+ professionalsInLocationEarn(a: {
132
+ entity: string;
133
+ place: string;
134
+ range: string;
135
+ count: number;
136
+ }): string;
137
+ seniorityRange(a: {
138
+ lowLabel: string;
139
+ lowAmount: string;
140
+ highLabel: string;
141
+ highAmount: string;
142
+ }): string;
143
+ seniorityRangeWhile(a: {
144
+ lowLabel: string;
145
+ lowAmount: string;
146
+ highLabel: string;
147
+ highAmount: string;
148
+ }): string;
149
+ compareAcrossTopPaying(a: {
150
+ count: number;
151
+ }): string;
152
+ compareAcrossTop(a: {
153
+ count: number;
154
+ }): string;
155
+ boardWideAverage(a: {
156
+ entity: string;
157
+ range: string;
158
+ }): string;
159
+ averageSalaryInCity(a: {
160
+ place: string;
161
+ range: string;
162
+ count: number;
163
+ }): string;
164
+ browseLocationsInRegion(a: {
165
+ count: number;
166
+ place: string;
167
+ }): string;
168
+ companyPays(a: {
169
+ company: string;
170
+ range: string;
171
+ count: number;
172
+ }): string;
173
+ breakdownAcrossCategories(a: {
174
+ count: number;
175
+ }): string;
176
+ compareWithSimilar(a: {
177
+ count: number;
178
+ }): string;
179
+ salaryInfoFor(a: {
180
+ company: string;
181
+ board: string;
182
+ }): string;
183
+ categoryRolesAtCompanyPay(a: {
184
+ category: string;
185
+ company: string;
186
+ range: string;
187
+ count: number;
188
+ }): string;
189
+ categorySalaryDataFor(a: {
190
+ category: string;
191
+ company: string;
192
+ board: string;
193
+ }): string;
194
+ }
195
+ interface SalaryLexicon {
196
+ /** Per-timeframe display word, e.g. `per_year` → "Yearly" / "pro Jahr". */
197
+ timeframe: Record<SalaryTimeframeValue, string>;
198
+ /** Open-ended range prefixes, e.g. "From" / "ab", "Up to" / "bis zu". */
199
+ rangePrefix: {
200
+ from: string;
201
+ upTo: string;
202
+ };
203
+ /** Seniority-level labels, e.g. `senior` → "Senior". */
204
+ seniority: Record<SeniorityKey, string>;
205
+ /** Metadata `<title>`/`<description>` sentence builders. */
206
+ frames: SalaryFrames;
207
+ }
208
+ /**
209
+ * Resolve the salary lexicon for a board language; unseeded locales fall back
210
+ * to the English source.
211
+ */
212
+ declare function getSalaryLexicon(locale: string | undefined): SalaryLexicon;
213
+
214
+ type SalaryTimeframeInput = SalaryTimeframeValue | string | null | undefined;
215
+ /** Owner-customized timeframe words (board settings) — from the board's SEO/labels config. */
216
+ interface SalaryTimeframeOverrides {
217
+ yearlyLabel?: string;
218
+ monthlyLabel?: string;
219
+ weeklyLabel?: string;
220
+ dailyLabel?: string;
221
+ hourlyLabel?: string;
222
+ }
223
+ /**
224
+ * Format a per-job salary range in the BOARD language
225
+ * (`board.context().language` — required, never defaulted; see ADR-0057).
226
+ * The fixed words (timeframe, "From"/"Up to") come from
227
+ * `getSalaryLexicon(language)` with precedence owner-override > lexicon >
228
+ * English source. Currency placement is gated: `en` keeps the legacy manual
229
+ * symbol-before form (byte-identical to the hosted board); non-`en` uses
230
+ * `Intl` `style:'currency'` so the symbol sits per locale.
231
+ *
232
+ * @example
233
+ * formatSalaryRange('en', 90000, 120000, 'per_year', 'USD');
234
+ * // "$90K – $120K Yearly"
235
+ * formatSalaryRange('de', 90000, null, 'per_year', 'EUR');
236
+ * // "ab 90.000 € pro Jahr"
237
+ */
238
+ declare function formatSalaryRange(language: string | undefined, min: number | null, max: number | null, timeframe: SalaryTimeframeInput, currency?: string | null, timeframeOverrides?: SalaryTimeframeOverrides): string | null;
239
+
240
+ export { type SalaryFrames, type SalaryLexicon, type SalaryTimeframeInput, type SalaryTimeframeOverrides, type SalaryTimeframeValue, type SeniorityKey, cardLocationLabel, fieldLabel, formatDate, formatMonthYear, formatPublishedRelativeDate, formatSalaryRange, fullJobToCard, getSalaryLexicon, locationLabel };
@@ -0,0 +1,240 @@
1
+ import { P as PublicJob, m as PublicJobCard } from './jobs-CM67_J6H.js';
2
+
3
+ /**
4
+ * Date display helpers in the board language (required leading parameter,
5
+ * ADR-0057) — transcribed from the hosted board's `date-labels.ts` and
6
+ * golden-tested against it in-monorepo.
7
+ *
8
+ * Job cards and the job-detail header render the RELATIVE form
9
+ * (`formatPublishedRelativeDate` — "5d ago"); the absolute medium date
10
+ * (`formatDate`) is what hosted uses for blog metadata and detail facts.
11
+ */
12
+ /**
13
+ * Absolute medium date in the board language, UTC-pinned — e.g.
14
+ * "Jun 24, 2026" (`en`) / "24.06.2026"-style per locale. What hosted uses
15
+ * for blog metadata and detail facts; job CARDS use the relative form.
16
+ * Returns `null` for missing/unparseable input.
17
+ */
18
+ declare function formatDate(locale: string, value: string | number | null): string | null;
19
+ /**
20
+ * Relative published label — "5d ago" (en) / "vor 5 T." (de) — the form the
21
+ * hosted board renders on job cards and the job-detail header. Under one
22
+ * minute renders "now". `referenceNowMs` exists for deterministic tests.
23
+ */
24
+ declare function formatPublishedRelativeDate(locale: string, value: string | number | null, referenceNowMs?: number): string | null;
25
+ /**
26
+ * Month-year label, e.g. "Jun 2023" — transcribed from the hosted board's
27
+ * `formatMonthYear` (golden-tested in-monorepo). The `T00:00:00` suffix
28
+ * prevents timezone-related off-by-one-month issues on date-only strings.
29
+ */
30
+ declare function formatMonthYear(locale: string, dateStr: string): string;
31
+
32
+ /**
33
+ * Adapt a full `PublicJob` (e.g. an embedded saved job) to the slim
34
+ * `PublicJobCard` shape a list card renders — computing the card's
35
+ * pre-resolved `locationLabel`/`remoteLocationLabel` from the full job's
36
+ * fields. Board language is required because the computed location label is
37
+ * display text (ADR-0057).
38
+ */
39
+ declare function fullJobToCard(locale: string, job: PublicJob): PublicJobCard;
40
+
41
+ /**
42
+ * Display label for a job enum value (`remoteOption`, `employmentType`,
43
+ * `seniority`) in the board language. Unknown values pass through unchanged
44
+ * (enums grow additively within v1).
45
+ *
46
+ * @example
47
+ * fieldLabel('en', 'mid_level'); // "Mid-level"
48
+ * fieldLabel('de', 'executive'); // "Führungskraft"
49
+ * fieldLabel('de', 'full_time'); // "Full-time" (hosted parity: en-only vocab)
50
+ */
51
+ declare function fieldLabel(locale: string, value: string | null): string | null;
52
+
53
+ /**
54
+ * Location display labels for job cards and detail pages, in the board
55
+ * language (required leading parameter, ADR-0057). The wrapper words
56
+ * ("Remote", "(hybrid)", "Worldwide") follow the hosted board's current
57
+ * English-only rendering; place names arrive already board-language from the
58
+ * wire (the API serves canonical, localized labels).
59
+ */
60
+ /**
61
+ * Location label for the full `PublicJob` (detail pages, embedded saved
62
+ * jobs): first office location's display name, remote/hybrid wrapping per
63
+ * `remoteOption`.
64
+ */
65
+ declare function locationLabel(locale: string, job: PublicJob): string;
66
+ /**
67
+ * Location label for a list CARD. The card read-model pre-computes
68
+ * `locationLabel` and (for remote jobs) `remoteLocationLabel` server-side —
69
+ * the slim card carries no `officeLocations`/`remoteWorldwide` — so those
70
+ * wire fields are used directly, matching what the hosted card renders.
71
+ */
72
+ declare function cardLocationLabel(locale: string, job: Pick<PublicJobCard, 'remoteOption' | 'remoteLocationLabel' | 'locationLabel'>): string;
73
+
74
+ /**
75
+ * The closed, board-language salary LEXICON — the fixed salary words:
76
+ * timeframe labels + range prefixes, seniority labels, and metadata
77
+ * `<title>`/`<description>` sentence FRAMES, keyed by the board language
78
+ * (`board.context().language`), NOT by any visitor locale.
79
+ *
80
+ * Transcribed from the hosted board's `salary-lexicon.ts` (CAV-352 /
81
+ * ADR-0012) and golden-tested against it in-monorepo (ADR-0057): the two
82
+ * copies cannot drift silently. The `de` strings are native-reviewed.
83
+ * Unseeded locales fall back to the English source WORDS while number and
84
+ * currency formatting stays locale-correct.
85
+ */
86
+ /** Wire enum for `salaryTimeframe`. */
87
+ type SalaryTimeframeValue = 'per_year' | 'per_month' | 'per_week' | 'per_day' | 'per_hour';
88
+ type SeniorityKey = 'entry_level' | 'associate' | 'mid_level' | 'senior' | 'lead' | 'principal' | 'director' | 'executive';
89
+ /**
90
+ * Board-language builders for the salary-page metadata sentence frames. Each
91
+ * receives already-formatted parts (entity name, board-language `range`/amount
92
+ * strings, counts) and returns one localized sentence. The numbers are
93
+ * formatted upstream (`formatSalaryRange`); the FRAME is the words.
94
+ */
95
+ interface SalaryFrames {
96
+ entitySalariesTitle(a: {
97
+ entity: string;
98
+ range: string;
99
+ }): string;
100
+ salariesInPlaceTitle(a: {
101
+ place: string;
102
+ range: string;
103
+ }): string;
104
+ salariesInPlaceTitleNoRange(a: {
105
+ place: string;
106
+ }): string;
107
+ entitySalariesInPlaceTitle(a: {
108
+ entity: string;
109
+ place: string;
110
+ range: string;
111
+ }): string;
112
+ companySalariesTitle(a: {
113
+ company: string;
114
+ range: string | null;
115
+ }): string;
116
+ companyCategorySalariesTitle(a: {
117
+ company: string;
118
+ category: string;
119
+ range: string | null;
120
+ }): string;
121
+ professionalsEarn(a: {
122
+ entity: string;
123
+ range: string;
124
+ count: number;
125
+ }): string;
126
+ rolesPay(a: {
127
+ entity: string;
128
+ range: string;
129
+ count: number;
130
+ }): string;
131
+ professionalsInLocationEarn(a: {
132
+ entity: string;
133
+ place: string;
134
+ range: string;
135
+ count: number;
136
+ }): string;
137
+ seniorityRange(a: {
138
+ lowLabel: string;
139
+ lowAmount: string;
140
+ highLabel: string;
141
+ highAmount: string;
142
+ }): string;
143
+ seniorityRangeWhile(a: {
144
+ lowLabel: string;
145
+ lowAmount: string;
146
+ highLabel: string;
147
+ highAmount: string;
148
+ }): string;
149
+ compareAcrossTopPaying(a: {
150
+ count: number;
151
+ }): string;
152
+ compareAcrossTop(a: {
153
+ count: number;
154
+ }): string;
155
+ boardWideAverage(a: {
156
+ entity: string;
157
+ range: string;
158
+ }): string;
159
+ averageSalaryInCity(a: {
160
+ place: string;
161
+ range: string;
162
+ count: number;
163
+ }): string;
164
+ browseLocationsInRegion(a: {
165
+ count: number;
166
+ place: string;
167
+ }): string;
168
+ companyPays(a: {
169
+ company: string;
170
+ range: string;
171
+ count: number;
172
+ }): string;
173
+ breakdownAcrossCategories(a: {
174
+ count: number;
175
+ }): string;
176
+ compareWithSimilar(a: {
177
+ count: number;
178
+ }): string;
179
+ salaryInfoFor(a: {
180
+ company: string;
181
+ board: string;
182
+ }): string;
183
+ categoryRolesAtCompanyPay(a: {
184
+ category: string;
185
+ company: string;
186
+ range: string;
187
+ count: number;
188
+ }): string;
189
+ categorySalaryDataFor(a: {
190
+ category: string;
191
+ company: string;
192
+ board: string;
193
+ }): string;
194
+ }
195
+ interface SalaryLexicon {
196
+ /** Per-timeframe display word, e.g. `per_year` → "Yearly" / "pro Jahr". */
197
+ timeframe: Record<SalaryTimeframeValue, string>;
198
+ /** Open-ended range prefixes, e.g. "From" / "ab", "Up to" / "bis zu". */
199
+ rangePrefix: {
200
+ from: string;
201
+ upTo: string;
202
+ };
203
+ /** Seniority-level labels, e.g. `senior` → "Senior". */
204
+ seniority: Record<SeniorityKey, string>;
205
+ /** Metadata `<title>`/`<description>` sentence builders. */
206
+ frames: SalaryFrames;
207
+ }
208
+ /**
209
+ * Resolve the salary lexicon for a board language; unseeded locales fall back
210
+ * to the English source.
211
+ */
212
+ declare function getSalaryLexicon(locale: string | undefined): SalaryLexicon;
213
+
214
+ type SalaryTimeframeInput = SalaryTimeframeValue | string | null | undefined;
215
+ /** Owner-customized timeframe words (board settings) — from the board's SEO/labels config. */
216
+ interface SalaryTimeframeOverrides {
217
+ yearlyLabel?: string;
218
+ monthlyLabel?: string;
219
+ weeklyLabel?: string;
220
+ dailyLabel?: string;
221
+ hourlyLabel?: string;
222
+ }
223
+ /**
224
+ * Format a per-job salary range in the BOARD language
225
+ * (`board.context().language` — required, never defaulted; see ADR-0057).
226
+ * The fixed words (timeframe, "From"/"Up to") come from
227
+ * `getSalaryLexicon(language)` with precedence owner-override > lexicon >
228
+ * English source. Currency placement is gated: `en` keeps the legacy manual
229
+ * symbol-before form (byte-identical to the hosted board); non-`en` uses
230
+ * `Intl` `style:'currency'` so the symbol sits per locale.
231
+ *
232
+ * @example
233
+ * formatSalaryRange('en', 90000, 120000, 'per_year', 'USD');
234
+ * // "$90K – $120K Yearly"
235
+ * formatSalaryRange('de', 90000, null, 'per_year', 'EUR');
236
+ * // "ab 90.000 € pro Jahr"
237
+ */
238
+ declare function formatSalaryRange(language: string | undefined, min: number | null, max: number | null, timeframe: SalaryTimeframeInput, currency?: string | null, timeframeOverrides?: SalaryTimeframeOverrides): string | null;
239
+
240
+ export { type SalaryFrames, type SalaryLexicon, type SalaryTimeframeInput, type SalaryTimeframeOverrides, type SalaryTimeframeValue, type SeniorityKey, cardLocationLabel, fieldLabel, formatDate, formatMonthYear, formatPublishedRelativeDate, formatSalaryRange, fullJobToCard, getSalaryLexicon, locationLabel };