mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
i18n: convert settings, chat, patient records and notes; update docs
Finish the i18n pass: settings panels (profile/care-team/signing), the chat heading + input, the patient cards / detail / create-edit form, and the notes page + rich-text editor are all keyed in en/translation.json. All 526 static t() keys resolve. Document the new backend resources + Socket.io realtime (backend README/CLAUDE) and the i18n coverage (frontend CLAUDE). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { NotesEditor } from "@/components/notes/notes-editor";
|
||||
import {
|
||||
Sheet,
|
||||
@@ -30,11 +32,14 @@ export function NoteDetailSheet({
|
||||
onSave: (data: { title: string; content: string }) => void;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Sheet onOpenChange={onOpenChange} open={open}>
|
||||
<SheetPopup className="sm:max-w-2xl" side="right">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{note?.id ? "Edit note" : "New note"}</SheetTitle>
|
||||
<SheetTitle>
|
||||
{note?.id ? t("notes.editNote") : t("notes.new")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
{/* Plain flex container (not SheetPanel) so the editor gets a bounded
|
||||
height and scrolls internally rather than nesting two scroll areas. */}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Undo2,
|
||||
} from "lucide-react";
|
||||
import { type ReactNode, useReducer, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
@@ -77,6 +78,7 @@ export function NotesEditor({
|
||||
onSave: (data: { title: string; content: string }) => void;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [title, setTitle] = useState(note.title);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
// Force a re-render on every editor transaction so the toolbar reflects the
|
||||
@@ -87,7 +89,7 @@ export function NotesEditor({
|
||||
content: note.content,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Placeholder.configure({ placeholder: "Write your note…" }),
|
||||
Placeholder.configure({ placeholder: t("notes.editor.placeholder") }),
|
||||
],
|
||||
// Required under Next's SSR to avoid a hydration mismatch.
|
||||
immediatelyRender: false,
|
||||
@@ -105,7 +107,7 @@ export function NotesEditor({
|
||||
|
||||
const save = () =>
|
||||
onSave({
|
||||
title: title.trim() || "Untitled note",
|
||||
title: title.trim() || t("notes.untitled"),
|
||||
content: editor.getHTML(),
|
||||
});
|
||||
|
||||
@@ -115,12 +117,12 @@ export function NotesEditor({
|
||||
<Input
|
||||
className="font-medium text-base"
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Note title"
|
||||
placeholder={t("notes.editor.titlePlaceholder")}
|
||||
value={title}
|
||||
/>
|
||||
{onDelete && (
|
||||
<Button
|
||||
aria-label="Delete note"
|
||||
aria-label={t("notes.editor.delete")}
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
size="icon"
|
||||
type="button"
|
||||
@@ -131,7 +133,7 @@ export function NotesEditor({
|
||||
)}
|
||||
<Button disabled={saving} onClick={save} type="button">
|
||||
<Save className="size-4" />
|
||||
{saving ? "Saving…" : "Save"}
|
||||
{saving ? t("notes.editor.saving") : t("notes.editor.save")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -139,21 +141,21 @@ export function NotesEditor({
|
||||
<ToolbarGroup>
|
||||
<FormatButton
|
||||
active={editor.isActive("bold")}
|
||||
label="Bold"
|
||||
label={t("notes.editor.bold")}
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
>
|
||||
<Bold />
|
||||
</FormatButton>
|
||||
<FormatButton
|
||||
active={editor.isActive("italic")}
|
||||
label="Italic"
|
||||
label={t("notes.editor.italic")}
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
>
|
||||
<Italic />
|
||||
</FormatButton>
|
||||
<FormatButton
|
||||
active={editor.isActive("underline")}
|
||||
label="Underline"
|
||||
label={t("notes.editor.underline")}
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
>
|
||||
<UnderlineIcon />
|
||||
@@ -163,7 +165,7 @@ export function NotesEditor({
|
||||
<ToolbarGroup>
|
||||
<FormatButton
|
||||
active={editor.isActive("heading", { level: 1 })}
|
||||
label="Heading 1"
|
||||
label={t("notes.editor.heading1")}
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||
}
|
||||
@@ -172,7 +174,7 @@ export function NotesEditor({
|
||||
</FormatButton>
|
||||
<FormatButton
|
||||
active={editor.isActive("heading", { level: 2 })}
|
||||
label="Heading 2"
|
||||
label={t("notes.editor.heading2")}
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
}
|
||||
@@ -184,14 +186,14 @@ export function NotesEditor({
|
||||
<ToolbarGroup>
|
||||
<FormatButton
|
||||
active={editor.isActive("bulletList")}
|
||||
label="Bullet list"
|
||||
label={t("notes.editor.bulletList")}
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
>
|
||||
<List />
|
||||
</FormatButton>
|
||||
<FormatButton
|
||||
active={editor.isActive("orderedList")}
|
||||
label="Numbered list"
|
||||
label={t("notes.editor.numberedList")}
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
>
|
||||
<ListOrdered />
|
||||
@@ -201,14 +203,14 @@ export function NotesEditor({
|
||||
<ToolbarGroup>
|
||||
<FormatButton
|
||||
disabled={!editor.can().undo()}
|
||||
label="Undo"
|
||||
label={t("notes.editor.undo")}
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
>
|
||||
<Undo2 />
|
||||
</FormatButton>
|
||||
<FormatButton
|
||||
disabled={!editor.can().redo()}
|
||||
label="Redo"
|
||||
label={t("notes.editor.redo")}
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
>
|
||||
<Redo2 />
|
||||
@@ -222,12 +224,14 @@ export function NotesEditor({
|
||||
|
||||
{onDelete && (
|
||||
<ConfirmDialog
|
||||
confirmLabel="Delete note"
|
||||
description={`"${title.trim() || "Untitled note"}" will be permanently deleted. This can't be undone.`}
|
||||
confirmLabel={t("notes.editor.confirmLabel")}
|
||||
description={t("notes.editor.confirmDescription", {
|
||||
name: title.trim() || t("notes.untitled"),
|
||||
})}
|
||||
onConfirm={onDelete}
|
||||
onOpenChange={setConfirmOpen}
|
||||
open={confirmOpen}
|
||||
title="Delete this note?"
|
||||
title={t("notes.editor.confirmTitle")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { NotebookPen, Plus } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { NoteDetailSheet } from "@/components/notes/note-detail-sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -31,6 +32,7 @@ const newDraft = (): Note => ({
|
||||
});
|
||||
|
||||
export function NotesView() {
|
||||
const { t } = useTranslation();
|
||||
const [notes, setNotes] = useState<Note[]>([]);
|
||||
// The note shown in the editor Sheet; null when the Sheet is closed.
|
||||
const [selected, setSelected] = useState<Note | null>(null);
|
||||
@@ -48,8 +50,8 @@ export function NotesView() {
|
||||
.catch((err) => {
|
||||
if (active) {
|
||||
notify.error(
|
||||
"Couldn't load notes",
|
||||
err instanceof Error ? err.message : "Please try again.",
|
||||
t("notes.loadFailed"),
|
||||
err instanceof Error ? err.message : t("notes.tryAgain"),
|
||||
);
|
||||
}
|
||||
})
|
||||
@@ -81,11 +83,11 @@ export function NotesView() {
|
||||
const list = await listNotes();
|
||||
setNotes(list);
|
||||
setSelected(list.find((n) => n.id === saved.id) ?? saved);
|
||||
notify.success("Note saved");
|
||||
notify.success(t("notes.saved"));
|
||||
} catch (err) {
|
||||
notify.error(
|
||||
"Couldn't save note",
|
||||
err instanceof Error ? err.message : "Please try again.",
|
||||
t("notes.saveFailed"),
|
||||
err instanceof Error ? err.message : t("notes.tryAgain"),
|
||||
);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -99,11 +101,11 @@ export function NotesView() {
|
||||
setNotes(list);
|
||||
setSelected(null);
|
||||
setSheetOpen(false);
|
||||
notify.success("Note deleted");
|
||||
notify.success(t("notes.deleted"));
|
||||
} catch (err) {
|
||||
notify.error(
|
||||
"Couldn't delete note",
|
||||
err instanceof Error ? err.message : "Please try again.",
|
||||
t("notes.deleteFailed"),
|
||||
err instanceof Error ? err.message : t("notes.tryAgain"),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -112,20 +114,20 @@ export function NotesView() {
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-6 px-6 py-10">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="font-semibold text-2xl tracking-tight">Notes</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Clinical notes. Click a note to open it.
|
||||
</p>
|
||||
<h1 className="font-semibold text-2xl tracking-tight">
|
||||
{t("notes.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">{t("notes.subtitle")}</p>
|
||||
</div>
|
||||
<Button className="rounded-3xl" onClick={startNew} type="button">
|
||||
<Plus className="size-4" />
|
||||
New note
|
||||
{t("notes.new")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="rounded-2xl border bg-card/30 px-4 py-10 text-center text-muted-foreground text-sm">
|
||||
Loading…
|
||||
{t("notes.loading")}
|
||||
</div>
|
||||
) : notes.length === 0 ? (
|
||||
<div className="flex flex-1 items-center justify-center rounded-2xl border bg-card/30 py-16">
|
||||
@@ -134,15 +136,15 @@ export function NotesView() {
|
||||
<EmptyMedia variant="icon">
|
||||
<NotebookPen />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No notes yet</EmptyTitle>
|
||||
<EmptyTitle>{t("notes.emptyTitle")}</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Create a note to start writing.
|
||||
{t("notes.emptyDescription")}
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button onClick={startNew} type="button">
|
||||
<Plus className="size-4" />
|
||||
New note
|
||||
{t("notes.new")}
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
@@ -157,10 +159,12 @@ export function NotesView() {
|
||||
type="button"
|
||||
>
|
||||
<span className="w-full truncate font-medium text-foreground text-sm">
|
||||
{n.title || "Untitled note"}
|
||||
{n.title || t("notes.untitled")}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
Updated {new Date(n.updatedAt).toLocaleDateString()}
|
||||
{t("notes.updated", {
|
||||
date: new Date(n.updatedAt).toLocaleDateString(),
|
||||
})}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user