Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/api/src/pdf-v2/templates/classic-ats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,9 @@ export const ClassicAtsFactory: ReactPdfTemplateFactory = {
return function ClassicAtsResume({ data, meta }: ReactPdfResumeProps): ReactElement {
const accent = meta.accentColor || ACCENT_FALLBACK;
const styles = buildStyles(rp, accent);
const lang = meta.language || data.language || 'en';
// Prefer the explicit export-request language (data.language) over the
// DB template row's language (meta.language). See issue #536.
const lang = data.language || meta.language || 'en';
const contactParts = buildResumeContactParts(data);

return createElement(
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/pdf-v2/templates/elegant-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,9 @@ export const ElegantSidebarFactory: ReactPdfTemplateFactory = {
return function ElegantSidebarResume({ data, meta }: ReactPdfResumeProps): ReactElement {
const palette = deriveElegantSidebarPalette(meta.accentColor);
const styles = buildResumeStyles(rp, palette);
const lang = meta.language || data.language || 'en';
// Prefer the explicit export-request language (data.language) over the
// DB template row's language (meta.language). See issue #536.
const lang = data.language || meta.language || 'en';
const contactParts = buildResumeContactParts(data);

// Render contact items with optional Links
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/pdf-v2/templates/harvard-classic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,13 @@ export const HarvardClassicFactory: ReactPdfTemplateFactory = {

return function HarvardClassicResume({ data, meta }: ReactPdfResumeProps): ReactElement {
const styles = buildStyles(rp);
const lang = meta.language || data.language || 'en';
// Prefer the explicit export-request language (data.language) over the
// DB template row's language (meta.language). The DB row may be the
// English fallback when no DE/FR/ES/IT variant has been seeded for
// this design, in which case meta.language is 'en' but the user has
// explicitly asked to export in another language — and the LLM body
// is already in that language. See issue #536.
const lang = data.language || meta.language || 'en';
const contactParts = buildResumeContactParts(data);

return createElement(
Expand Down
95 changes: 95 additions & 0 deletions apps/api/src/pdf-v2/templates/language-precedence.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, it, expect } from 'vitest';
import * as ReactPdf from '@react-pdf/renderer';
import { PDFParse } from 'pdf-parse';
import { HarvardClassicFactory } from './harvard-classic';
import { ClassicAtsFactory } from './classic-ats';
import { ElegantSidebarFactory } from './elegant-sidebar';
import type { ResumeTemplateData } from '../template-data';
import type { ReactPdfTemplateMeta } from '../types';
import { createElement } from 'react';

/**
* Regression test for issue #536.
*
* The DB template row for these designs only ships in English
* (`seed-multilingual-templates.ts` seeds one variant per design). When a
* user picks "Harvard Classic + German" and exports, the export pipeline
* resolves the template id to that English DB row, so `meta.language` is
* `'en'`. The user's choice is carried by `data.language` (set by the
* application processor from the export request).
*
* Before the fix, the resume templates picked `meta.language || data.language`,
* so DB-meta won and section headings rendered in English even though the
* LLM body was German. After the fix, `data.language` wins.
*
* We assert against the rendered PDF text via `pdf-parse` rather than the
* React element tree so a future regression that bypasses `tLabel` (e.g.
* hard-coding "Education") still trips the test. We call the factories
* directly to bypass the `react-pdf-loader.ts` dynamic-import shim, which
* doesn't work under Vitest's module transform.
*/
describe('react-pdf templates — language precedence (regression #536)', () => {
const FACTORIES = [
['harvard-classic', HarvardClassicFactory],
['classic-ats', ClassicAtsFactory],
['elegant-sidebar', ElegantSidebarFactory],
] as const;

// German labels from pdf-v2/i18n.ts that prove the German render path won.
const GERMAN_HEADINGS = ['Berufserfahrung', 'Ausbildung', 'Fähigkeiten'];
// English equivalents that must NOT appear when language='de'.
const ENGLISH_HEADINGS = ['Professional Experience', 'Education', 'Skills'];

const sampleData: ResumeTemplateData = {
candidateName: 'Anna Beispiel',
email: 'anna@example.com',
phone: '+49 30 123456',
summary: 'Erfahrene Fachkraft mit nachgewiesenen Erfolgen.',
skillCategories: [{ type: 'Core', skills: ['Projektmanagement', 'Kommunikation'] }],
experiences: [
{
title: 'Projektleiterin',
company: 'ACME GmbH',
location: 'Berlin',
dateRange: 'Jan 2022 – Heute',
achievements: ['Leitete ein Team von 5 Personen'],
},
],
education: [{ degree: 'M.Sc. Informatik', institution: 'TU Berlin', year: '2020' }],
language: 'de', // ← the user's export-time choice
};

// Simulate the missing-DE-variant fallback: DB-meta says English.
const englishMeta: ReactPdfTemplateMeta = {
language: 'en',
accentColor: undefined,
colorVariantName: undefined,
atsOptimized: false,
};

for (const [name, factory] of FACTORIES) {
it(`${name} renders German headings when data.language='de' overrides meta.language='en'`, async () => {
const Resume = factory.resume(ReactPdf);
const element = createElement(Resume, { data: sampleData, meta: englishMeta });
const buf = await ReactPdf.renderToBuffer(element);
const parser = new PDFParse({ data: new Uint8Array(buf) });
const parsed = await parser.getText();
// Templates apply `text-transform: uppercase` to section headings,
// so compare in lower-case.
const text = parsed.text.toLowerCase();

for (const heading of GERMAN_HEADINGS) {
expect(
text,
`${name} should contain German heading "${heading}"`,
).toContain(heading.toLowerCase());
}
for (const heading of ENGLISH_HEADINGS) {
expect(
text,
`${name} should NOT contain English heading "${heading}"`,
).not.toContain(heading.toLowerCase());
}
}, 30_000);
}
});
Loading