Skip to content

Commit a38ec92

Browse files
committed
fix: notes localization
1 parent 4c8472a commit a38ec92

10 files changed

Lines changed: 83 additions & 39 deletions

File tree

apps/desktop/src/i18n/locales/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@
356356
"brand": "Amical"
357357
},
358358
"search": {
359+
"buttonLabel": "Search",
359360
"buttonPlaceholder": "Search...",
360361
"inputPlaceholder": "Search settings and notes...",
361362
"noResults": "No results found.",
@@ -385,6 +386,10 @@
385386
"label": "Mute system audio during recording",
386387
"description": "Silence system output while capturing microphone audio"
387388
},
389+
"autoDictateOnNewNote": {
390+
"label": "Auto-dictate on new note",
391+
"description": "Automatically start dictation when creating a new note"
392+
},
388393
"language": {
389394
"label": "Language",
390395
"description": "Choose the language used throughout the app",
@@ -641,6 +646,7 @@
641646
},
642647
"notes": {
643648
"create": "Create note",
649+
"defaultTitleWithDate": "Note - {{date}}",
644650
"untitledTitle": "Untitled Note",
645651
"empty": {
646652
"title": "No notes yet",
@@ -656,6 +662,7 @@
656662
"deleted": "Note deleted",
657663
"deleteFailed": "Failed to delete note: {{message}}",
658664
"saveFailed": "Failed to save changes",
665+
"upgradedToRichNotes": "Your notes have been upgraded to rich notes with markdown formatting.",
659666
"loadFailed": "Failed to load note"
660667
},
661668
"note": {

apps/desktop/src/lib/utils.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,27 @@ export function cn(...inputs: ClassValue[]) {
77

88
export function formatDate(date: Date): string {
99
const now = new Date();
10-
const diffInDays = Math.floor(
11-
(now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24),
10+
const resolvedLocale =
11+
typeof navigator !== "undefined"
12+
? navigator.language
13+
: Intl.DateTimeFormat().resolvedOptions().locale;
14+
15+
const rtf = new Intl.RelativeTimeFormat(resolvedLocale, { numeric: "auto" });
16+
17+
const startOfToday = new Date(now);
18+
startOfToday.setHours(0, 0, 0, 0);
19+
const startOfDate = new Date(date);
20+
startOfDate.setHours(0, 0, 0, 0);
21+
const diffInDays = Math.round(
22+
(startOfToday.getTime() - startOfDate.getTime()) / (1000 * 60 * 60 * 24),
1223
);
1324

14-
if (diffInDays === 0) return "Today";
15-
if (diffInDays === 1) return "Yesterday";
25+
if (diffInDays === 0) return rtf.format(0, "day");
26+
if (diffInDays === 1) return rtf.format(-1, "day");
1627

17-
return date.toLocaleDateString("en-US", {
28+
return new Intl.DateTimeFormat(resolvedLocale, {
1829
month: "short",
1930
day: "numeric",
2031
year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
21-
});
32+
}).format(date);
2233
}

apps/desktop/src/renderer/main/components/command-search-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function CommandSearchButton() {
9494
<>
9595
<SidebarMenuButton onClick={() => setOpen(true)}>
9696
<IconSearch />
97-
<span>Search</span>
97+
<span>{t("settings.search.buttonLabel")}</span>
9898
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground ml-auto">
9999
{shortcutDisplay}
100100
</kbd>

apps/desktop/src/renderer/main/components/create-note-button.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { useNavigate } from "@tanstack/react-router";
44
import { SidebarMenuButton } from "@/components/ui/sidebar";
55
import { api } from "@/trpc/react";
66
import { toast } from "sonner";
7+
import { useTranslation } from "react-i18next";
78

89
export function CreateNoteButton() {
10+
const { t, i18n } = useTranslation();
911
const navigate = useNavigate();
1012
const utils = api.useUtils();
1113
const preferencesQuery = api.settings.getPreferences.useQuery();
@@ -31,21 +33,23 @@ export function CreateNoteButton() {
3133
});
3234
},
3335
onError: (error) => {
34-
toast.error("Failed to create note: " + error.message);
36+
toast.error(
37+
t("settings.notes.toast.createFailed", { message: error.message }),
38+
);
3539
},
3640
});
3741

3842
const onCreateNote = useCallback(() => {
3943
if (createNoteMutation.isPending) return;
40-
const dateStr = new Date().toLocaleDateString("en-GB", {
44+
const dateStr = new Date().toLocaleDateString(i18n.language, {
4145
day: "numeric",
4246
month: "short",
4347
});
4448
createNoteMutation.mutate({
45-
title: `Note - ${dateStr}`,
49+
title: t("settings.notes.defaultTitleWithDate", { date: dateStr }),
4650
initialContent: "",
4751
});
48-
}, [createNoteMutation]);
52+
}, [createNoteMutation, i18n.language, t]);
4953

5054
// Keyboard shortcut: Cmd+N (Mac) / Ctrl+N (Windows/Linux)
5155
useEffect(() => {
@@ -69,7 +73,7 @@ export function CreateNoteButton() {
6973
disabled={createNoteMutation.isPending}
7074
>
7175
<Plus />
72-
<span>New Note</span>
76+
<span>{t("settings.notes.create")}</span>
7377
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground ml-auto">
7478
{shortcutDisplay}
7579
</kbd>

apps/desktop/src/renderer/main/components/editor/yjs-sync-plugin.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { $createParagraphNode, $createTextNode, $getRoot } from "lexical";
44
import type * as Y from "yjs";
55
import { toast } from "sonner";
66
import { debounce } from "@/renderer/main/utils/debounce";
7+
import { useTranslation } from "react-i18next";
78

89
interface YjsSyncPluginProps {
910
yText: Y.Text;
@@ -14,6 +15,7 @@ export function YjsSyncPlugin({
1415
yText,
1516
onSyncStatusChange,
1617
}: YjsSyncPluginProps): null {
18+
const { t } = useTranslation();
1719
const [editor] = useLexicalComposerContext();
1820
const isUpdatingFromYjsRef = useRef(false);
1921
const isUpdatingFromLexicalRef = useRef(false);
@@ -105,7 +107,7 @@ export function YjsSyncPlugin({
105107
}
106108

107109
toast.success(
108-
"Your notes have been upgraded to rich notes with markdown formatting.",
110+
t("settings.notes.toast.upgradedToRichNotes"),
109111
);
110112

111113
isMigratingRef.current = false;

apps/desktop/src/renderer/main/pages/notes/components/note-editor.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { NoteSyncProvider } from "@/renderer/main/providers/sync-provider";
2727
import { YjsSyncPlugin } from "@/renderer/main/components/editor/yjs-sync-plugin";
2828
import { CodeBlockShortcutPlugin } from "@/renderer/main/components/editor/code-block-plugin";
2929
import { ChecklistShortcutPlugin } from "@/renderer/main/components/editor/checklist-shortcut-plugin";
30+
import { toast } from "sonner";
31+
import { useTranslation } from "react-i18next";
3032

3133
interface NoteEditorProps {
3234
noteId: number;
@@ -175,13 +177,17 @@ export function NoteEditor({
175177
onSyncStatusChange,
176178
onReady,
177179
}: NoteEditorProps): React.ReactNode {
180+
const { t } = useTranslation();
178181
const [isLoading, setIsLoading] = useState(true);
179182
const [syncProvider, setSyncProvider] = useState<NoteSyncProvider | null>(
180183
null,
181184
);
182185
const providerRef = useRef<NoteSyncProvider | null>(null);
183186
const destroyQueueRef = useRef<Array<NoteSyncProvider>>([]);
184187
const onReadyCalledRef = useRef(false);
188+
const onSaveErrorRef = useRef(() =>
189+
toast.error(t("settings.notes.toast.saveFailed")),
190+
);
185191

186192
// Handle sync status changes and propagate to parent
187193
const handleSyncStatusChange = useCallback(
@@ -196,6 +202,11 @@ export function NoteEditor({
196202
onReadyCalledRef.current = false;
197203
}, [noteId]);
198204

205+
useEffect(() => {
206+
onSaveErrorRef.current = () =>
207+
toast.error(t("settings.notes.toast.saveFailed"));
208+
}, [t]);
209+
199210
// Notify parent when editor is ready
200211
useEffect(() => {
201212
if (!isLoading && syncProvider && !onReadyCalledRef.current) {
@@ -236,6 +247,7 @@ export function NoteEditor({
236247

237248
const provider = new NoteSyncProvider({
238249
noteId,
250+
onSaveError: () => onSaveErrorRef.current(),
239251
});
240252

241253
providerRef.current = provider;
@@ -299,10 +311,10 @@ export function NoteEditor({
299311
contentEditable={
300312
<ContentEditable
301313
className="min-h-[500px] px-4 py-2 outline-none text-base leading-relaxed"
302-
aria-placeholder="Start writing your note..."
314+
aria-placeholder={t("settings.notes.note.bodyPlaceholder")}
303315
placeholder={
304316
<div className="absolute top-2 left-4 text-muted-foreground pointer-events-none">
305-
Start writing your note...
317+
{t("settings.notes.note.bodyPlaceholder")}
306318
</div>
307319
}
308320
/>

apps/desktop/src/renderer/main/pages/notes/components/note.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -78,44 +78,40 @@ export type NotePageUIProps = {
7878
children?: React.ReactNode;
7979
};
8080

81-
function formatRelativeTime(date: Date): string {
81+
function formatRelativeTime(date: Date, locale: string): string {
8282
const now = new Date();
83-
const diffMs = now.getTime() - date.getTime();
84-
const diffMins = Math.floor(diffMs / (1000 * 60));
85-
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
83+
const diffSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
84+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
8685

87-
// Less than 1 minute
88-
if (diffMins < 1) {
89-
return "now";
86+
if (diffSeconds < 60) {
87+
return rtf.format(0, "second");
9088
}
9189

92-
// Less than 1 hour
90+
const diffMins = Math.floor(diffSeconds / 60);
9391
if (diffMins < 60) {
94-
return `${diffMins} min${diffMins === 1 ? "" : "s"} ago`;
92+
return rtf.format(-diffMins, "minute");
9593
}
9694

97-
// Less than 24 hours
95+
const diffHours = Math.floor(diffMins / 60);
9896
if (diffHours < 24) {
99-
return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
97+
return rtf.format(-diffHours, "hour");
10098
}
10199

102-
// Check if yesterday
103100
const yesterday = new Date(now);
104101
yesterday.setDate(yesterday.getDate() - 1);
105102
if (
106103
date.getDate() === yesterday.getDate() &&
107104
date.getMonth() === yesterday.getMonth() &&
108105
date.getFullYear() === yesterday.getFullYear()
109106
) {
110-
return "yesterday";
107+
return rtf.format(-1, "day");
111108
}
112109

113-
// Older than yesterday - show date
114-
return date.toLocaleDateString("en-GB", {
110+
return new Intl.DateTimeFormat(locale, {
115111
day: "numeric",
116112
month: "short",
117113
year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
118-
});
114+
}).format(date);
119115
}
120116

121117
export default function Note({
@@ -130,7 +126,7 @@ export default function Note({
130126
isDeleting = false,
131127
children,
132128
}: NotePageUIProps) {
133-
const { t } = useTranslation();
129+
const { t, i18n } = useTranslation();
134130
// Local UI state
135131
const [shareEmail, setShareEmail] = useState("");
136132
const [accessLevel, setAccessLevel] = useState("can-read");
@@ -301,6 +297,7 @@ export default function Note({
301297
localEditTime && localEditTime > lastEditDate
302298
? localEditTime
303299
: lastEditDate,
300+
i18n.language,
304301
),
305302
})}
306303
</span>

apps/desktop/src/renderer/main/pages/notes/components/notes-list.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { NoteCard } from "./note-card";
33
import { api } from "@/trpc/react";
44
import { Skeleton } from "@/components/ui/skeleton";
55
import { useNavigate } from "@tanstack/react-router";
6+
import { useTranslation } from "react-i18next";
67

78
export function NotesList() {
9+
const { t } = useTranslation();
810
const navigate = useNavigate();
911

1012
const { data: notes, isLoading } = api.notes.getNotes.useQuery({
@@ -28,7 +30,7 @@ export function NotesList() {
2830
return (
2931
<div className="container mx-auto p-6 max-w-5xl">
3032
<div className="mb-8">
31-
<h1 className="text-xl font-bold">Notes</h1>
33+
<h1 className="text-xl font-bold">{t("settings.nav.notes.title")}</h1>
3234
</div>
3335
<div className="space-y-2">
3436
{[1, 2, 3].map((i) => (
@@ -49,7 +51,7 @@ export function NotesList() {
4951
<div className="container mx-auto p-6 max-w-5xl">
5052
{/* Header Section */}
5153
<div className="mb-8">
52-
<h1 className="text-xl font-bold">Notes</h1>
54+
<h1 className="text-xl font-bold">{t("settings.nav.notes.title")}</h1>
5355
</div>
5456

5557
{formattedNotes.length > 0 && (
@@ -64,9 +66,11 @@ export function NotesList() {
6466
<div className="border border-dashed rounded-lg p-6 text-center space-y-4">
6567
<NotebookText className="w-8 h-8 text-muted-foreground mx-auto" />
6668
<div className="space-y-2">
67-
<p className="text-sm text-muted-foreground">No notes yet</p>
69+
<p className="text-sm text-muted-foreground">
70+
{t("settings.notes.empty.title")}
71+
</p>
6872
<p className="text-xs text-muted-foreground">
69-
Click "New Note" in the sidebar to create your first note
73+
{t("settings.notes.empty.description")}
7074
</p>
7175
</div>
7276
</div>

apps/desktop/src/renderer/main/pages/settings/preferences/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,10 @@ export default function PreferencesSettingsPage() {
245245
<div className="flex items-center justify-between">
246246
<div className="space-y-1">
247247
<Label className="text-base font-medium text-foreground">
248-
Auto-dictate on new note
248+
{t("settings.preferences.autoDictateOnNewNote.label")}
249249
</Label>
250250
<p className="text-xs text-muted-foreground">
251-
Automatically start dictation when creating a new note
251+
{t("settings.preferences.autoDictateOnNewNote.description")}
252252
</p>
253253
</div>
254254
<Switch

apps/desktop/src/renderer/main/providers/sync-provider.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ import { toast } from "sonner";
33

44
export interface SyncProviderConfig {
55
noteId: number;
6+
onSaveError?: () => void;
67
}
78

89
export class NoteSyncProvider {
910
private ydoc: Y.Doc;
1011
private text: Y.Text;
1112
private noteId: number;
13+
private onSaveError: (() => void) | undefined;
1214
private destroyed = false;
1315
private updateHandler: ((update: Uint8Array, origin: unknown) => void) | null =
1416
null;
1517

1618
constructor(config: SyncProviderConfig) {
1719
this.noteId = config.noteId;
20+
this.onSaveError = config.onSaveError;
1821

1922
// Initialize Y.Doc and Y.Text
2023
this.ydoc = new Y.Doc();
@@ -44,7 +47,11 @@ export class NoteSyncProvider {
4447
} catch (error) {
4548
if (!this.destroyed) {
4649
console.error("Failed to save yjs update:", error);
47-
toast.error("Failed to save changes");
50+
if (this.onSaveError) {
51+
this.onSaveError();
52+
} else {
53+
toast.error("Failed to save changes");
54+
}
4855
}
4956
}
5057
};

0 commit comments

Comments
 (0)