@cavuno/board 3.1.0 → 4.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +421 -0
  2. package/README.md +6 -2
  3. package/dist/{_spec-nkWAraS0.d.mts → _spec-GVe6WAfT.d.mts} +68 -59
  4. package/dist/{_spec-nkWAraS0.d.ts → _spec-GVe6WAfT.d.ts} +68 -59
  5. package/dist/bin.mjs +7 -2
  6. package/dist/blog-BsmKizpd.d.ts +29 -0
  7. package/dist/blog-Cf64mI8A.d.mts +29 -0
  8. package/dist/{salaries-BahBpxWT.d.ts → board-DUhgiacm.d.ts} +17 -29
  9. package/dist/{salaries-ChSqDBAM.d.mts → board-GGWJ4coe.d.mts} +17 -29
  10. package/dist/breadcrumbs-DFKr0HzU.d.mts +372 -0
  11. package/dist/breadcrumbs-DiccJhk4.d.ts +372 -0
  12. package/dist/chunk-2ETP5UH4.mjs +271 -0
  13. package/dist/chunk-BGKNXG4H.mjs +146 -0
  14. package/dist/chunk-DLSO6XXY.mjs +33 -0
  15. package/dist/chunk-T2KC6XGV.mjs +1060 -0
  16. package/dist/chunk-YEAQ3JEI.mjs +853 -0
  17. package/dist/doctor.js +1 -1
  18. package/dist/doctor.mjs +1 -1
  19. package/dist/filters.d.mts +25 -20
  20. package/dist/filters.d.ts +25 -20
  21. package/dist/filters.js +3 -692
  22. package/dist/filters.mjs +3 -690
  23. package/dist/format.d.mts +57 -230
  24. package/dist/format.d.ts +57 -230
  25. package/dist/format.js +355 -927
  26. package/dist/format.mjs +213 -931
  27. package/dist/go.js +11 -1
  28. package/dist/go.mjs +4 -29
  29. package/dist/index.d.mts +23 -75
  30. package/dist/index.d.ts +23 -75
  31. package/dist/index.js +11 -36
  32. package/dist/index.mjs +32 -313
  33. package/dist/{jobs-lVvKCBCU.d.mts → jobs-BuLpV8iy.d.mts} +4 -4
  34. package/dist/{jobs-BiEvL9d7.d.ts → jobs-QrbXOYB9.d.ts} +4 -4
  35. package/dist/paths.d.mts +89 -1
  36. package/dist/paths.d.ts +89 -1
  37. package/dist/paths.js +62 -14
  38. package/dist/paths.mjs +29 -78
  39. package/dist/route-contract.js +23 -13
  40. package/dist/route-contract.mjs +30 -1079
  41. package/dist/{search-CCyFWPi3.d.mts → search-Cr4noJJZ.d.mts} +14 -3
  42. package/dist/{search-CyRvVMUp.d.ts → search-DdsniXQv.d.ts} +14 -3
  43. package/dist/seo.d.mts +33 -189
  44. package/dist/seo.d.ts +33 -189
  45. package/dist/seo.js +197 -718
  46. package/dist/seo.mjs +44 -1417
  47. package/dist/server.d.mts +5 -5
  48. package/dist/server.d.ts +5 -5
  49. package/dist/server.mjs +5 -31
  50. package/dist/sitemap.d.mts +5 -5
  51. package/dist/sitemap.d.ts +5 -5
  52. package/dist/sitemap.js +25 -14
  53. package/dist/sitemap.mjs +22 -97
  54. package/dist/skills.js +1 -1
  55. package/dist/skills.mjs +1 -1
  56. package/dist/suggest.d.mts +2 -2
  57. package/dist/suggest.d.ts +2 -2
  58. package/dist/suggest.js +17 -3
  59. package/dist/suggest.mjs +17 -3
  60. package/dist/well-known.js +23 -13
  61. package/dist/well-known.mjs +4 -421
  62. package/package.json +5 -4
  63. package/skills/cavuno-board-applications/SKILL.md +4 -1
  64. package/skills/cavuno-board-filters/SKILL.md +11 -12
  65. package/skills/cavuno-board-format/SKILL.md +68 -56
  66. package/skills/cavuno-board-i18n/SKILL.md +102 -21
  67. package/skills/cavuno-board-search-suggestions/SKILL.md +8 -3
  68. package/skills/cavuno-board-seo/BLOG.md +4 -1
  69. package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +5 -2
  70. package/skills/cavuno-board-seo/LISTING.md +35 -12
  71. package/skills/cavuno-board-seo/SALARY.md +83 -18
  72. package/skills/cavuno-board-seo/SKILL.md +3 -2
  73. package/skills/manifest.json +3 -3
  74. package/dist/board-NE98PlwN.d.ts +0 -16
  75. package/dist/board-muu8BI8P.d.mts +0 -16
  76. package/dist/ui-copy-CCkz6CVF.d.mts +0 -230
  77. package/dist/ui-copy-CCkz6CVF.d.ts +0 -230
package/dist/format.js CHANGED
@@ -21,72 +21,99 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var format_exports = {};
22
22
  __export(format_exports, {
23
23
  COUNTRY_CODES: () => COUNTRY_CODES,
24
- PUBLIC_LABEL_GROUPS: () => PUBLIC_LABEL_GROUPS,
25
- cardLocationLabel: () => cardLocationLabel,
26
- companyIntro: () => companyIntro,
24
+ buildJobBreadcrumbs: () => buildJobBreadcrumbs,
27
25
  countryOptions: () => countryOptions,
28
- fieldLabel: () => fieldLabel,
29
26
  formatDate: () => formatDate,
30
27
  formatMonthYear: () => formatMonthYear,
31
28
  formatPublishedRelativeDate: () => formatPublishedRelativeDate,
32
29
  formatSalaryRange: () => formatSalaryRange,
33
- fullJobToCard: () => fullJobToCard,
34
- getSalaryLexicon: () => getSalaryLexicon,
35
- locationLabel: () => locationLabel,
36
- resolveCustomFieldDisplay: () => resolveCustomFieldDisplay,
37
- uiCopy: () => uiCopy
30
+ formatSalaryStat: () => formatSalaryStat,
31
+ formatSalaryStatRange: () => formatSalaryStatRange,
32
+ normalizeWebsiteUrl: () => normalizeWebsiteUrl,
33
+ resolveCustomFieldDisplay: () => resolveCustomFieldDisplay
38
34
  });
39
35
  module.exports = __toCommonJS(format_exports);
40
36
 
