/**
* Response-language options.
*
* Single source of truth for both the config dropdown (LM Studio renders it
* from `languageOptions()`) and the prompt instruction injected at runtime
* (`languageInstruction()`), so the two can never drift.
*/
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { resolve } from "node:path";
export interface LanguageOption {
/** Stored config value. */
code: string;
/** Label shown in the LM Studio dropdown. */
label: string;
/** Instruction injected into the prompt; empty means "no constraint". */
instruction: string;
}
// Ordered by relevance for LLM role play: "Model default" first, then the
// most-used languages for this use case (English leads, followed by the largest
// speaker bases and the strongest RP communities).
export const LANGUAGES: LanguageOption[] = [
{ code: "model-default", label: "Model default", instruction: "" },
{ code: "en", label: "English", instruction: "Always write your entire response in English." },
{ code: "zh", label: "中文", instruction: "Always write your entire response in Chinese (中文)." },
{ code: "es", label: "Español", instruction: "Always write your entire response in Spanish (español)." },
{ code: "ru", label: "Русский", instruction: "Always write your entire response in Russian (русский)." },
{ code: "ja", label: "日本語", instruction: "Always write your entire response in Japanese (日本語)." },
{ code: "fr", label: "Français", instruction: "Always write your entire response in French (français)." },
{ code: "de", label: "Deutsch", instruction: "Always write your entire response in German (Deutsch)." },
{ code: "pt", label: "Português", instruction: "Always write your entire response in Portuguese (português)." },
{ code: "ko", label: "한국어", instruction: "Always write your entire response in Korean (한국어)." },
{ code: "it", label: "Italiano", instruction: "Always write your entire response in Italian (italiano)." },
];
/** Dropdown options for `createConfigSchematics(...).field(..., "select", ...)`. */
export function languageOptions(): { value: string; displayName: string }[] {
return LANGUAGES.map((l) => ({ value: l.code, displayName: l.label }));
}
/** Prompt instruction for a language code (empty string for unknown / default). */
export function languageInstruction(code: string): string {
return LANGUAGES.find((l) => l.code === code)?.instruction ?? "";
}
/**
* Best-effort default for the "Response language" dropdown: match LM Studio's
* own UI language, so a player whose app is in French / Spanish / … gets the
* game master replying in that language out of the box (no setting to touch).
*
* LM Studio stores its UI language in `~/.lmstudio/settings.json` as an
* ISO-639-1 code (`"language": "fr"`); some locales carry a region/script
* subtag (`zh-Hans`, `pt-BR`). The config schema is built synchronously at
* plugin load with no `client` access — so, exactly like the embedding-model
* discovery in `config.ts`, we read the on-disk settings file directly (same
* two candidate locations LM Studio has used across versions).
*
* Returns a supported language `code` (one of `LANGUAGES`) when the UI language
* maps to one, else "en" (English) as the safe fallback. We deliberately do NOT
* fall back to "model-default" here: an explicit English default is wanted when
* detection fails or the UI language isn't one we offer. The first readable
* settings file wins (mirrors the embedding-model discovery).
*/
export function defaultResponseLanguage(): string {
const candidates = [
resolve(homedir(), ".lmstudio", "settings.json"),
resolve(homedir(), ".cache", "lm-studio", "settings.json"),
];
for (const path of candidates) {
try {
const settings = JSON.parse(readFileSync(path, "utf-8")) as { language?: string };
const raw = (settings.language ?? "").trim().toLowerCase();
// Exact match first ("en", "fr", …), then the primary subtag
// ("zh-hans" → "zh", "pt-br" → "pt"). "model-default" is never auto-picked.
const primary = raw.split(/[-_]/)[0];
const hit = LANGUAGES.find(
(l) => l.code !== "model-default" && (l.code === raw || l.code === primary),
);
return hit ? hit.code : "en";
} catch {
// Settings file missing/unreadable at this location — try the next.
}
}
return "en";
}