feat(i18n): localise document editing controls
This commit is contained in:
@@ -66,7 +66,12 @@ function routeGet(overrides: Record<string, any> = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => jest.clearAllMocks());
|
beforeEach(() => {
|
||||||
|
window.localStorage.removeItem("uiLanguage");
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => window.localStorage.removeItem("uiLanguage"));
|
||||||
|
|
||||||
// ---------- CV ----------
|
// ---------- CV ----------
|
||||||
|
|
||||||
@@ -185,6 +190,19 @@ test("an empty cover letter offers the template and an empty history", async ()
|
|||||||
.toContain("Dear Hiring Manager");
|
.toContain("Dear Hiring Manager");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the empty cover-letter template and rich-text controls follow Bokmål UI language", async () => {
|
||||||
|
window.localStorage.setItem("uiLanguage", "nb");
|
||||||
|
routeGet({ coverLetter: { text: null, currentVersion: 0, versions: [], aiSuggestionCount: 0 } });
|
||||||
|
|
||||||
|
render(<ApplicationCoverLetterSection jobId={7} />);
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: /start fra mal/i }));
|
||||||
|
expect((screen.getByLabelText("Søknadsbrev") as HTMLTextAreaElement).value)
|
||||||
|
.toContain("Kjære rekrutteringsansvarlig");
|
||||||
|
expect(screen.getByRole("button", { name: "Fet" })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "Lenke" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test("AI cover letter suggestions use the linked CV and require explicit apply", async () => {
|
test("AI cover letter suggestions use the linked CV and require explicit apply", async () => {
|
||||||
routeGet();
|
routeGet();
|
||||||
mockedApi.post.mockResolvedValue({
|
mockedApi.post.mockResolvedValue({
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ export function ApplicationTailoringSection({ jobId }: { jobId: number }) {
|
|||||||
|
|
||||||
// ---------- Cover letter ----------
|
// ---------- Cover letter ----------
|
||||||
|
|
||||||
const TEMPLATE = `Dear Hiring Manager,
|
const COVER_LETTER_TEMPLATES = {
|
||||||
|
en: `Dear Hiring Manager,
|
||||||
|
|
||||||
I am writing to apply for the [role] position at [company]. [One sentence on why this company, specifically.]
|
I am writing to apply for the [role] position at [company]. [One sentence on why this company, specifically.]
|
||||||
|
|
||||||
@@ -264,10 +265,23 @@ In my current role I [the most relevant thing you have done, with a concrete out
|
|||||||
I would welcome the chance to talk it through.
|
I would welcome the chance to talk it through.
|
||||||
|
|
||||||
Kind regards,
|
Kind regards,
|
||||||
[Your name]`;
|
[Your name]`,
|
||||||
|
nb: `Kjære rekrutteringsansvarlig,
|
||||||
|
|
||||||
|
Jeg søker med dette på stillingen som [stilling] hos [selskap]. [Én konkret setning om hvorfor akkurat dette selskapet.]
|
||||||
|
|
||||||
|
I min nåværende rolle har jeg [det mest relevante du har gjort, med et konkret resultat]. [Et annet eksempel som samsvarer med det stillingsannonsen etterspør.]
|
||||||
|
|
||||||
|
[Hvorfor du ønsker denne stillingen, med egne ord.]
|
||||||
|
|
||||||
|
Jeg ser frem til muligheten til å utdype dette i et intervju.
|
||||||
|
|
||||||
|
Med vennlig hilsen
|
||||||
|
[Navnet ditt]`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId: number; onDirtyChange?: (dirty: boolean) => void }) {
|
export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId: number; onDirtyChange?: (dirty: boolean) => void }) {
|
||||||
const { t } = useI18n();
|
const { language, t } = useI18n();
|
||||||
const { data, error, loading, setData, setError } = useAsset<CoverLetter>(
|
const { data, error, loading, setData, setError } = useAsset<CoverLetter>(
|
||||||
() => applicationAssetsApi.coverLetter(jobId),
|
() => applicationAssetsApi.coverLetter(jobId),
|
||||||
[jobId],
|
[jobId],
|
||||||
@@ -341,7 +355,7 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={busy || text.trim().length > 0}
|
disabled={busy || text.trim().length > 0}
|
||||||
onClick={() => setDraft(TEMPLATE)}
|
onClick={() => setDraft(COVER_LETTER_TEMPLATES[language])}
|
||||||
>
|
>
|
||||||
{t("assetsStartTemplate")}
|
{t("assetsStartTemplate")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import FormatUnderlinedIcon from "@mui/icons-material/FormatUnderlined";
|
|||||||
import LinkIcon from "@mui/icons-material/Link";
|
import LinkIcon from "@mui/icons-material/Link";
|
||||||
|
|
||||||
import { wrapSelection } from "../cvBuilder";
|
import { wrapSelection } from "../cvBuilder";
|
||||||
|
import { useI18n } from "../i18n/I18nProvider";
|
||||||
|
|
||||||
// Lightweight rich text: a plain textarea plus a markdown toolbar. Storage stays plain text
|
// Lightweight rich text: a plain textarea plus a markdown toolbar. Storage stays plain text
|
||||||
// (**bold**, *italic*, __underline__, [text](url)); the server renderer converts the same whitelist
|
// (**bold**, *italic*, __underline__, [text](url)); the server renderer converts the same whitelist
|
||||||
@@ -29,6 +30,7 @@ export default function RichTextField({
|
|||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
const ref = useRef<HTMLTextAreaElement | null>(null);
|
const ref = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
const apply = (before: string, after: string, placeholderText: string) => {
|
const apply = (before: string, after: string, placeholderText: string) => {
|
||||||
@@ -53,10 +55,10 @@ export default function RichTextField({
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box sx={{ display: "flex", gap: 0.25, mb: 0.25 }}>
|
<Box sx={{ display: "flex", gap: 0.25, mb: 0.25 }}>
|
||||||
{btn("Bold", <FormatBoldIcon fontSize="inherit" />, "**", "**", "bold text")}
|
{btn(t("richTextBold"), <FormatBoldIcon fontSize="inherit" />, "**", "**", t("richTextBoldPlaceholder"))}
|
||||||
{btn("Italic", <FormatItalicIcon fontSize="inherit" />, "*", "*", "italic text")}
|
{btn(t("richTextItalic"), <FormatItalicIcon fontSize="inherit" />, "*", "*", t("richTextItalicPlaceholder"))}
|
||||||
{btn("Underline", <FormatUnderlinedIcon fontSize="inherit" />, "__", "__", "underlined")}
|
{btn(t("richTextUnderline"), <FormatUnderlinedIcon fontSize="inherit" />, "__", "__", t("richTextUnderlinePlaceholder"))}
|
||||||
{btn("Link", <LinkIcon fontSize="inherit" />, "[", "](https://)", "link text")}
|
{btn(t("richTextLink"), <LinkIcon fontSize="inherit" />, "[", "](https://)", t("richTextLinkPlaceholder"))}
|
||||||
</Box>
|
</Box>
|
||||||
<TextField
|
<TextField
|
||||||
inputRef={ref}
|
inputRef={ref}
|
||||||
|
|||||||
@@ -184,6 +184,14 @@ export const translations = {
|
|||||||
assetsCoverPlaceholder: "Write it yourself, start from the template, or generate a tailored draft.",
|
assetsCoverPlaceholder: "Write it yourself, start from the template, or generate a tailored draft.",
|
||||||
assetsDiscardChanges: "Discard changes",
|
assetsDiscardChanges: "Discard changes",
|
||||||
assetsStartTemplate: "Start from template",
|
assetsStartTemplate: "Start from template",
|
||||||
|
richTextBold: "Bold",
|
||||||
|
richTextBoldPlaceholder: "bold text",
|
||||||
|
richTextItalic: "Italic",
|
||||||
|
richTextItalicPlaceholder: "italic text",
|
||||||
|
richTextUnderline: "Underline",
|
||||||
|
richTextUnderlinePlaceholder: "underlined text",
|
||||||
|
richTextLink: "Link",
|
||||||
|
richTextLinkPlaceholder: "link text",
|
||||||
assetsUnsavedChanges: "Unsaved changes",
|
assetsUnsavedChanges: "Unsaved changes",
|
||||||
assetsVersionHistory: "Version history",
|
assetsVersionHistory: "Version history",
|
||||||
assetsNoVersions: "No versions yet. The first save starts the history.",
|
assetsNoVersions: "No versions yet. The first save starts the history.",
|
||||||
@@ -1559,6 +1567,14 @@ export const translations = {
|
|||||||
assetsCoverPlaceholder: "Skriv selv, start fra malen eller generer et tilpasset utkast.",
|
assetsCoverPlaceholder: "Skriv selv, start fra malen eller generer et tilpasset utkast.",
|
||||||
assetsDiscardChanges: "Forkast endringer",
|
assetsDiscardChanges: "Forkast endringer",
|
||||||
assetsStartTemplate: "Start fra mal",
|
assetsStartTemplate: "Start fra mal",
|
||||||
|
richTextBold: "Fet",
|
||||||
|
richTextBoldPlaceholder: "fet tekst",
|
||||||
|
richTextItalic: "Kursiv",
|
||||||
|
richTextItalicPlaceholder: "kursiv tekst",
|
||||||
|
richTextUnderline: "Understrek",
|
||||||
|
richTextUnderlinePlaceholder: "understreket tekst",
|
||||||
|
richTextLink: "Lenke",
|
||||||
|
richTextLinkPlaceholder: "lenketekst",
|
||||||
assetsUnsavedChanges: "Ulagrede endringer",
|
assetsUnsavedChanges: "Ulagrede endringer",
|
||||||
assetsVersionHistory: "Versjonshistorikk",
|
assetsVersionHistory: "Versjonshistorikk",
|
||||||
assetsNoVersions: "Ingen versjoner ennå. Første lagring starter historikken.",
|
assetsNoVersions: "Ingen versjoner ennå. Første lagring starter historikken.",
|
||||||
|
|||||||
Reference in New Issue
Block a user