37
+ // src/format/locale.ts
38
+ function normalizeLocale(language) {
39
+ const trimmed = language.trim();
40
+ if (!trimmed) return null;
41
+ try {
42
+ const candidate = trimmed.replace(/_/g, "-");
43
+ const [canonical] = Intl.getCanonicalLocales(candidate);
44
+ if (!canonical) return null;
45
+ const [supported] = Intl.NumberFormat.supportedLocalesOf([canonical]);
46
+ return supported ?? null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
41
52
  // src/format/dates.ts
42
- var rtfNarrowCache = /* @__PURE__ */ new Map();
53
+ var rtfShortCache = /* @__PURE__ */ new Map();
54
+ var rtfNowCache = /* @__PURE__ */ new Map();
43
55
  var dtfCache = /* @__PURE__ */ new Map();
44
- function getNarrowRelativeTimeFormat(language) {
45
- const key = language ?? "en";
46
- let rtf = rtfNarrowCache.get(key);
56
+ var monthYearCache = /* @__PURE__ */ new Map();
57
+ function getShortRelativeTimeFormat(language) {
58
+ let rtf = rtfShortCache.get(language);
47
59
  if (!rtf) {
48
- rtf = new Intl.RelativeTimeFormat(key, {
60
+ rtf = new Intl.RelativeTimeFormat(language, {
49
61
  numeric: "always",
50
- style: "narrow"
62
+ style: "short"
51
63
  });
52
- rtfNarrowCache.set(key, rtf);
64
+ rtfShortCache.set(language, rtf);
65
+ }
66
+ return rtf;
67
+ }
68
+ function getNowRelativeTimeFormat(language) {
69
+ let rtf = rtfNowCache.get(language);
70
+ if (!rtf) {
71
+ rtf = new Intl.RelativeTimeFormat(language, {
72
+ numeric: "auto",
73
+ style: "short"
74
+ });
75
+ rtfNowCache.set(language, rtf);
53
76
  }
54
77
  return rtf;
55
78
  }
56
79
  function getDateTimeFormat(language) {
57
- const key = language ?? "en";
58
- let dtf = dtfCache.get(key);
80
+ let dtf = dtfCache.get(language);
59
81
  if (!dtf) {
60
- dtf = new Intl.DateTimeFormat(key, {
82
+ dtf = new Intl.DateTimeFormat(language, {
61
83
  dateStyle: "medium",
62
84
  timeZone: "UTC"
63
85
  });
64
- dtfCache.set(key, dtf);
86
+ dtfCache.set(language, dtf);
87
+ }
88
+ return dtf;
89
+ }
90
+ function getMonthYearFormat(language) {
91
+ let dtf = monthYearCache.get(language);
92
+ if (!dtf) {
93
+ dtf = new Intl.DateTimeFormat(language, {
94
+ month: "short",
95
+ year: "numeric",
96
+ timeZone: "UTC"
97
+ });
98
+ monthYearCache.set(language, dtf);
65
99
  }
66
100
  return dtf;
67
101
  }
68
- function formatNarrow(value, unit, language) {
102
+ function formatShort(value, unit, language) {
69
103
  try {
70
- return getNarrowRelativeTimeFormat(language).format(value, unit);
104
+ return getShortRelativeTimeFormat(language).format(value, unit);
71
105
  } catch {
72
- const shortMap = {
73
- year: "y",
74
- month: "mo",
75
- week: "w",
76
- day: "d",
77
- hour: "h",
78
- minute: "m",
79
- second: "s"
80
- };
81
- return `${Math.abs(value)}${shortMap[unit] ?? unit}`;
106
+ return null;
82
107
  }
83
108
  }
84
109
  function formatDate(locale, value) {
85
110
  if (!value) return null;
86
111
  const parsed = new Date(value);
87
112
  if (Number.isNaN(parsed.getTime())) return null;
113
+ const normalized = normalizeLocale(locale);
114
+ if (!normalized) return null;
88
115
  try {
89
- return getDateTimeFormat(locale).format(parsed);
116
+ return getDateTimeFormat(normalized).format(parsed);
90
117
  } catch {
91
118
  return parsed.toISOString().split("T")[0];
92
119
  }
@@ -95,6 +122,8 @@ function formatPublishedRelativeDate(locale, value, referenceNowMs = Date.now())
95
122
  if (!value) return null;
96
123
  const parsed = new Date(value);
97
124
  if (Number.isNaN(parsed.getTime())) return null;
125
+ const normalized = normalizeLocale(locale);
126
+ if (!normalized) return null;
98
127
  const diff = referenceNowMs - parsed.getTime();
99
128
  const absDiff = Math.abs(diff);
100
129
  const minute = 60 * 1e3;
@@ -104,7 +133,11 @@ function formatPublishedRelativeDate(locale, value, referenceNowMs = Date.now())
104
133
  const month = 30 * day;
105
134
  const year = 365 * day;
106
135
  if (absDiff < minute) {
107
- return "now";
136
+ try {
137
+ return getNowRelativeTimeFormat(normalized).format(0, "second");
138
+ } catch {
139
+ return formatShort(0, "second", normalized);
140
+ }
108
141
  }
109
142
  const units = [
110
143
  { unit: "year", ms: year },
@@ -118,432 +151,292 @@ function formatPublishedRelativeDate(locale, value, referenceNowMs = Date.now())
118
151
  if (absDiff >= candidate.ms) {
119
152
  const valueInUnit = Math.round(absDiff / candidate.ms);
120
153
  const direction2 = diff >= 0 ? -valueInUnit : valueInUnit;
121
- return formatNarrow(direction2, candidate.unit, locale);
154
+ return formatShort(direction2, candidate.unit, normalized);
122
155
  }
123
156
  }
124
157
  const seconds = Math.round(absDiff / 1e3);
125
158
  const direction = diff >= 0 ? -seconds : seconds;
126
- return formatNarrow(direction, "second", locale);
159
+ return formatShort(direction, "second", normalized);
127
160
  }
128
161
  function formatMonthYear(locale, dateStr) {
129
- const date = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
130
- return date.toLocaleDateString(locale, {
131
- month: "short",
132
- year: "numeric"
133
- });
134
- }
135
-
136
- // src/format/salary-lexicon.ts
137
- var EN = {
138
- timeframe: {
139
- per_year: "Yearly",
140
- per_month: "Monthly",
141
- per_week: "Weekly",
142
- per_day: "Daily",
143
- per_hour: "Hourly"
144
- },
145
- rangePrefix: { from: "From", upTo: "Up to" },
146
- seniority: {
147
- entry_level: "Entry level",
148
- associate: "Associate",
149
- mid_level: "Mid-level",
150
- senior: "Senior",
151
- lead: "Lead",
152
- principal: "Principal",
153
- director: "Director",
154
- executive: "Executive"
155
- },
156
- frames: {
157
- entitySalariesTitle: ({ entity, range }) => `${entity} Salaries (${range}/yr)`,
158
- salariesInPlaceTitle: ({ place, range }) => `Salaries in ${place} (${range}/yr)`,
159
- salariesInPlaceTitleNoRange: ({ place }) => `Salaries in ${place}`,
160
- entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Salaries in ${place} (${range}/yr)`,
161
- companySalariesTitle: ({ company, range }) => `${company} Salaries${range ? ` (${range}/yr)` : ""}`,
162
- companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Salaries${range ? ` (${range}/yr)` : ""}`,
163
- professionalsEarn: ({ entity, range, count }) => `${entity} professionals earn ${range}/yr on average across ${count} job postings.`,
164
- rolesPay: ({ entity, range, count }) => `${entity} roles pay ${range}/yr on average across ${count} job postings.`,
165
- professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity} professionals in ${place} earn ${range}/yr on average across ${count} job postings.`,
166
- seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} roles start at ${lowAmount}, ${highLabel} roles reach ${highAmount}.`,
167
- seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} salaries start at ${lowAmount}, while ${highLabel} roles reach ${highAmount}.`,
168
- compareAcrossTopPaying: ({ count }) => `Compare across ${count} top-paying ${count === 1 ? "company" : "companies"}.`,
169
- compareAcrossTop: ({ count }) => `Compare across ${count} top ${count === 1 ? "company" : "companies"}.`,
170
- boardWideAverage: ({ entity, range }) => `Board-wide ${entity} average: ${range}/yr.`,
171
- averageSalaryInCity: ({ place, range, count }) => `Average salary in ${place} is ${range}/yr across ${count} jobs. Browse salary data by title and skill.`,
172
- browseLocationsInRegion: ({ count, place }) => `Browse salary data across ${count} locations in ${place}. Compare salaries by title, skill, and company.`,
173
- companyPays: ({ company, range, count }) => `${company} pays ${range}/yr on average across ${count} job postings.`,
174
- breakdownAcrossCategories: ({ count }) => `Breakdown across ${count} job ${count === 1 ? "category" : "categories"}.`,
175
- compareWithSimilar: ({ count }) => `Compare with ${count} similar ${count === 1 ? "company" : "companies"}.`,
176
- salaryInfoFor: ({ company, board }) => `Salary information for ${company} on ${board}.`,
177
- categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category} roles at ${company} pay ${range}/yr based on ${count} job postings.`,
178
- categorySalaryDataFor: ({ category, company, board }) => `${category} salary data for ${company} on ${board}.`
179
- }
180
- };
181
- var DE = {
182
- timeframe: {
183
- per_year: "pro Jahr",
184
- per_month: "pro Monat",
185
- per_week: "pro Woche",
186
- per_day: "pro Tag",
187
- per_hour: "pro Stunde"
188
- },
189
- rangePrefix: { from: "ab", upTo: "bis zu" },
190
- seniority: {
191
- entry_level: "Entry-Level",
192
- associate: "Associate",
193
- mid_level: "Mid-Level",
194
- senior: "Senior",
195
- lead: "Lead",
196
- principal: "Principal",
197
- director: "Director",
198
- executive: "F\xFChrungskraft"
199
- },
200
- frames: {
201
- entitySalariesTitle: ({ entity, range }) => `${entity} Geh\xE4lter (${range}/Jahr)`,
202
- salariesInPlaceTitle: ({ place, range }) => `Geh\xE4lter in ${place} (${range}/Jahr)`,
203
- salariesInPlaceTitleNoRange: ({ place }) => `Geh\xE4lter in ${place}`,
204
- entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Geh\xE4lter in ${place} (${range}/Jahr)`,
205
- companySalariesTitle: ({ company, range }) => `${company} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
206
- companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
207
- professionalsEarn: ({ entity, range, count }) => `${entity}-Fachkr\xE4fte verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
208
- rolesPay: ({ entity, range, count }) => `${entity}-Positionen werden mit durchschnittlich ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
209
- professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity}-Fachkr\xE4fte in ${place} verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
210
- seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Positionen beginnen bei ${lowAmount}, ${highLabel}-Positionen erreichen ${highAmount}.`,
211
- seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Geh\xE4lter beginnen bei ${lowAmount}, w\xE4hrend ${highLabel}-Positionen ${highAmount} erreichen.`,
212
- compareAcrossTopPaying: ({ count }) => `Vergleichen Sie ${count} ${count === 1 ? "bestzahlendes" : "bestzahlende"} Unternehmen.`,
213
- compareAcrossTop: ({ count }) => `Vergleichen Sie ${count} Top-Unternehmen.`,
214
- boardWideAverage: ({ entity, range }) => `Gesamtdurchschnitt ${entity}: ${range}/Jahr.`,
215
- averageSalaryInCity: ({ place, range, count }) => `Das Durchschnittsgehalt in ${place} betr\xE4gt ${range}/Jahr, basierend auf ${count} Stellen. Gehaltsdaten nach Titel und F\xE4higkeiten durchsuchen.`,
216
- browseLocationsInRegion: ({ count, place }) => `Gehaltsdaten f\xFCr ${count} Standorte in ${place} durchsuchen. Geh\xE4lter nach Titel, F\xE4higkeiten und Unternehmen vergleichen.`,
217
- companyPays: ({ company, range, count }) => `${company} zahlt durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
218
- breakdownAcrossCategories: ({ count }) => `Aufschl\xFCsselung \xFCber ${count} Job-${count === 1 ? "Kategorie" : "Kategorien"}.`,
219
- compareWithSimilar: ({ count }) => `Vergleichen Sie mit ${count} \xE4hnlichen Unternehmen.`,
220
- salaryInfoFor: ({ company, board }) => `Gehaltsinformationen f\xFCr ${company} auf ${board}.`,
221
- categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category}-Positionen bei ${company} werden mit ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
222
- categorySalaryDataFor: ({ category, company, board }) => `${category}-Gehaltsdaten f\xFCr ${company} auf ${board}.`
223
- }
224
- };
225
- var LEXICONS = { en: EN, de: DE };
226
- function getSalaryLexicon(locale) {
227
- return locale && LEXICONS[locale] || EN;
228
- }
229
-
230
- // src/format/labels.ts
231
- var EN_LABELS = {
232
- // remoteOption
233
- on_site: "On-site",
234
- hybrid: "Hybrid",
235
- remote: "Remote",
236
- // employmentType
237
- full_time: "Full-time",
238
- part_time: "Part-time",
239
- contract: "Contract",
240
- internship: "Internship",
241
- temporary: "Temporary",
242
- volunteer: "Volunteer",
243
- other: "Other"
244
- };
245
- var DE_LABELS = {
246
- on_site: "Vor Ort",
247
- hybrid: "Hybrid",
248
- remote: "Remote",
249
- full_time: "Vollzeit",
250
- part_time: "Teilzeit",
251
- contract: "Freiberuflich",
252
- internship: "Praktikum",
253
- temporary: "Befristet",
254
- volunteer: "Ehrenamtlich",
255
- other: "Sonstiges"
256
- };
257
- var FR_LABELS = {
258
- on_site: "Sur site",
259
- hybrid: "Hybride",
260
- remote: "T\xE9l\xE9travail",
261
- full_time: "Temps plein",
262
- part_time: "Temps partiel",
263
- contract: "Contrat",
264
- internship: "Stage",
265
- temporary: "Temporaire",
266
- volunteer: "B\xE9n\xE9volat",
267
- other: "Autre"
268
- };
269
- var VOCABULARIES = {
270
- en: EN_LABELS,
271
- de: DE_LABELS,
272
- fr: FR_LABELS
273
- };
274
- var OVERRIDE_KEYS = {
275
- on_site: "onSiteLabel",
276
- hybrid: "hybridLabel",
277
- remote: "remoteLabel",
278
- full_time: "fullTimeLabel",
279
- part_time: "partTimeLabel",
280
- contract: "contractLabel",
281
- internship: "internshipLabel",
282
- temporary: "temporaryLabel",
283
- volunteer: "volunteerLabel",
284
- entry_level: "seniorityEntryLevel",
285
- associate: "seniorityAssociate",
286
- mid_level: "seniorityMidLevel",
287
- senior: "senioritySenior",
288
- lead: "seniorityLead",
289
- principal: "seniorityPrincipal",
290
- director: "seniorityDirector",
291
- executive: "seniorityExecutive"
292
- };
293
- var SENIORITY_KEYS = /* @__PURE__ */ new Set([
294
- "entry_level",
295
- "associate",
296
- "mid_level",
297
- "senior",
298
- "lead",
299
- "principal",
300
- "director",
301
- "executive"
302
- ]);
303
- function storedOverride(value, labels) {
304
- const key = OVERRIDE_KEYS[value];
305
- const override = key ? labels?.jobCardLabels?.[key] : void 0;
306
- return typeof override === "string" && override.trim() !== "" ? override : null;
307
- }
308
- function fieldLabel(locale, value, labels) {
309
- if (!value) return null;
310
- const override = storedOverride(value, labels);
311
- if (override) return override;
312
- if (SENIORITY_KEYS.has(value)) {
313
- return getSalaryLexicon(locale).seniority[value];
314
- }
315
- const vocabulary = VOCABULARIES[locale] ?? EN_LABELS;
316
- return vocabulary[value] ?? EN_LABELS[value] ?? value;
317
- }
318
-
319
- // src/format/location.ts
320
- function locationLabel(locale, job) {
321
- const office = job.officeLocations[0];
322
- const place = office ? office.displayName ?? [office.city ?? office.locality, office.country].filter(Boolean).join(", ") : null;
323
- if (job.remoteOption === "remote") {
324
- return job.remoteWorldwide ? "Remote (worldwide)" : "Remote";
325
- }
326
- if (!place) return fieldLabel(locale, job.remoteOption) ?? "";
327
- return job.remoteOption === "hybrid" ? `${place} (hybrid)` : place;
328
- }
329
- function cardLocationLabel(locale, job) {
330
- if (job.remoteOption === "remote") {
331
- return job.remoteLocationLabel ? `Remote \xB7 ${job.remoteLocationLabel}` : "Remote";
162
+ if (!dateStr) return null;
163
+ const isDateOnly = /^\d{4}-\d{2}(-\d{2})?$/.test(dateStr);
164
+ const parsed = isDateOnly ? /* @__PURE__ */ new Date(
165
+ dateStr.length === 7 ? `${dateStr}-01T00:00:00.000Z` : `${dateStr}T00:00:00.000Z`
166
+ ) : new Date(dateStr);
167
+ if (Number.isNaN(parsed.getTime())) return null;
168
+ const normalized = normalizeLocale(locale);
169
+ if (!normalized) return null;
170
+ try {
171
+ return getMonthYearFormat(normalized).format(parsed);
172
+ } catch {
173
+ return null;
332
174
  }
333
- return job.locationLabel ?? fieldLabel(locale, job.remoteOption) ?? "";
334
- }
335
-
336
- // src/format/job-card.ts
337
- function fullJobToCard(locale, job) {
338
- return {
339
- id: job.id,
340
- object: "job_card",
341
- slug: job.slug ?? "",
342
- title: job.title,
343
- publishedAt: job.publishedAt,
344
- employmentType: job.employmentType,
345
- remoteOption: job.remoteOption,
346
- remoteLocationLabel: job.remoteWorldwide ? "Worldwide" : null,
347
- salaryMin: job.salaryMin,
348
- salaryMax: job.salaryMax,
349
- salaryCurrency: job.salaryCurrency,
350
- salaryTimeframe: job.salaryTimeframe,
351
- isFeatured: job.isFeatured,
352
- locationLabel: locationLabel(locale, job),
353
- company: job.company?.slug ? {
354
- slug: job.company.slug,
355
- name: job.company.name ?? "",
356
- logoUrl: job.company.logoUrl
357
- } : null,
358
- categories: job.categories,
359
- skills: job.skills,
360
- links: job.links
361
- };
362
175
  }
363
176
 
364
177
  // src/format/salary-range.ts
365
- var CURRENCY_SYMBOLS = {
366
- USD: "$",
367
- EUR: "\u20AC",
368
- GBP: "\xA3",
369
- JPY: "\xA5",
370
- CNY: "\xA5",
371
- KRW: "\u20A9",
372
- INR: "\u20B9",
373
- RUB: "\u20BD",
374
- BRL: "R$",
375
- CAD: "C$",
376
- AUD: "A$",
377
- CHF: "CHF ",
378
- SEK: "kr ",
379
- NOK: "kr ",
380
- DKK: "kr ",
381
- PLN: "z\u0142 ",
382
- MXN: "MX$",
383
- SGD: "S$",
384
- HKD: "HK$",
385
- NZD: "NZ$",
386
- ZAR: "R ",
387
- THB: "\u0E3F",
388
- PHP: "\u20B1",
389
- IDR: "Rp ",
390
- MYR: "RM ",
391
- VND: "\u20AB",
392
- AED: "AED ",
393
- SAR: "SAR ",
394
- ILS: "\u20AA",
395
- TRY: "\u20BA",
396
- CZK: "K\u010D ",
397
- HUF: "Ft ",
398
- RON: "lei ",
399
- BGN: "\u043B\u0432 ",
400
- HRK: "kn ",
401
- ISK: "kr ",
402
- CLP: "CLP$",
403
- COP: "COP$",
404
- PEN: "S/",
405
- ARS: "ARS$",
406
- TWD: "NT$",
407
- NGN: "\u20A6",
408
- EGP: "E\xA3",
409
- PKR: "\u20A8",
410
- BDT: "\u09F3",
411
- UAH: "\u20B4",
412
- KZT: "\u20B8"
413
- };
414
- function getCurrencySymbol(currency) {
415
- if (!currency) {
416
- return "$";
417
- }
418
- const normalized = currency.trim().toUpperCase();
419
- return CURRENCY_SYMBOLS[normalized] ?? `${normalized} `;
420
- }
421
- var compactFormatCache = /* @__PURE__ */ new Map();
422
- function getCompactFormatter(language) {
423
- const cached = compactFormatCache.get(language);
424
- if (cached) return cached;
425
- const formatter = new Intl.NumberFormat(language, {
426
- notation: "compact",
427
- compactDisplay: "short",
428
- maximumFractionDigits: 1
429
- });
430
- compactFormatCache.set(language, formatter);
431
- return formatter;
432
- }
433
- function formatCompactNumber(value, language) {
434
- try {
435
- return getCompactFormatter(language ?? "en").format(value);
436
- } catch {
437
- if (value >= 1e6) {
438
- const millions = value / 1e6;
439
- return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
440
- }
441
- if (value >= 1e3) {
442
- const thousands = value / 1e3;
443
- return thousands % 1 === 0 ? `${thousands}k` : `${thousands.toFixed(1)}k`;
178
+ var currencyCache = /* @__PURE__ */ new Map();
179
+ function formatAmount(value, language, currencyCode, currencyDisplay = "symbol", notation = "compact") {
180
+ const cacheKey = `${language}:${currencyCode}:${currencyDisplay}:${notation}`;
181
+ let formatter = currencyCache.get(cacheKey);
182
+ if (formatter === void 0) {
183
+ try {
184
+ const options = {
185
+ style: "currency",
186
+ currency: currencyCode,
187
+ currencyDisplay,
188
+ notation
189
+ };
190
+ if (notation === "compact") {
191
+ options.compactDisplay = "short";
192
+ options.maximumFractionDigits = 1;
193
+ options.minimumFractionDigits = 0;
194
+ }
195
+ formatter = new Intl.NumberFormat(language, options);
196
+ } catch {
197
+ formatter = null;
444
198
  }
445
- return value.toString();
199
+ currencyCache.set(cacheKey, formatter);
446
200
  }
201
+ return formatter ? formatter.format(value) : null;
447
202
  }
448
- var compactCurrencyCache = /* @__PURE__ */ new Map();
449
- function formatCompactCurrency(value, language, currency) {
450
- const currencyCode = (currency ?? "USD").trim().toUpperCase();
451
- const cacheKey = `${language}:${currencyCode}`;
452
- let formatter = compactCurrencyCache.get(cacheKey);
203
+ function formatRange(min, max, language, currencyCode, currencyDisplay = "symbol", notation = "compact") {
204
+ const cacheKey = `${language}:${currencyCode}:${currencyDisplay}:${notation}:range`;
205
+ let formatter = currencyCache.get(cacheKey);
453
206
  if (formatter === void 0) {
454
207
  try {
455
- formatter = new Intl.NumberFormat(language, {
208
+ const options = {
456
209
  style: "currency",
457
210
  currency: currencyCode,
458
- notation: "compact",
459
- compactDisplay: "short",
460
- maximumFractionDigits: 1
461
- });
211
+ currencyDisplay,
212
+ notation
213
+ };
214
+ if (notation === "compact") {
215
+ options.compactDisplay = "short";
216
+ options.maximumFractionDigits = 1;
217
+ options.minimumFractionDigits = 0;
218
+ }
219
+ formatter = new Intl.NumberFormat(language, options);
462
220
  } catch {
463
221
  formatter = null;
464
222
  }
465
- compactCurrencyCache.set(cacheKey, formatter);
223
+ currencyCache.set(cacheKey, formatter);
466
224
  }
467
- if (!formatter) {
468
- return `${getCurrencySymbol(currency)}${formatCompactNumber(value, language)}`;
225
+ if (!formatter) return null;
226
+ return formatter.formatRange(min, max);
227
+ }
228
+ var SALARY_TIMEFRAMES = /* @__PURE__ */ new Set([
229
+ "per_year",
230
+ "per_month",
231
+ "per_week",
232
+ "per_day",
233
+ "per_hour"
234
+ ]);
235
+ function notationForMagnitude(...values) {
236
+ let anyFinite = false;
237
+ for (const v of values) {
238
+ if (v == null || !Number.isFinite(v)) continue;
239
+ anyFinite = true;
240
+ if (Math.abs(v) >= 1e3) return "compact";
469
241
  }
470
- return formatter.format(value);
242
+ return anyFinite ? "standard" : "compact";
471
243
  }
472
- function appendTimeframe(value, timeframeLabel) {
473
- if (!value) {
244
+ function formatSalaryRange(language, min, max, timeframe, currency = null, currencyDisplay = "symbol", notation) {
245
+ const minOk = min == null || Number.isFinite(min);
246
+ const maxOk = max == null || Number.isFinite(max);
247
+ if (!minOk || !maxOk) {
474
248
  return null;
475
249
  }
476
- if (!timeframeLabel) {
477
- return value;
478
- }
479
- return `${value} ${timeframeLabel}`;
480
- }
481
- function formatSalaryRange(language, min, max, timeframe, currency = null, timeframeOverrides) {
482
250
  if (min == null && max == null) {
483
251
  return null;
484
252
  }
485
- const locale = language ?? "en";
486
- const lexicon = getSalaryLexicon(locale);
487
- const overrideMap = {
488
- per_year: timeframeOverrides?.yearlyLabel,
489
- per_month: timeframeOverrides?.monthlyLabel,
490
- per_week: timeframeOverrides?.weeklyLabel,
491
- per_day: timeframeOverrides?.dailyLabel,
492
- per_hour: timeframeOverrides?.hourlyLabel
493
- };
494
- const rawOverride = timeframe ? overrideMap[timeframe] : void 0;
495
- const enSourceWord = timeframe ? getSalaryLexicon("en").timeframe[timeframe] : void 0;
496
- const lexiconTimeframe = timeframe ? lexicon.timeframe[timeframe] ?? null : null;
497
- const timeframeLabel = rawOverride && rawOverride !== enSourceWord ? rawOverride : lexiconTimeframe;
498
- const formatAmount = locale === "en" ? (value) => `${getCurrencySymbol(currency)}${formatCompactNumber(value, locale)}` : (value) => formatCompactCurrency(value, locale, currency);
253
+ const currencyCode = currency?.trim().toUpperCase() || null;
254
+ if (!currencyCode) {
255
+ return null;
256
+ }
257
+ const locale = normalizeLocale(language);
258
+ if (!locale) {
259
+ return null;
260
+ }
261
+ const resolvedNotation = notation ?? notationForMagnitude(min, max);
262
+ const timeframeLabel = typeof timeframe === "string" && SALARY_TIMEFRAMES.has(timeframe) ? timeframe : null;
499
263
  if (min != null && max != null) {
500
- return appendTimeframe(
501
- `${formatAmount(min)} \u2013 ${formatAmount(max)}`,
502
- timeframeLabel
264
+ const low = min <= max ? min : max;
265
+ const high = min <= max ? max : min;
266
+ const text2 = low === high ? formatAmount(
267
+ low,
268
+ locale,
269
+ currencyCode,
270
+ currencyDisplay,
271
+ resolvedNotation
272
+ ) : formatRange(
273
+ low,
274
+ high,
275
+ locale,
276
+ currencyCode,
277
+ currencyDisplay,
278
+ resolvedNotation
503
279
  );
280
+ return text2 ? { text: text2, timeframe: timeframeLabel, bound: "range" } : null;
504
281
  }
505
282
  if (min != null) {
506
- return appendTimeframe(
507
- `${lexicon.rangePrefix.from} ${formatAmount(min)}`,
508
- timeframeLabel
283
+ const text2 = formatAmount(
284
+ min,
285
+ locale,
286
+ currencyCode,
287
+ currencyDisplay,
288
+ resolvedNotation
509
289
  );
290
+ return text2 ? { text: text2, timeframe: timeframeLabel, bound: "from" } : null;
510
291
  }
511
- return appendTimeframe(
512
- `${lexicon.rangePrefix.upTo} ${formatAmount(max)}`,
513
- timeframeLabel
292
+ const text = formatAmount(
293
+ max,
294
+ locale,
295
+ currencyCode,
296
+ currencyDisplay,
297
+ resolvedNotation
514
298
  );
299
+ return text ? { text, timeframe: timeframeLabel, bound: "upTo" } : null;
515
300
  }
516
301
 
517
- // src/format/company-intro.ts
518
- var NAMED_ENTITIES = {
519
- amp: "&",
520
- lt: "<",
521
- gt: ">",
522
- quot: '"',
523
- apos: "'",
524
- nbsp: " "
525
- };
526
- function decodeEntities(input) {
527
- return input.replace(/&(#\d+|#x[0-9a-f]+|[a-z]+);/gi, (match, body) => {
528
- const token = String(body);
529
- if (token[0] === "#") {
530
- const code = token[1] === "x" || token[1] === "X" ? parseInt(token.slice(2), 16) : parseInt(token.slice(1), 10);
531
- if (!Number.isInteger(code) || code < 0 || code > 1114111) return match;
532
- return String.fromCodePoint(code);
302
+ // src/seo/salary.ts
303
+ var currencyFormatterCache = /* @__PURE__ */ new Map();
304
+ function notationForMagnitude2(...values) {
305
+ let anyFinite = false;
306
+ for (const v of values) {
307
+ if (v == null || !Number.isFinite(v)) continue;
308
+ anyFinite = true;
309
+ if (Math.abs(v) >= 1e3) return "compact";
310
+ }
311
+ return anyFinite ? "standard" : "compact";
312
+ }
313
+ function getCurrencyFormatter(language, currencyCode, notation) {
314
+ const key = `${language}:${currencyCode}:${notation}`;
315
+ const cached = currencyFormatterCache.get(key);
316
+ if (cached !== void 0) return cached;
317
+ let formatter;
318
+ try {
319
+ const options = {
320
+ style: "currency",
321
+ currency: currencyCode,
322
+ notation
323
+ };
324
+ if (notation === "compact") {
325
+ options.compactDisplay = "short";
326
+ options.minimumFractionDigits = 0;
327
+ options.maximumFractionDigits = 1;
533
328
  }
534
- return NAMED_ENTITIES[token.toLowerCase()] ?? match;
535
- });
329
+ formatter = new Intl.NumberFormat(language, options);
330
+ } catch {
331
+ formatter = null;
332
+ }
333
+ currencyFormatterCache.set(key, formatter);
334
+ return formatter;
335
+ }
336
+ function formatSalaryStat(locale, value, currency, notation) {
337
+ if (!Number.isFinite(value)) return null;
338
+ const currencyCode = currency?.trim().toUpperCase() || null;
339
+ if (!currencyCode) return null;
340
+ const normalized = normalizeLocale(locale);
341
+ if (!normalized) return null;
342
+ const resolved = notation ?? notationForMagnitude2(value);
343
+ const formatter = getCurrencyFormatter(normalized, currencyCode, resolved);
344
+ if (!formatter) return null;
345
+ return formatter.format(value);
346
+ }
347
+ function formatSalaryStatRange(locale, min, max, currency, notation) {
348
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
349
+ const currencyCode = currency?.trim().toUpperCase() || null;
350
+ if (!currencyCode) return null;
351
+ const normalized = normalizeLocale(locale);
352
+ if (!normalized) return null;
353
+ const resolved = notation ?? notationForMagnitude2(min, max);
354
+ const formatter = getCurrencyFormatter(normalized, currencyCode, resolved);
355
+ if (!formatter) return null;
356
+ const low = Math.min(min, max);
357
+ const high = Math.max(min, max);
358
+ if (low === high) return formatter.format(low);
359
+ return formatter.formatRange(low, high);
536
360
  }
537
- function companyIntro(summary, description) {
538
- const trimmedSummary = summary?.trim();
539
- if (trimmedSummary) return trimmedSummary;
540
- if (!description) return null;
541
- const text = decodeEntities(
542
- description.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<br\s*\/?>/gi, " ").replace(/<\/(p|div|li|h[1-6])>/gi, " ").replace(/<[^>]*>/g, "")
543
- ).replace(/\s+/g, " ").trim();
544
- if (!text) return null;
545
- const [first] = text.split(/(?<=[.!?])\s+/);
546
- return first ?? text;
361
+
362
+ // src/seo/job-posting.ts
363
+ function normalizeWebsiteUrl(website) {
364
+ const trimmed = website.trim();
365
+ if (!trimmed) return null;
366
+ return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
367
+ }
368
+
369
+ // src/paths/index.ts
370
+ function encodePathSegment(segment) {
371
+ try {
372
+ return encodeURIComponent(decodeURIComponent(segment)).replace(
373
+ /%3A/gi,
374
+ ":"
375
+ );
376
+ } catch {
377
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
378
+ }
379
+ }
380
+ function jobsCategoryPath(categorySlug) {
381
+ return `/jobs/${encodePathSegment(categorySlug)}`;
382
+ }
383
+ function jobsLocationPath(placeSlug) {
384
+ return `/jobs/locations/${encodePathSegment(placeSlug)}`;
385
+ }
386
+ function jobsLocationCategoryPath(placeSlug, categorySlug) {
387
+ return `${jobsLocationPath(placeSlug)}/${encodePathSegment(categorySlug)}`;
388
+ }
389
+ var BOARD_PATHS = {
390
+ home: "/",
391
+ jobs: "/jobs",
392
+ companies: "/companies",
393
+ salaries: "/salaries",
394
+ salaryCompanies: "/salaries/companies",
395
+ salaryTitles: "/salaries/titles",
396
+ salarySkills: "/salaries/skills",
397
+ salaryLocations: "/salaries/locations",
398
+ blog: "/blog",
399
+ about: "/about",
400
+ privacyPolicy: "/privacy-policy",
401
+ termsOfService: "/terms-of-service",
402
+ cookiePolicy: "/cookie-policy",
403
+ impressum: "/impressum",
404
+ talent: "/talent",
405
+ employers: "/employers",
406
+ /**
407
+ * Alert-email surfaces. Promoted out of raw literals in
408
+ * the job-alerts composer so /go indirection and email composition share
409
+ * one definition.
410
+ */
411
+ alertsManage: "/alerts/manage",
412
+ alertsConfirm: "/alerts/confirm"
413
+ };
414
+
415
+ // src/seo/breadcrumbs.ts
416
+ function buildJobBreadcrumbs(job) {
417
+ const crumbs = [
418
+ { kind: "home", path: BOARD_PATHS.home },
419
+ { kind: "jobs", path: BOARD_PATHS.jobs }
420
+ ];
421
+ let lastPlaceSlug = null;
422
+ for (const place of job.placeHierarchy) {
423
+ lastPlaceSlug = place.slug;
424
+ crumbs.push({
425
+ kind: "place",
426
+ name: place.name,
427
+ path: jobsLocationPath(place.slug)
428
+ });
429
+ }
430
+ const primaryCategory = job.categories[0];
431
+ if (primaryCategory) {
432
+ crumbs.push({
433
+ kind: "category",
434
+ name: primaryCategory.name,
435
+ path: lastPlaceSlug ? jobsLocationCategoryPath(lastPlaceSlug, primaryCategory.slug) : jobsCategoryPath(primaryCategory.slug)
436
+ });
437
+ }
438
+ crumbs.push({ kind: "job", name: job.title });
439
+ return crumbs;
547
440
  }
548
441
 
549
442
  // src/format/countries.ts
@@ -813,8 +706,14 @@ function collator(language) {
813
706
  }
814
707
  }
815
708
  function countryOptions(language) {
816
- const names = regionNames(language);
817
- const compare = collator(language).compare;
709
+ const locale = normalizeLocale(language);
710
+ if (!locale) {
711
+ return COUNTRY_CODES.map((code) => ({ code, name: code })).sort(
712
+ (a, b) => a.code < b.code ? -1 : a.code > b.code ? 1 : 0
713
+ );
714
+ }
715
+ const names = regionNames(locale);
716
+ const compare = collator(locale).compare;
818
717
  return COUNTRY_CODES.map((code) => ({
819
718
  code,
820
719
  name: names?.of(code) ?? code
@@ -822,24 +721,27 @@ function countryOptions(language) {
822
721
  }
823
722
 
824
723
  // src/format/custom-fields.ts
724
+ function resolveMultiSelectLabels(def, raw) {
725
+ if (!Array.isArray(raw) || raw.length === 0) return null;
726
+ const labels = raw.map((key) => def.options?.find((o) => o.key === key)?.label).filter((label) => Boolean(label));
727
+ return labels.length > 0 ? labels : null;
728
+ }
825
729
  function renderTextValue(def, raw) {
826
730
  switch (def.type) {
827
731
  case "number":
828
- return typeof raw === "number" ? String(raw) : null;
732
+ return null;
733
+ case "multi_select":
734
+ return null;
829
735
  case "single_select": {
830
736
  if (typeof raw !== "string") return null;
831
737
  return def.options?.find((o) => o.key === raw)?.label ?? null;
832
738
  }
833
- case "multi_select": {
834
- if (!Array.isArray(raw) || raw.length === 0) return null;
835
- const labels = raw.map((key) => def.options?.find((o) => o.key === key)?.label).filter((label) => Boolean(label));
836
- return labels.length > 0 ? labels.join(", ") : null;
837
- }
838
739
  default:
839
740
  return typeof raw === "string" && raw.trim() !== "" ? raw : null;
840
741
  }
841
742
  }
842
- function resolveCustomFieldDisplay(definitions, values) {
743
+ function resolveCustomFieldDisplay(language, definitions, values) {
744
+ void language;
843
745
  const entries = [];
844
746
  for (const def of definitions ?? []) {
845
747
  const raw = values?.[def.key];
@@ -854,6 +756,29 @@ function resolveCustomFieldDisplay(definitions, values) {
854
756
  }
855
757
  continue;
856
758
  }
759
+ if (def.type === "number") {
760
+ if (typeof raw === "number" && Number.isFinite(raw)) {
761
+ entries.push({
762
+ key: def.key,
763
+ label: def.label,
764
+ kind: "number",
765
+ value: raw
766
+ });
767
+ }
768
+ continue;
769
+ }
770
+ if (def.type === "multi_select") {
771
+ const labels = resolveMultiSelectLabels(def, raw);
772
+ if (labels) {
773
+ entries.push({
774
+ key: def.key,
775
+ label: def.label,
776
+ kind: "multi_select",
777
+ values: labels
778
+ });
779
+ }
780
+ continue;
781
+ }
857
782
  const value = renderTextValue(def, raw);
858
783
  if (value !== null) {
859
784
  entries.push({ key: def.key, label: def.label, kind: "text", value });
@@ -861,500 +786,3 @@ function resolveCustomFieldDisplay(definitions, values) {
861
786
  }
862
787
  return entries;
863
788
  }
864
-
865
- // src/format/ui-copy.ts
866
- var PUBLIC_LABEL_GROUPS = [
867
- "jobCardLabels",
868
- "navLabels",
869
- "breadcrumbsLabels",
870
- "footerLabels",
871
- "entityLabels",
872
- "jobSearchLabels",
873
- "globalPaginationLabels",
874
- "blogSharedLabels",
875
- "salaryLabels"
876
- ];
877
- var EN2 = {
878
- jobCard: {
879
- featuredLabel: "Featured",
880
- aiRankedLabel: "AI-ranked",
881
- sortNewestLabel: "Most recent",
882
- sortSalaryHighLabel: "Salary: high to low"
883
- },
884
- jobSearch: {
885
- headingJobs: "Jobs",
886
- keywordLabel: "Keyword",
887
- keywordPlaceholder: "Search jobs\u2026",
888
- locationLabel: "Location",
889
- locationPlaceholder: "City or region",
890
- workplacePlaceholder: "Workplace",
891
- anyWorkplaceLabel: "Any workplace",
892
- typePlaceholder: "Type",
893
- anyTypeLabel: "Any type",
894
- sortPlaceholder: "Sort",
895
- loadMoreLabel: "Load more",
896
- noJobsMatchText: "No jobs match \u2014 try clearing a filter."
897
- },
898
- jobDetail: {
899
- categoriesHeading: "Categories",
900
- skillsHeading: "Skills",
901
- additionalDetailsHeading: "Additional details",
902
- customFieldYesLabel: "Yes",
903
- customFieldNoLabel: "No",
904
- locationsLabel: "Locations",
905
- workPermitsLabel: "Work permits",
906
- timezonesLabel: "Timezones",
907
- educationLabel: "Education",
908
- experienceLabel: "Experience",
909
- worldwideLabel: "Worldwide",
910
- noExperienceRequiredLabel: "No experience required",
911
- experienceYears: (years) => `${years}+ years`,
912
- posted: (published) => `Posted ${published}`,
913
- noDescriptionText: "No description provided.",
914
- viewCompanyProfileLabel: "View company profile",
915
- similarJobsHeading: "Similar jobs",
916
- breadcrumbAriaLabel: "Breadcrumb"
917
- },
918
- apply: {
919
- applyButtonText: "Apply for this job",
920
- applyingLabel: "Applying\u2026",
921
- applyOnEmployerSiteLabel: "Apply on employer site",
922
- signInToApplyLabel: "Sign in to apply",
923
- verifyEmailToApplyLabel: "Verify email to apply",
924
- appliedViewApplicationsLabel: "Applied \u2014 view applications",
925
- applicationSubmitError: "Something went wrong. Please try again."
926
- },
927
- alerts: {
928
- jobAlertTitle: "Get job alerts",
929
- jobAlertEmailPlaceholder: "you@example.com",
930
- jobAlertButtonText: "Get alerts",
931
- subscribingLabel: "Subscribing\u2026",
932
- jobAlertSuccessToast: "If this email isn't already subscribed, we've sent a confirmation link \u2014 check your inbox.",
933
- jobAlertErrorToast: "Something went wrong. Please try again.",
934
- sectionAriaLabel: "Job alerts",
935
- emailAriaLabel: "email",
936
- submitAriaLabel: "get job alerts"
937
- },
938
- copyLink: {
939
- copyLinkLabel: "Copy link",
940
- copiedLabel: "Copied",
941
- ariaLabel: "copy link"
942
- },
943
- salary: {
944
- comparisonHeadlineAverage: "Average salary",
945
- perYearSuffix: "/ year",
946
- comparisonPercentile25Label: "25th percentile",
947
- medianLabel: "Median",
948
- comparisonPercentile75Label: "75th percentile",
949
- basedOnLabel: "Based on",
950
- seniorityTableHeaderLevel: "Level",
951
- seniorityTableHeaderAvg: "Average",
952
- boardBaselineLabel: "Board baseline",
953
- seniorityTableHeaderDiff: "vs. board",
954
- faqHeading: "Frequently asked questions"
955
- },
956
- nav: {
957
- home: "Jobs",
958
- companies: "Companies",
959
- pricing: "Pricing",
960
- talent: "Talent",
961
- post: "Post a job",
962
- blog: "Blog"
963
- },
964
- footer: {
965
- forCandidatesHeading: "For Candidates",
966
- forCompaniesHeading: "For Companies",
967
- resourcesHeading: "Resources",
968
- aboutHeading: "About",
969
- websiteLabel: "Website",
970
- aboutLabel: "About",
971
- contactLabel: "Contact us",
972
- locationsLabel: "Locations",
973
- salariesLabel: "Salaries",
974
- sitemapLabel: "Sitemap",
975
- termsOfServiceLabel: "Terms of Service",
976
- privacyPolicyLabel: "Privacy Policy",
977
- cookiePolicyLabel: "Cookie Policy",
978
- impressumLabel: "Impressum",
979
- allRightsReservedText: "All rights reserved.",
980
- copyrightPrefix: "\xA9 {{year}} {{board_name}}.",
981
- poweredByText: "Powered by",
982
- defaultDescription: "Discover the latest roles from {{board_name}}."
983
- },
984
- breadcrumbs: {
985
- home: "Home",
986
- jobs: "Jobs",
987
- locations: "Locations",
988
- salaries: "Salaries",
989
- companies: "Companies",
990
- skills: "Skills",
991
- titles: "Titles",
992
- blog: "Blog",
993
- post: "Post a Job",
994
- pricing: "Pricing",
995
- about: "About",
996
- impressum: "Impressum",
997
- termsOfService: "Terms of Service",
998
- privacyPolicy: "Privacy Policy",
999
- cookiePolicy: "Cookie Policy",
1000
- talent: "Talent"
1001
- },
1002
- pagination: {
1003
- ariaLabel: "Pagination",
1004
- previousLabel: "Previous",
1005
- nextLabel: "Next"
1006
- },
1007
- blog: {
1008
- bylineLabel: "By",
1009
- readingTimeLabel: "min read",
1010
- searchLabel: "Search",
1011
- searchPlaceholder: "Search posts...",
1012
- clearButtonLabel: "Clear search",
1013
- tagFilterLabel: "Topics",
1014
- tagFilterAllLabel: "All",
1015
- emptyTitle: "No matching posts",
1016
- emptyDescription: "Try adjusting your search or clearing your filters.",
1017
- emptyResetLabel: "Clear filters"
1018
- },
1019
- entity: {
1020
- jobSingular: "job",
1021
- jobPlural: "jobs",
1022
- companySingular: "company",
1023
- companyPlural: "companies"
1024
- }
1025
- };
1026
- var DE2 = {
1027
- jobCard: {
1028
- featuredLabel: "Hervorgehoben",
1029
- aiRankedLabel: "KI-sortiert",
1030
- sortNewestLabel: "Neueste zuerst",
1031
- sortSalaryHighLabel: "Gehalt: absteigend"
1032
- },
1033
- jobSearch: {
1034
- headingJobs: "Jobs",
1035
- keywordLabel: "Stichwort",
1036
- keywordPlaceholder: "Jobtitel oder -beschreibungen suchen",
1037
- locationLabel: "Standort",
1038
- locationPlaceholder: "Stadt, Land oder Region",
1039
- workplacePlaceholder: "Arbeitsmodell",
1040
- anyWorkplaceLabel: "Alle Arbeitsmodelle",
1041
- typePlaceholder: "Anstellungsart",
1042
- anyTypeLabel: "Alle Anstellungsarten",
1043
- sortPlaceholder: "Sortieren",
1044
- loadMoreLabel: "Mehr laden",
1045
- noJobsMatchText: "Keine passenden Jobs \u2014 setze einen Filter zur\xFCck."
1046
- },
1047
- jobDetail: {
1048
- categoriesHeading: "Kategorien",
1049
- skillsHeading: "F\xE4higkeiten",
1050
- additionalDetailsHeading: "Weitere Details",
1051
- customFieldYesLabel: "Ja",
1052
- customFieldNoLabel: "Nein",
1053
- locationsLabel: "Standorte",
1054
- workPermitsLabel: "Arbeitserlaubnis",
1055
- timezonesLabel: "Zeitzonen",
1056
- educationLabel: "Ausbildung",
1057
- experienceLabel: "Berufserfahrung",
1058
- worldwideLabel: "Weltweit",
1059
- noExperienceRequiredLabel: "Keine Berufserfahrung erforderlich",
1060
- experienceYears: (years) => `${years}+ Jahre`,
1061
- posted: (published) => `Ver\xF6ffentlicht ${published}`,
1062
- noDescriptionText: "Keine Beschreibung vorhanden.",
1063
- viewCompanyProfileLabel: "Unternehmensprofil ansehen",
1064
- similarJobsHeading: "\xC4hnliche Jobs",
1065
- breadcrumbAriaLabel: "Seitenpfad"
1066
- },
1067
- apply: {
1068
- applyButtonText: "Jetzt bewerben",
1069
- applyingLabel: "Wird gesendet \u2026",
1070
- applyOnEmployerSiteLabel: "Beim Unternehmen bewerben",
1071
- signInToApplyLabel: "Zum Bewerben anmelden",
1072
- verifyEmailToApplyLabel: "E-Mail best\xE4tigen, um dich zu bewerben",
1073
- appliedViewApplicationsLabel: "Beworben \u2014 Bewerbungen ansehen",
1074
- applicationSubmitError: "Etwas ist schiefgelaufen. Bitte versuche es erneut."
1075
- },
1076
- alerts: {
1077
- jobAlertTitle: "Job-Benachrichtigungen erhalten",
1078
- jobAlertEmailPlaceholder: "name@beispiel.de",
1079
- jobAlertButtonText: "Abonnieren",
1080
- subscribingLabel: "Wird abonniert \u2026",
1081
- jobAlertSuccessToast: "Falls diese E-Mail noch nicht abonniert ist, haben wir einen Best\xE4tigungslink gesendet \u2014 pr\xFCfe deinen Posteingang.",
1082
- jobAlertErrorToast: "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
1083
- sectionAriaLabel: "Job-Benachrichtigungen",
1084
- emailAriaLabel: "E-Mail",
1085
- submitAriaLabel: "Job-Benachrichtigungen erhalten"
1086
- },
1087
- copyLink: {
1088
- copyLinkLabel: "Link kopieren",
1089
- copiedLabel: "Kopiert",
1090
- ariaLabel: "Link kopieren"
1091
- },
1092
- salary: {
1093
- comparisonHeadlineAverage: "Durchschnittsgehalt",
1094
- perYearSuffix: "/ Jahr",
1095
- comparisonPercentile25Label: "25. Perzentil",
1096
- medianLabel: "Median",
1097
- comparisonPercentile75Label: "75. Perzentil",
1098
- basedOnLabel: "Basierend auf",
1099
- seniorityTableHeaderLevel: "Erfahrungslevel",
1100
- seniorityTableHeaderAvg: "Durchschnitt",
1101
- boardBaselineLabel: "Board-Durchschnitt",
1102
- seniorityTableHeaderDiff: "vs. Board",
1103
- faqHeading: "H\xE4ufig gestellte Fragen"
1104
- },
1105
- nav: {
1106
- home: "Jobs",
1107
- companies: "Unternehmen",
1108
- pricing: "Preise",
1109
- talent: "Talente",
1110
- post: "Job ver\xF6ffentlichen",
1111
- blog: "Blog"
1112
- },
1113
- footer: {
1114
- forCandidatesHeading: "F\xFCr Kandidaten",
1115
- forCompaniesHeading: "F\xFCr Unternehmen",
1116
- resourcesHeading: "Ressourcen",
1117
- aboutHeading: "\xDCber uns",
1118
- websiteLabel: "Website",
1119
- aboutLabel: "\xDCber uns",
1120
- contactLabel: "Kontakt",
1121
- locationsLabel: "Standorte",
1122
- salariesLabel: "Geh\xE4lter",
1123
- sitemapLabel: "Sitemap",
1124
- termsOfServiceLabel: "Nutzungsbedingungen",
1125
- privacyPolicyLabel: "Datenschutzerkl\xE4rung",
1126
- cookiePolicyLabel: "Cookie-Richtlinie",
1127
- impressumLabel: "Impressum",
1128
- allRightsReservedText: "Alle Rechte vorbehalten.",
1129
- copyrightPrefix: "\xA9 {{year}} {{board_name}}.",
1130
- poweredByText: "Bereitgestellt von",
1131
- defaultDescription: "Entdecke die neuesten Stellen von {{board_name}}."
1132
- },
1133
- breadcrumbs: {
1134
- home: "Startseite",
1135
- jobs: "Jobs",
1136
- locations: "Standorte",
1137
- salaries: "Geh\xE4lter",
1138
- companies: "Unternehmen",
1139
- skills: "F\xE4higkeiten",
1140
- titles: "Berufsbezeichnungen",
1141
- blog: "Blog",
1142
- post: "Job ver\xF6ffentlichen",
1143
- pricing: "Preise",
1144
- about: "\xDCber uns",
1145
- impressum: "Impressum",
1146
- termsOfService: "Nutzungsbedingungen",
1147
- privacyPolicy: "Datenschutzerkl\xE4rung",
1148
- cookiePolicy: "Cookie-Richtlinie",
1149
- talent: "Talente"
1150
- },
1151
- pagination: {
1152
- ariaLabel: "Seitennummerierung",
1153
- previousLabel: "Zur\xFCck",
1154
- nextLabel: "Weiter"
1155
- },
1156
- blog: {
1157
- bylineLabel: "Von",
1158
- readingTimeLabel: "Minuten Lesezeit",
1159
- searchLabel: "Suche",
1160
- searchPlaceholder: "Artikel suchen...",
1161
- clearButtonLabel: "Suche leeren",
1162
- tagFilterLabel: "Themen",
1163
- tagFilterAllLabel: "Alle",
1164
- emptyTitle: "Keine passenden Artikel gefunden",
1165
- emptyDescription: "Versuche, deine Suche anzupassen oder die Filter zur\xFCckzusetzen.",
1166
- emptyResetLabel: "Filter zur\xFCcksetzen"
1167
- },
1168
- entity: {
1169
- jobSingular: "Job",
1170
- jobPlural: "Jobs",
1171
- companySingular: "Unternehmen",
1172
- companyPlural: "Unternehmen"
1173
- }
1174
- };
1175
- var FR = {
1176
- jobCard: {
1177
- featuredLabel: "\xC0 la une",
1178
- aiRankedLabel: "Classement IA",
1179
- sortNewestLabel: "Plus r\xE9centes",
1180
- sortSalaryHighLabel: "Salaire : d\xE9croissant"
1181
- },
1182
- jobSearch: {
1183
- headingJobs: "Offres d'emploi",
1184
- keywordLabel: "Mot-cl\xE9",
1185
- keywordPlaceholder: "Rechercher une offre\u2026",
1186
- locationLabel: "Lieu",
1187
- locationPlaceholder: "Ville ou r\xE9gion",
1188
- workplacePlaceholder: "Mode de travail",
1189
- anyWorkplaceLabel: "Tous les modes de travail",
1190
- typePlaceholder: "Type de contrat",
1191
- anyTypeLabel: "Tous les types",
1192
- sortPlaceholder: "Trier",
1193
- loadMoreLabel: "Voir plus",
1194
- noJobsMatchText: "Aucune offre ne correspond \u2014 essayez de retirer un filtre."
1195
- },
1196
- jobDetail: {
1197
- categoriesHeading: "Cat\xE9gories",
1198
- skillsHeading: "Comp\xE9tences",
1199
- additionalDetailsHeading: "Informations compl\xE9mentaires",
1200
- customFieldYesLabel: "Oui",
1201
- customFieldNoLabel: "Non",
1202
- locationsLabel: "Lieux",
1203
- workPermitsLabel: "Permis de travail",
1204
- timezonesLabel: "Fuseaux horaires",
1205
- educationLabel: "Formation",
1206
- experienceLabel: "Exp\xE9rience",
1207
- worldwideLabel: "Monde entier",
1208
- noExperienceRequiredLabel: "Aucune exp\xE9rience requise",
1209
- experienceYears: (years) => `${years}+ ans`,
1210
- posted: (published) => `Publi\xE9e ${published}`,
1211
- noDescriptionText: "Aucune description fournie.",
1212
- viewCompanyProfileLabel: "Voir le profil de l'entreprise",
1213
- similarJobsHeading: "Offres similaires",
1214
- breadcrumbAriaLabel: "Fil d'Ariane"
1215
- },
1216
- apply: {
1217
- applyButtonText: "Postuler \xE0 cette offre",
1218
- applyingLabel: "Envoi en cours\u2026",
1219
- applyOnEmployerSiteLabel: "Postuler sur le site de l'employeur",
1220
- signInToApplyLabel: "Connectez-vous pour postuler",
1221
- verifyEmailToApplyLabel: "V\xE9rifiez votre e-mail pour postuler",
1222
- appliedViewApplicationsLabel: "Candidature envoy\xE9e \u2014 voir mes candidatures",
1223
- applicationSubmitError: "Une erreur est survenue. Veuillez r\xE9essayer."
1224
- },
1225
- alerts: {
1226
- jobAlertTitle: "Recevoir des alertes emploi",
1227
- jobAlertEmailPlaceholder: "vous@exemple.fr",
1228
- jobAlertButtonText: "Activer les alertes",
1229
- subscribingLabel: "Abonnement en cours\u2026",
1230
- jobAlertSuccessToast: "Si cet e-mail n'est pas d\xE9j\xE0 abonn\xE9, nous avons envoy\xE9 un lien de confirmation \u2014 v\xE9rifiez votre bo\xEEte de r\xE9ception.",
1231
- jobAlertErrorToast: "Une erreur est survenue. Veuillez r\xE9essayer.",
1232
- sectionAriaLabel: "Alertes emploi",
1233
- emailAriaLabel: "e-mail",
1234
- submitAriaLabel: "recevoir des alertes emploi"
1235
- },
1236
- copyLink: {
1237
- copyLinkLabel: "Copier le lien",
1238
- copiedLabel: "Copi\xE9",
1239
- ariaLabel: "copier le lien"
1240
- },
1241
- salary: {
1242
- comparisonHeadlineAverage: "Salaire moyen",
1243
- perYearSuffix: "/ an",
1244
- comparisonPercentile25Label: "25e percentile",
1245
- medianLabel: "M\xE9diane",
1246
- comparisonPercentile75Label: "75e percentile",
1247
- basedOnLabel: "Bas\xE9 sur",
1248
- seniorityTableHeaderLevel: "Niveau",
1249
- seniorityTableHeaderAvg: "Moyenne",
1250
- boardBaselineLabel: "R\xE9f\xE9rence du site",
1251
- seniorityTableHeaderDiff: "vs site",
1252
- faqHeading: "Questions fr\xE9quentes"
1253
- },
1254
- nav: {
1255
- home: "Offres",
1256
- companies: "Entreprises",
1257
- pricing: "Tarifs",
1258
- talent: "Talents",
1259
- post: "Publier une offre",
1260
- blog: "Blog"
1261
- },
1262
- footer: {
1263
- forCandidatesHeading: "Pour les candidats",
1264
- forCompaniesHeading: "Pour les entreprises",
1265
- resourcesHeading: "Ressources",
1266
- aboutHeading: "\xC0 propos",
1267
- websiteLabel: "Site web",
1268
- aboutLabel: "\xC0 propos",
1269
- contactLabel: "Nous contacter",
1270
- locationsLabel: "Lieux",
1271
- salariesLabel: "Salaires",
1272
- sitemapLabel: "Plan du site",
1273
- termsOfServiceLabel: "Conditions d'utilisation",
1274
- privacyPolicyLabel: "Politique de confidentialit\xE9",
1275
- cookiePolicyLabel: "Politique de cookies",
1276
- impressumLabel: "Mentions l\xE9gales",
1277
- allRightsReservedText: "Tous droits r\xE9serv\xE9s.",
1278
- copyrightPrefix: "\xA9 {{year}} {{board_name}}.",
1279
- poweredByText: "Propuls\xE9 par",
1280
- defaultDescription: "D\xE9couvrez les derni\xE8res offres de {{board_name}}."
1281
- },
1282
- breadcrumbs: {
1283
- home: "Accueil",
1284
- jobs: "Offres",
1285
- locations: "Lieux",
1286
- salaries: "Salaires",
1287
- companies: "Entreprises",
1288
- skills: "Comp\xE9tences",
1289
- titles: "Intitul\xE9s de poste",
1290
- blog: "Blog",
1291
- post: "Publier une offre",
1292
- pricing: "Tarifs",
1293
- about: "\xC0 propos",
1294
- impressum: "Mentions l\xE9gales",
1295
- termsOfService: "Conditions d'utilisation",
1296
- privacyPolicy: "Politique de confidentialit\xE9",
1297
- cookiePolicy: "Politique de cookies",
1298
- talent: "Talents"
1299
- },
1300
- pagination: {
1301
- ariaLabel: "Pagination",
1302
- previousLabel: "Pr\xE9c\xE9dent",
1303
- nextLabel: "Suivant"
1304
- },
1305
- blog: {
1306
- bylineLabel: "Par",
1307
- readingTimeLabel: "min de lecture",
1308
- searchLabel: "Recherche",
1309
- searchPlaceholder: "Rechercher un article...",
1310
- clearButtonLabel: "Effacer la recherche",
1311
- tagFilterLabel: "Th\xE8mes",
1312
- tagFilterAllLabel: "Tous",
1313
- emptyTitle: "Aucun article correspondant",
1314
- emptyDescription: "Essayez de modifier votre recherche ou de r\xE9initialiser les filtres.",
1315
- emptyResetLabel: "R\xE9initialiser les filtres"
1316
- },
1317
- entity: {
1318
- jobSingular: "offre",
1319
- jobPlural: "offres",
1320
- companySingular: "entreprise",
1321
- companyPlural: "entreprises"
1322
- }
1323
- };
1324
- var CATALOGS = { en: EN2, de: DE2, fr: FR };
1325
- function mergeGroup(group, overrides) {
1326
- if (!overrides) return group;
1327
- const out = { ...group };
1328
- for (const [key, value] of Object.entries(group)) {
1329
- if (typeof value !== "string") continue;
1330
- const override = overrides[key];
1331
- if (typeof override === "string" && override.trim() !== "") {
1332
- out[key] = override;
1333
- }
1334
- }
1335
- return out;
1336
- }
1337
- var GROUP_OVERRIDE_SOURCE = {
1338
- jobCard: "jobCardLabels",
1339
- jobSearch: "jobSearchLabels",
1340
- jobDetail: "jobCardLabels",
1341
- apply: "jobCardLabels",
1342
- alerts: "jobCardLabels",
1343
- copyLink: "jobCardLabels",
1344
- salary: "salaryLabels",
1345
- nav: "navLabels",
1346
- footer: "footerLabels",
1347
- breadcrumbs: "breadcrumbsLabels",
1348
- pagination: "globalPaginationLabels",
1349
- blog: "blogSharedLabels",
1350
- entity: "entityLabels"
1351
- };
1352
- function uiCopy(language, labels) {
1353
- const catalog = language && CATALOGS[language] || EN2;
1354
- if (!labels) return catalog;
1355
- const resolved = {};
1356
- for (const [group, source] of Object.entries(GROUP_OVERRIDE_SOURCE)) {
1357
- resolved[group] = mergeGroup(catalog[group], labels[source]);
1358
- }
1359
- return resolved;
1360
- }