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:
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user