Add language settings for AI-generated resume output (#252)

* Add language settings for AI-generated resume output

* Resolve merge conflicts for language settings PR

* Fix language settings review feedback and CI lint

* Tighten language setting precedence and onboarding validation

---------

Co-authored-by: saad <Saad>
This commit is contained in:
Saad
2026-03-11 18:24:01 +00:00
committed by GitHub
co-authored by saad <Saad>
parent faea61a249
commit f92b80dfe2
23 changed files with 1117 additions and 58 deletions
+61
View File
@@ -118,6 +118,67 @@ describe("settingsRegistry helpers", () => {
});
});
describe("writing-style language settings", () => {
it("defaults to manual english", () => {
const previousLanguageMode = process.env.CHAT_STYLE_LANGUAGE_MODE;
const previousManualLanguage = process.env.CHAT_STYLE_MANUAL_LANGUAGE;
delete process.env.CHAT_STYLE_LANGUAGE_MODE;
delete process.env.CHAT_STYLE_MANUAL_LANGUAGE;
try {
expect(settingsRegistry.chatStyleLanguageMode.default()).toBe("manual");
expect(settingsRegistry.chatStyleManualLanguage.default()).toBe(
"english",
);
} finally {
if (previousLanguageMode === undefined) {
delete process.env.CHAT_STYLE_LANGUAGE_MODE;
} else {
process.env.CHAT_STYLE_LANGUAGE_MODE = previousLanguageMode;
}
if (previousManualLanguage === undefined) {
delete process.env.CHAT_STYLE_MANUAL_LANGUAGE;
} else {
process.env.CHAT_STYLE_MANUAL_LANGUAGE = previousManualLanguage;
}
}
});
it("parses and serializes supported language settings", () => {
expect(settingsRegistry.chatStyleLanguageMode.parse("manual")).toBe(
"manual",
);
expect(settingsRegistry.chatStyleLanguageMode.parse("match-resume")).toBe(
"match-resume",
);
expect(settingsRegistry.chatStyleLanguageMode.parse("auto")).toBeNull();
expect(settingsRegistry.chatStyleLanguageMode.parse("")).toBeNull();
expect(
settingsRegistry.chatStyleLanguageMode.serialize("match-resume"),
).toBe("match-resume");
expect(settingsRegistry.chatStyleLanguageMode.serialize(null)).toBeNull();
expect(settingsRegistry.chatStyleManualLanguage.parse("english")).toBe(
"english",
);
expect(settingsRegistry.chatStyleManualLanguage.parse("german")).toBe(
"german",
);
expect(
settingsRegistry.chatStyleManualLanguage.parse("italian"),
).toBeNull();
expect(settingsRegistry.chatStyleManualLanguage.parse("")).toBeNull();
expect(
settingsRegistry.chatStyleManualLanguage.serialize("spanish"),
).toBe("spanish");
expect(
settingsRegistry.chatStyleManualLanguage.serialize(null),
).toBeNull();
});
});
describe("LLM provider parsing", () => {
it("normalizes the documented openai-compatible alias", () => {
expect(settingsRegistry.llmProvider.parse("openai-compatible")).toBe(
+54 -1
View File
@@ -1,5 +1,11 @@
import { z } from "zod";
import type { ResumeProjectsSettings } from "./types/settings";
import {
CHAT_STYLE_LANGUAGE_MODE_VALUES,
CHAT_STYLE_MANUAL_LANGUAGE_VALUES,
type ChatStyleLanguageMode,
type ChatStyleManualLanguage,
type ResumeProjectsSettings,
} from "./types/settings";
function parseNonEmptyStringOrNull(raw: string | undefined): string | null {
return raw === undefined || raw === "" ? null : raw;
@@ -49,6 +55,25 @@ function serializeBitBool(value: boolean | null | undefined): string | null {
return value ? "1" : "0";
}
function createEnumParser<const TValues extends readonly [string, ...string[]]>(
values: TValues,
): (raw: string | undefined) => TValues[number] | null {
const allowedValues = new Set<string>(values);
return (raw: string | undefined): TValues[number] | null => {
if (!raw) return null;
return allowedValues.has(raw) ? (raw as TValues[number]) : null;
};
}
const parseChatStyleLanguageModeOrNull = createEnumParser(
CHAT_STYLE_LANGUAGE_MODE_VALUES,
);
const parseChatStyleManualLanguageOrNull = createEnumParser(
CHAT_STYLE_MANUAL_LANGUAGE_VALUES,
);
export const resumeProjectsSchema = z.object({
maxProjects: z.number().int().min(0).max(100),
lockedProjectIds: z.array(z.string().trim().min(1)).max(200),
@@ -307,6 +332,34 @@ export const settingsRegistry = {
serialize: (value: string | null | undefined): string | null =>
value ?? null,
},
chatStyleLanguageMode: {
kind: "typed" as const,
schema: z.enum(CHAT_STYLE_LANGUAGE_MODE_VALUES),
default: (): ChatStyleLanguageMode =>
parseChatStyleLanguageModeOrNull(
typeof process !== "undefined"
? process.env.CHAT_STYLE_LANGUAGE_MODE
: undefined,
) ?? "manual",
parse: parseChatStyleLanguageModeOrNull,
serialize: (
value: ChatStyleLanguageMode | null | undefined,
): string | null => value ?? null,
},
chatStyleManualLanguage: {
kind: "typed" as const,
schema: z.enum(CHAT_STYLE_MANUAL_LANGUAGE_VALUES),
default: (): ChatStyleManualLanguage =>
parseChatStyleManualLanguageOrNull(
typeof process !== "undefined"
? process.env.CHAT_STYLE_MANUAL_LANGUAGE
: undefined,
) ?? "english",
parse: parseChatStyleManualLanguageOrNull,
serialize: (
value: ChatStyleManualLanguage | null | undefined,
): string | null => value ?? null,
},
backupEnabled: {
kind: "typed" as const,
schema: z.boolean(),
+46
View File
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { updateSettingsSchema } from "./settings-schema";
describe("updateSettingsSchema language settings", () => {
it("accepts supported language mode and manual language values", () => {
expect(
updateSettingsSchema.parse({
chatStyleLanguageMode: "manual",
chatStyleManualLanguage: "german",
}),
).toEqual({
chatStyleLanguageMode: "manual",
chatStyleManualLanguage: "german",
});
expect(
updateSettingsSchema.parse({
chatStyleLanguageMode: null,
chatStyleManualLanguage: null,
}),
).toEqual({
chatStyleLanguageMode: null,
chatStyleManualLanguage: null,
});
});
it("rejects unsupported language mode and manual language values", () => {
const result = updateSettingsSchema.safeParse({
chatStyleLanguageMode: "auto",
chatStyleManualLanguage: "italian",
});
expect(result.success).toBe(false);
if (result.success) {
return;
}
expect(
result.error.flatten().fieldErrors.chatStyleLanguageMode,
).toBeDefined();
expect(
result.error.flatten().fieldErrors.chatStyleManualLanguage,
).toBeDefined();
});
});
+10
View File
@@ -188,6 +188,16 @@ export const createAppSettings = (
chatStyleFormality: { value: "medium", default: "medium", override: null },
chatStyleConstraints: { value: "", default: "", override: null },
chatStyleDoNotUse: { value: "", default: "", override: null },
chatStyleLanguageMode: {
value: "manual",
default: "manual",
override: null,
},
chatStyleManualLanguage: {
value: "english",
default: "english",
override: null,
},
llmApiKeyHint: null,
rxresumeApiKeyHint: null,
rxresumeEmail: null,
+30
View File
@@ -14,6 +14,34 @@ export interface ResumeProjectsSettings {
export type RxResumeMode = "v4" | "v5";
export const CHAT_STYLE_LANGUAGE_MODE_VALUES = [
"manual",
"match-resume",
] as const;
export type ChatStyleLanguageMode =
(typeof CHAT_STYLE_LANGUAGE_MODE_VALUES)[number];
export const CHAT_STYLE_MANUAL_LANGUAGE_VALUES = [
"english",
"german",
"french",
"spanish",
] as const;
export type ChatStyleManualLanguage =
(typeof CHAT_STYLE_MANUAL_LANGUAGE_VALUES)[number];
export const CHAT_STYLE_MANUAL_LANGUAGE_LABELS: Record<
ChatStyleManualLanguage,
string
> = {
english: "English",
german: "German",
french: "French",
spanish: "Spanish",
};
export interface ResumeProfile {
basics?: {
name?: string;
@@ -135,6 +163,8 @@ export interface AppSettings {
chatStyleFormality: Resolved<string>;
chatStyleConstraints: Resolved<string>;
chatStyleDoNotUse: Resolved<string>;
chatStyleLanguageMode: Resolved<ChatStyleLanguageMode>;
chatStyleManualLanguage: Resolved<ChatStyleManualLanguage>;
backupEnabled: Resolved<boolean>;
backupHour: Resolved<number>;
backupMaxCount: Resolved<number>;