feat: add Russian localization and independent STT languages
This commit is contained in:
@@ -11,14 +11,14 @@
|
||||
* markers are invisible by the engine's law).
|
||||
*/
|
||||
import { sessionMarkdown } from "@/src/core";
|
||||
import { getChat } from "@/src/server";
|
||||
import { getChat, applicationLanguage } from "@/src/server";
|
||||
|
||||
/** Filename-safe title (own module scope on purpose: tiny + pure). */
|
||||
function slugify(title: string): string {
|
||||
const slug = title
|
||||
.toLowerCase()
|
||||
.normalize("NFKD")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return slug.length > 0 ? slug : "session";
|
||||
}
|
||||
@@ -27,11 +27,11 @@ export async function GET(_req: Request, ctx: { params: Promise<{ id: string }>
|
||||
const { id } = await ctx.params;
|
||||
const record = getChat(id);
|
||||
if (!record) return Response.json({ error: "Unknown chat" }, { status: 404 });
|
||||
const markdown = sessionMarkdown(record.messages, { title: record.title, anchors: false });
|
||||
const markdown = sessionMarkdown(record.messages, { title: record.title, anchors: false, language: applicationLanguage(_req.headers.get("accept-language")) });
|
||||
return new Response(markdown, {
|
||||
headers: {
|
||||
"content-type": "text/markdown; charset=utf-8",
|
||||
"content-disposition": `attachment; filename="${slugify(record.title)}.md"`,
|
||||
"content-disposition": `attachment; filename="${/^[a-z0-9-]+$/.test(slugify(record.title)) ? slugify(record.title) : "session"}.md"; filename*=UTF-8''${encodeURIComponent(slugify(record.title))}.md`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* healthy. The dock's chips + graph refetch track progress live through
|
||||
* /api/graph/status, as with session runs.
|
||||
*/
|
||||
import { extraction, NotesImportError } from "@/src/server";
|
||||
import { extraction, NotesImportError, applicationLanguage } from "@/src/server";
|
||||
import {
|
||||
ModelsNotConfiguredError,
|
||||
PROVIDER_KEY_MISSING_CODE,
|
||||
@@ -56,7 +56,7 @@ export async function POST(request: Request) {
|
||||
const { file, requestIndex } = next;
|
||||
try {
|
||||
const text = await file.text();
|
||||
const stats = await extraction.importNotes({ filePath: file.name, text });
|
||||
const stats = await extraction.importNotes({ filePath: file.name, text, language: applicationLanguage(request.headers.get("accept-language")) });
|
||||
results.push({ filePath: file.name, requestIndex, outcome: "completed", ok: true, stats });
|
||||
} catch (err) {
|
||||
if (err instanceof ModelsNotConfiguredError) {
|
||||
|
||||
@@ -598,3 +598,6 @@ body {
|
||||
outline: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Source Sans 3 lacks Cyrillic; Russian prose uses the bundled Noto Sans subset. */
|
||||
html[lang="ru"] { --font-sans3: var(--font-russian-prose); }
|
||||
|
||||
+22
-9
@@ -1,5 +1,9 @@
|
||||
import { headers } from "next/headers";
|
||||
import { readSettingsRecord } from "@/src/server";
|
||||
import { resolveLanguage, systemLanguage, translate } from "@/src/core";
|
||||
import { I18nProvider } from "@/components/i18n";
|
||||
import type { Metadata } from "next";
|
||||
import { JetBrains_Mono, Source_Sans_3 } from "next/font/google";
|
||||
import { JetBrains_Mono, Source_Sans_3, Noto_Sans } from "next/font/google";
|
||||
import "katex/dist/katex.min.css";
|
||||
import "./globals.css";
|
||||
import { AppShell, SidebarProvider } from "@/components/sidebar";
|
||||
@@ -10,21 +14,29 @@ import UpdateBanner from "@/components/update-banner";
|
||||
|
||||
// Chrome = JetBrains Mono, teaching prose = Source Sans 3, titles = CMU
|
||||
// Serif (the bundled lh-cmu @font-face in globals.css — no CDN).
|
||||
const ui = JetBrains_Mono({ subsets: ["latin"], variable: "--font-jetbrains" });
|
||||
const ui = JetBrains_Mono({ subsets: ["latin", "cyrillic"], variable: "--font-jetbrains" });
|
||||
const prose = Source_Sans_3({ subsets: ["latin"], variable: "--font-sans3" });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Learning Harness",
|
||||
description: "A teacher agent with a memory — sessions, quizzes, and a knowledge graph of what you know.",
|
||||
};
|
||||
const russianProse = Noto_Sans({ subsets: ["latin", "cyrillic"], variable: "--font-russian-prose" });
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const language = resolveLanguage(readSettingsRecord()?.general?.language, (await headers()).get("accept-language"));
|
||||
return {
|
||||
title: "Learning Harness",
|
||||
description: translate(language, "A teacher agent with a memory — sessions, quizzes, and a knowledge graph of what you know."),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const general = readSettingsRecord()?.general ?? {};
|
||||
const system = systemLanguage((await headers()).get("accept-language"));
|
||||
const language = resolveLanguage(general.language, system);
|
||||
return (
|
||||
<html lang="en" className={`dark ${ui.variable} ${prose.variable}`}>
|
||||
<html lang={language} className={`dark ${ui.variable} ${prose.variable} ${russianProse.variable}`}>
|
||||
<body className="min-h-dvh antialiased">
|
||||
{/* The notification channel rides OUTSIDE everything: sidebar, dock,
|
||||
and pages all subscribe as the polls retire (push S3, 2026-10-02). */}
|
||||
<ServerEventsProvider>
|
||||
<I18nProvider initialGeneral={general} system={system}><ServerEventsProvider>
|
||||
<AppearanceProvider>
|
||||
<SidebarProvider>
|
||||
<ActivityProvider>
|
||||
@@ -35,6 +47,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
</ServerEventsProvider>
|
||||
{/* Auto-update banner (Wave C): fixed card, Electron-only pushes. */}
|
||||
<UpdateBanner />
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user