\n")
sb.WriteString("
\n")
- sb.WriteString("
\n")
sb.WriteString(" ⭐ Favoris
\n") + sb.WriteString("⭐ " + h.t(r, "favorites.title") + "
\n") sb.WriteString("\n")
sb.WriteString("
\n")
@@ -423,7 +433,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
} else {
// Fichier
sb.WriteString(fmt.Sprintf("
\n"))
- sb.WriteString(fmt.Sprintf(" ", fav.Path))
+ sb.WriteString(fmt.Sprintf(" ", fav.Path))
sb.WriteString(fmt.Sprintf("📄 %s", fav.Title))
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf("
\n"))
@@ -436,7 +446,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
}
// generateRecentNotesSection génère la section des notes récemment modifiées
-func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
+func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Request) {
recentDocs := h.idx.GetRecentDocuments(5)
if len(recentDocs) == 0 {
@@ -445,7 +455,7 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
sb.WriteString("\n")
sb.WriteString("
\n")
- sb.WriteString("
\n")
sb.WriteString(" 🕒 Récemment modifiés
\n") + sb.WriteString("🕒 " + h.t(r, "home.recentlyModified") + "
\n") sb.WriteString("\n")
sb.WriteString("
\n")
@@ -464,7 +474,7 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
}
sb.WriteString("
\n")
- sb.WriteString(fmt.Sprintf(" \n", doc.Path))
+ sb.WriteString(fmt.Sprintf(" \n", doc.Path))
sb.WriteString(fmt.Sprintf("
Fermer
@@ -467,7 +503,7 @@
%s
\n", doc.Title))
sb.WriteString(fmt.Sprintf(" \n"))
sb.WriteString(fmt.Sprintf(" 📅 %s\n", dateStr))
@@ -527,7 +537,7 @@ func (h *Handler) generateFavoriteFolderContent(sb *strings.Builder, folderPath
// Fichier markdown
displayName := strings.TrimSuffix(name, ".md")
sb.WriteString(fmt.Sprintf("%s
+
+
+ \n", indent, indentClass))
- sb.WriteString(fmt.Sprintf("%s ", indent, relativePath))
+ sb.WriteString(fmt.Sprintf("%s ", indent, relativePath))
sb.WriteString(fmt.Sprintf("📄 %s", displayName))
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf("%s
\n", indent))
@@ -1484,3 +1494,60 @@ func (h *Handler) generateFolderViewMarkdown(folderPath string) string {
return sb.String()
}
+
+// getLanguage extrait la langue préférée depuis les cookies ou Accept-Language header
+func (h *Handler) getLanguage(r *http.Request) string {
+ // 1. Vérifier le cookie
+ if cookie, err := r.Cookie("language"); err == nil && cookie.Value != "" {
+ return cookie.Value
+ }
+
+ // 2. Vérifier l'en-tête Accept-Language
+ acceptLang := r.Header.Get("Accept-Language")
+ if acceptLang != "" {
+ // Parse simple: prendre le premier code de langue
+ parts := strings.Split(acceptLang, ",")
+ if len(parts) > 0 {
+ lang := strings.Split(parts[0], ";")[0]
+ lang = strings.Split(lang, "-")[0] // "fr-FR" -> "fr"
+ return strings.TrimSpace(lang)
+ }
+ }
+
+ // 3. Par défaut: anglais
+ return "en"
+}
+
+// t est un helper pour traduire une clé dans la langue de la requête
+func (h *Handler) t(r *http.Request, key string, args ...map[string]string) string {
+ lang := h.getLanguage(r)
+ return h.i18n.T(lang, key, args...)
+}
+
+// handleI18n sert les fichiers de traduction JSON pour le frontend
+func (h *Handler) handleI18n(w http.ResponseWriter, r *http.Request) {
+ // Extraire le code de langue depuis l'URL: /api/i18n/en ou /api/i18n/fr
+ lang := strings.TrimPrefix(r.URL.Path, "/api/i18n/")
+ if lang == "" {
+ lang = "en"
+ }
+
+ // Récupérer les traductions pour cette langue
+ translations, ok := h.i18n.GetTranslations(lang)
+ if !ok {
+ // Fallback vers l'anglais si la langue n'existe pas
+ translations, ok = h.i18n.GetTranslations("en")
+ if !ok {
+ http.Error(w, "translations not found", http.StatusNotFound)
+ return
+ }
+ }
+
+ // Retourner le JSON
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(translations); err != nil {
+ h.logger.Printf("error encoding translations: %v", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/internal/api/handler_test.go b/internal/api/handler_test.go
index 68a9948..9fc458c 100644
--- a/internal/api/handler_test.go
+++ b/internal/api/handler_test.go
@@ -11,6 +11,7 @@ import (
"strings"
"testing"
+ "github.com/mathieu/personotes/internal/i18n"
"github.com/mathieu/personotes/internal/indexer"
)
@@ -32,7 +33,10 @@ func newTestHandler(t *testing.T, notesDir string) *Handler {
t.Fatalf("impossible d'analyser les templates de test: %v", err)
}
- return NewHandler(notesDir, indexer.New(), tpl, log.New(io.Discard, "", 0))
+ // Create a minimal translator for tests
+ translator := i18n.New("en")
+
+ return NewHandler(notesDir, indexer.New(), tpl, log.New(io.Discard, "", 0), translator)
}
func TestHandler_Search(t *testing.T) {
diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go
new file mode 100644
index 0000000..7317cd8
--- /dev/null
+++ b/internal/i18n/i18n.go
@@ -0,0 +1,139 @@
+package i18n
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+)
+
+// Translator manages translations for multiple languages
+type Translator struct {
+ translations map[string]map[string]interface{}
+ mu sync.RWMutex
+ defaultLang string
+}
+
+// New creates a new Translator with the specified default language
+func New(defaultLang string) *Translator {
+ t := &Translator{
+ translations: make(map[string]map[string]interface{}),
+ defaultLang: defaultLang,
+ }
+ return t
+}
+
+// LoadFromDir loads all translation files from a directory
+func (t *Translator) LoadFromDir(dir string) error {
+ files, err := filepath.Glob(filepath.Join(dir, "*.json"))
+ if err != nil {
+ return fmt.Errorf("failed to list translation files: %w", err)
+ }
+
+ for _, file := range files {
+ lang := strings.TrimSuffix(filepath.Base(file), ".json")
+ if err := t.LoadLanguage(lang, file); err != nil {
+ return fmt.Errorf("failed to load language %s: %w", lang, err)
+ }
+ }
+
+ return nil
+}
+
+// LoadLanguage loads translations for a specific language from a JSON file
+func (t *Translator) LoadLanguage(lang, filePath string) error {
+ data, err := os.ReadFile(filePath)
+ if err != nil {
+ return fmt.Errorf("failed to read translation file: %w", err)
+ }
+
+ var translations map[string]interface{}
+ if err := json.Unmarshal(data, &translations); err != nil {
+ return fmt.Errorf("failed to parse translation file: %w", err)
+ }
+
+ t.mu.Lock()
+ t.translations[lang] = translations
+ t.mu.Unlock()
+
+ return nil
+}
+
+// T translates a key for the given language with optional arguments
+// Key format: "section.subsection.key" (e.g., "menu.home")
+// Arguments can be passed as a map for variable interpolation
+func (t *Translator) T(lang, key string, args ...map[string]string) string {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ // Try to get translation for specified language
+ translation := t.getTranslation(lang, key)
+
+ // Fallback to default language if not found
+ if translation == "" && lang != t.defaultLang {
+ translation = t.getTranslation(t.defaultLang, key)
+ }
+
+ // Return key if no translation found
+ if translation == "" {
+ return key
+ }
+
+ // Interpolate variables if args provided
+ if len(args) > 0 && args[0] != nil {
+ for k, v := range args[0] {
+ placeholder := fmt.Sprintf("{{%s}}", k)
+ translation = strings.ReplaceAll(translation, placeholder, v)
+ }
+ }
+
+ return translation
+}
+
+// getTranslation retrieves a translation by key using dot notation
+func (t *Translator) getTranslation(lang, key string) string {
+ langTranslations, ok := t.translations[lang]
+ if !ok {
+ return ""
+ }
+
+ parts := strings.Split(key, ".")
+ var current interface{} = langTranslations
+
+ for _, part := range parts {
+ if m, ok := current.(map[string]interface{}); ok {
+ current = m[part]
+ } else {
+ return ""
+ }
+ }
+
+ if str, ok := current.(string); ok {
+ return str
+ }
+
+ return ""
+}
+
+// GetAvailableLanguages returns a list of loaded languages
+func (t *Translator) GetAvailableLanguages() []string {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ langs := make([]string, 0, len(t.translations))
+ for lang := range t.translations {
+ langs = append(langs, lang)
+ }
+ return langs
+}
+
+// GetTranslations returns all translations for a specific language
+func (t *Translator) GetTranslations(lang string) (map[string]interface{}, bool) {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ translations, ok := t.translations[lang]
+ return translations, ok
+}
diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go
new file mode 100644
index 0000000..3b67a98
--- /dev/null
+++ b/internal/i18n/i18n_test.go
@@ -0,0 +1,123 @@
+package i18n
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestTranslator(t *testing.T) {
+ // Create temporary test translations
+ tmpDir := t.TempDir()
+
+ enFile := filepath.Join(tmpDir, "en.json")
+ enContent := `{
+ "menu": {
+ "home": "Home",
+ "search": "Search"
+ },
+ "editor": {
+ "confirmDelete": "Are you sure you want to delete {{filename}}?"
+ }
+ }`
+ if err := os.WriteFile(enFile, []byte(enContent), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ frFile := filepath.Join(tmpDir, "fr.json")
+ frContent := `{
+ "menu": {
+ "home": "Accueil",
+ "search": "Rechercher"
+ },
+ "editor": {
+ "confirmDelete": "Êtes-vous sûr de vouloir supprimer {{filename}} ?"
+ }
+ }`
+ if err := os.WriteFile(frFile, []byte(frContent), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ // Test translator
+ trans := New("en")
+ if err := trans.LoadFromDir(tmpDir); err != nil {
+ t.Fatalf("Failed to load translations: %v", err)
+ }
+
+ tests := []struct {
+ name string
+ lang string
+ key string
+ args map[string]string
+ expected string
+ }{
+ {
+ name: "English simple key",
+ lang: "en",
+ key: "menu.home",
+ expected: "Home",
+ },
+ {
+ name: "French simple key",
+ lang: "fr",
+ key: "menu.search",
+ expected: "Rechercher",
+ },
+ {
+ name: "English with interpolation",
+ lang: "en",
+ key: "editor.confirmDelete",
+ args: map[string]string{"filename": "test.md"},
+ expected: "Are you sure you want to delete test.md?",
+ },
+ {
+ name: "French with interpolation",
+ lang: "fr",
+ key: "editor.confirmDelete",
+ args: map[string]string{"filename": "test.md"},
+ expected: "Êtes-vous sûr de vouloir supprimer test.md ?",
+ },
+ {
+ name: "Missing key returns key",
+ lang: "en",
+ key: "missing.key",
+ expected: "missing.key",
+ },
+ {
+ name: "Fallback to default language",
+ lang: "es", // Spanish not loaded, should fallback to English
+ key: "menu.home",
+ expected: "Home",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var result string
+ if tt.args != nil {
+ result = trans.T(tt.lang, tt.key, tt.args)
+ } else {
+ result = trans.T(tt.lang, tt.key)
+ }
+
+ if result != tt.expected {
+ t.Errorf("T(%s, %s) = %s, want %s", tt.lang, tt.key, result, tt.expected)
+ }
+ })
+ }
+
+ // Test GetAvailableLanguages
+ langs := trans.GetAvailableLanguages()
+ if len(langs) != 2 {
+ t.Errorf("Expected 2 languages, got %d", len(langs))
+ }
+
+ // Test GetTranslations
+ enTrans, ok := trans.GetTranslations("en")
+ if !ok {
+ t.Error("Expected to find English translations")
+ }
+ if enTrans == nil {
+ t.Error("English translations should not be nil")
+ }
+}
diff --git a/locales/README.md b/locales/README.md
new file mode 100644
index 0000000..82e1eac
--- /dev/null
+++ b/locales/README.md
@@ -0,0 +1,98 @@
+# Localization Files
+
+This directory contains translation files for the Personotes application.
+
+## Available Languages
+
+- **English** (`en.json`) - Default language
+- **Français** (`fr.json`) - French translation
+
+## File Structure
+
+Each language file is a JSON file with nested keys for organizing translations:
+
+```json
+{
+ "app": {
+ "name": "Personotes",
+ "tagline": "Simple Markdown note-taking"
+ },
+ "menu": {
+ "home": "Home",
+ "search": "Search"
+ },
+ "errors": {
+ "internalError": "Internal error"
+ }
+}
+```
+
+## Adding a New Language
+
+To add support for a new language:
+
+1. **Create a new JSON file** named with the language code (e.g., `es.json` for Spanish, `de.json` for German)
+2. **Copy the structure** from `en.json`
+3. **Translate all strings** to the target language
+4. **Keep placeholders intact**: Use `{{variable}}` syntax as-is (e.g., `{{filename}}`, `{{date}}`)
+5. **Test your translation** by setting the language in the application
+
+### Language Codes
+
+Use standard ISO 639-1 codes:
+- `en` - English
+- `fr` - Français (French)
+- `es` - Español (Spanish)
+- `de` - Deutsch (German)
+- `it` - Italiano (Italian)
+- `pt` - Português (Portuguese)
+- `ja` - 日本語 (Japanese)
+- `zh` - 中文 (Chinese)
+
+## Variable Interpolation
+
+Some strings contain variables in the format `{{variableName}}`. Keep these exactly as they are:
+
+```json
+{
+ "editor": {
+ "confirmDelete": "Are you sure you want to delete this note ({{filename}})?"
+ }
+}
+```
+
+In French:
+```json
+{
+ "editor": {
+ "confirmDelete": "Êtes-vous sûr de vouloir supprimer cette note ({{filename}}) ?"
+ }
+}
+```
+
+## Guidelines for Translators
+
+1. **Consistency**: Use consistent terminology throughout
+2. **Context**: Consider the UI context (button labels should be short, help text can be longer)
+3. **Formality**: Match the tone of the original language
+4. **Special Characters**: Ensure proper encoding for special characters
+5. **Testing**: Test in the actual application to see how translations fit in the UI
+
+## Contributing
+
+To contribute a new translation:
+
+1. Fork the repository
+2. Create your translation file (e.g., `locales/es.json`)
+3. Add the language to `languages` section in your file:
+ ```json
+ "languages": {
+ "en": "English",
+ "fr": "Français",
+ "es": "Español"
+ }
+ ```
+4. Update this README with your language
+5. Submit a pull request
+
+Thank you for helping make Personotes accessible to more users! 🌍
diff --git a/locales/en.json b/locales/en.json
new file mode 100644
index 0000000..3721666
--- /dev/null
+++ b/locales/en.json
@@ -0,0 +1,264 @@
+{
+ "app": {
+ "name": "Personotes",
+ "tagline": "Simple Markdown note-taking"
+ },
+ "menu": {
+ "home": "Home",
+ "newNote": "New Note",
+ "newFolder": "New Folder",
+ "search": "Search",
+ "settings": "Settings",
+ "about": "About",
+ "favorites": "Pinned Notes",
+ "daily": "Daily Notes"
+ },
+ "editor": {
+ "save": "Save",
+ "saving": "Saving...",
+ "saved": "Saved",
+ "autoSaved": "Auto-saved",
+ "delete": "Delete",
+ "confirmDelete": "Are you sure you want to delete this note ({{filename}})?",
+ "backlinks": "Backlinks",
+ "noBacklinks": "No backlinks",
+ "tags": "Tags",
+ "lastModified": "Last modified",
+ "splitView": "Split View",
+ "editorOnly": "Editor Only",
+ "previewOnly": "Preview Only",
+ "refresh": "Refresh",
+ "togglePreview": "Mode: Editor + Preview (click for Editor only)"
+ },
+ "fileTree": {
+ "notes": "Notes",
+ "noNotes": "No notes found.",
+ "newFolder": "New Folder",
+ "createNote": "Create Note",
+ "createFolder": "Create Folder",
+ "noteName": "Note name",
+ "noteNamePlaceholder": "my-note.md",
+ "noteNameLabel": "Name of the new note (e.g., my-super-note.md)",
+ "folderName": "Folder name",
+ "folderNamePlaceholder": "my-folder",
+ "cancel": "Cancel",
+ "create": "Create",
+ "createTheNote": "Create the note",
+ "createTheFolder": "Create the folder",
+ "selectAll": "Select All",
+ "deselectAll": "Deselect All",
+ "deleteSelected": "Delete Selected",
+ "confirmDeleteMultiple": "Are you sure you want to delete the selected items?"
+ },
+ "search": {
+ "title": "Search",
+ "placeholder": "Search notes (keyword, tag:project, title:...)",
+ "noResults": "No results found",
+ "searchHelp": "💡 Advanced search",
+ "searchHelpText": "Enter keywords to search in your notes",
+ "byTag": "Search by tag",
+ "byTagExample": "tag:project",
+ "byTitle": "Search in titles",
+ "byTitleExample": "title:meeting",
+ "byPath": "Search in paths",
+ "byPathExample": "path:backend",
+ "quotedPhrase": "Exact phrase",
+ "quotedPhraseExample": "\"exact phrase\""
+ },
+ "daily": {
+ "title": "Daily Notes",
+ "recent": "Recent",
+ "calendar": "Calendar",
+ "noRecent": "No recent notes",
+ "noteOf": "Note of {{date}}",
+ "noNote": "{{date}} - No note",
+ "openToday": "Open today's note (Ctrl/Cmd+D)",
+ "previousMonth": "Previous month",
+ "nextMonth": "Next month"
+ },
+ "favorites": {
+ "title": "bookmarks",
+ "noFavorites": "No bookmarks yet",
+ "add": "Add to bookmarks",
+ "remove": "Remove from bookmarks",
+ "alreadyInFavorites": "Already in bookmarks",
+ "notFound": "Bookmark not found"
+ },
+ "settings": {
+ "title": "Settings",
+ "theme": "Theme",
+ "font": "Font",
+ "fontSize": "Font Size",
+ "vimMode": "Vim Mode",
+ "language": "Language",
+ "appearance": "Appearance",
+ "editor": "Editor",
+ "other": "Other",
+ "apply": "Apply",
+ "close": "Close",
+ "fontSizeSmall": "Small",
+ "fontSizeMedium": "Medium",
+ "fontSizeLarge": "Large",
+ "fontSizeExtraLarge": "Extra Large"
+ },
+ "tabs": {
+ "themes": "Themes",
+ "fonts": "Fonts",
+ "shortcuts": "Shortcuts",
+ "other": "Other"
+ },
+ "newNoteModal": {
+ "title": "New Note",
+ "label": "Note name",
+ "placeholder": "my-note.md",
+ "create": "Create / Open",
+ "cancel": "Cancel"
+ },
+ "newFolderModal": {
+ "title": "New Folder",
+ "label": "Folder name",
+ "placeholder": "my-folder",
+ "create": "Create",
+ "cancel": "Cancel"
+ },
+ "selectionToolbar": {
+ "delete": "Delete",
+ "cancel": "Cancel"
+ },
+ "sidebar": {
+ "files": "Files",
+ "favorites": "Bookmarks",
+ "daily": "Daily Notes",
+ "search": "Search"
+ },
+ "themes": {
+ "materialDark": "Material Dark",
+ "monokai": "Monokai",
+ "dracula": "Dracula",
+ "oneDark": "One Dark",
+ "solarizedDark": "Solarized Dark",
+ "nord": "Nord",
+ "catppuccin": "Catppuccin",
+ "everforest": "Everforest"
+ },
+ "fonts": {
+ "jetbrainsMono": "JetBrains Mono",
+ "firaCode": "Fira Code",
+ "inter": "Inter",
+ "ibmPlexMono": "IBM Plex Mono",
+ "sourceCodePro": "Source Code Pro",
+ "cascadiaCode": "Cascadia Code",
+ "robotoMono": "Roboto Mono",
+ "ubuntuMono": "Ubuntu Mono"
+ },
+ "languages": {
+ "en": "English",
+ "fr": "Français"
+ },
+ "shortcuts": {
+ "title": "Keyboard Shortcuts",
+ "save": "Save note",
+ "search": "Open search",
+ "daily": "Create/open today's note",
+ "sidebar": "Toggle sidebar",
+ "help": "Show this help",
+ "newNote": "New note",
+ "close": "Close"
+ },
+ "errors": {
+ "methodNotAllowed": "Method not allowed",
+ "internalError": "Internal error",
+ "renderError": "Render error",
+ "invalidForm": "Invalid form",
+ "pathRequired": "Path required",
+ "fileNotFound": "File/folder not found",
+ "loadError": "Loading error",
+ "saveError": "Save error",
+ "deleteError": "Delete error",
+ "alreadyExists": "A note with this name already exists",
+ "invalidPath": "Invalid path",
+ "invalidFilename": "Invalid filename",
+ "invalidName": "Invalid name. Avoid \\ and .. characters",
+ "invalidFolderName": "Invalid folder name. Avoid \\ and .. characters",
+ "enterNoteName": "Please enter a note name",
+ "enterFolderName": "Please enter a folder name",
+ "moveFailed": "Failed to move file",
+ "createFolderFailed": "Failed to create folder",
+ "nothingSelected": "Nothing selected",
+ "cannotMoveIntoSelf": "Cannot move a folder into itself or into one of its subfolders",
+ "jsonInvalid": "Invalid JSON",
+ "readRequestError": "Error reading request",
+ "parseRequestError": "Error parsing request",
+ "formReadError": "Cannot read form",
+ "filenameMissing": "Filename missing",
+ "frontMatterError": "Error generating front matter"
+ },
+ "vim": {
+ "notAvailable": "❌ Vim mode is not available.\n\nThe @replit/codemirror-vim package is not installed.\n\nTo install it, run:\ncd frontend\nnpm install\nnpm run build",
+ "enabled": "Vim mode enabled",
+ "disabled": "Vim mode disabled"
+ },
+ "slashCommands": {
+ "h1": "Heading 1",
+ "h2": "Heading 2",
+ "h3": "Heading 3",
+ "bold": "Bold text",
+ "italic": "Italic text",
+ "code": "Inline code",
+ "codeblock": "Code block",
+ "quote": "Quote",
+ "list": "Bullet list",
+ "hr": "Horizontal rule",
+ "table": "Table",
+ "link": "Link",
+ "ilink": "Internal link",
+ "date": "Insert date"
+ },
+ "about": {
+ "title": "About Personotes",
+ "version": "Version",
+ "description": "A lightweight web-based Markdown note-taking application",
+ "features": "Features",
+ "github": "GitHub",
+ "documentation": "Documentation"
+ },
+ "home": {
+ "autoUpdate": "Auto-update",
+ "allNotes": "All notes",
+ "recentlyModified": "Recently modified"
+ },
+ "calendar": {
+ "monday": "Monday",
+ "tuesday": "Tuesday",
+ "wednesday": "Wednesday",
+ "thursday": "Thursday",
+ "friday": "Friday",
+ "saturday": "Saturday",
+ "sunday": "Sunday",
+ "mon": "Mon",
+ "tue": "Tue",
+ "wed": "Wed",
+ "thu": "Thu",
+ "fri": "Fri",
+ "sat": "Sat",
+ "sun": "Sun",
+ "january": "January",
+ "february": "February",
+ "march": "March",
+ "april": "April",
+ "may": "May",
+ "june": "June",
+ "july": "July",
+ "august": "August",
+ "september": "September",
+ "october": "October",
+ "november": "November",
+ "december": "December",
+ "today": "Today",
+ "thisMonth": "This month",
+ "prevMonth": "Previous month",
+ "nextMonth": "Next month",
+ "noNote": "No note",
+ "noteOf": "Note of"
+ }
+}
diff --git a/locales/fr.json b/locales/fr.json
new file mode 100644
index 0000000..3dc7fcd
--- /dev/null
+++ b/locales/fr.json
@@ -0,0 +1,264 @@
+{
+ "app": {
+ "name": "Personotes",
+ "tagline": "Prise de notes Markdown simple"
+ },
+ "menu": {
+ "home": "Accueil",
+ "newNote": "Nouvelle Note",
+ "newFolder": "Nouveau Dossier",
+ "search": "Rechercher",
+ "settings": "Paramètres",
+ "about": "À propos",
+ "favorites": "Favoris",
+ "daily": "Notes Quotidiennes"
+ },
+ "editor": {
+ "save": "Enregistrer",
+ "saving": "Sauvegarde...",
+ "saved": "Sauvegardé",
+ "autoSaved": "Auto-sauvegardé",
+ "delete": "Supprimer",
+ "confirmDelete": "Êtes-vous sûr de vouloir supprimer cette note ({{filename}}) ?",
+ "backlinks": "Rétroliens",
+ "noBacklinks": "Aucun rétrolien",
+ "tags": "Tags",
+ "lastModified": "Dernière modification",
+ "splitView": "Vue divisée",
+ "editorOnly": "Éditeur seul",
+ "previewOnly": "Aperçu seul",
+ "refresh": "Actualiser",
+ "togglePreview": "Mode: Éditeur + Preview (cliquer pour Éditeur seul)"
+ },
+ "fileTree": {
+ "notes": "Notes",
+ "noNotes": "Aucune note trouvée.",
+ "newFolder": "Nouveau Dossier",
+ "createNote": "Créer une Note",
+ "createFolder": "Créer un Dossier",
+ "noteName": "Nom de la note",
+ "noteNamePlaceholder": "ma-note.md",
+ "noteNameLabel": "Nom de la nouvelle note (ex: ma-super-note.md)",
+ "folderName": "Nom du dossier",
+ "folderNamePlaceholder": "mon-dossier",
+ "cancel": "Annuler",
+ "create": "Créer",
+ "createTheNote": "Créer la note",
+ "createTheFolder": "Créer le dossier",
+ "selectAll": "Tout sélectionner",
+ "deselectAll": "Tout désélectionner",
+ "deleteSelected": "Supprimer la sélection",
+ "confirmDeleteMultiple": "Êtes-vous sûr de vouloir supprimer les éléments sélectionnés ?"
+ },
+ "search": {
+ "title": "Recherche",
+ "placeholder": "Rechercher une note (mot-clé, tag:projet, title:...)",
+ "noResults": "Aucun résultat trouvé",
+ "searchHelp": "💡 Recherche avancée",
+ "searchHelpText": "Saisissez des mots-clés pour rechercher dans vos notes",
+ "byTag": "Rechercher par tag",
+ "byTagExample": "tag:projet",
+ "byTitle": "Rechercher dans les titres",
+ "byTitleExample": "title:réunion",
+ "byPath": "Rechercher dans les chemins",
+ "byPathExample": "path:backend",
+ "quotedPhrase": "Phrase exacte",
+ "quotedPhraseExample": "\"phrase exacte\""
+ },
+ "daily": {
+ "title": "Notes Quotidiennes",
+ "recent": "Récentes",
+ "calendar": "Calendrier",
+ "noRecent": "Aucune note récente",
+ "noteOf": "Note du {{date}}",
+ "noNote": "{{date}} - Pas de note",
+ "openToday": "Ouvrir la note du jour (Ctrl/Cmd+D)",
+ "previousMonth": "Mois précédent",
+ "nextMonth": "Mois suivant"
+ },
+ "favorites": {
+ "title": "Favoris",
+ "noFavorites": "Aucun favori pour le moment",
+ "add": "Ajouter aux favoris",
+ "remove": "Retirer des favoris",
+ "alreadyInFavorites": "Déjà en favoris",
+ "notFound": "Favori introuvable"
+ },
+ "settings": {
+ "title": "Paramètres",
+ "theme": "Thème",
+ "font": "Police",
+ "fontSize": "Taille de police",
+ "vimMode": "Mode Vim",
+ "language": "Langue",
+ "appearance": "Apparence",
+ "editor": "Éditeur",
+ "other": "Autre",
+ "apply": "Appliquer",
+ "close": "Fermer",
+ "fontSizeSmall": "Petite",
+ "fontSizeMedium": "Moyenne",
+ "fontSizeLarge": "Grande",
+ "fontSizeExtraLarge": "Très Grande"
+ },
+ "tabs": {
+ "themes": "Thèmes",
+ "fonts": "Polices",
+ "shortcuts": "Raccourcis",
+ "other": "Autre"
+ },
+ "newNoteModal": {
+ "title": "Nouvelle Note",
+ "label": "Nom de la note",
+ "placeholder": "ma-note.md",
+ "create": "Créer / Ouvrir",
+ "cancel": "Annuler"
+ },
+ "newFolderModal": {
+ "title": "Nouveau Dossier",
+ "label": "Nom du dossier",
+ "placeholder": "mon-dossier",
+ "create": "Créer",
+ "cancel": "Annuler"
+ },
+ "selectionToolbar": {
+ "delete": "Supprimer",
+ "cancel": "Annuler"
+ },
+ "sidebar": {
+ "files": "Fichiers",
+ "favorites": "Favoris",
+ "daily": "Notes Quotidiennes",
+ "search": "Recherche"
+ },
+ "themes": {
+ "materialDark": "Material Dark",
+ "monokai": "Monokai",
+ "dracula": "Dracula",
+ "oneDark": "One Dark",
+ "solarizedDark": "Solarized Dark",
+ "nord": "Nord",
+ "catppuccin": "Catppuccin",
+ "everforest": "Everforest"
+ },
+ "fonts": {
+ "jetbrainsMono": "JetBrains Mono",
+ "firaCode": "Fira Code",
+ "inter": "Inter",
+ "ibmPlexMono": "IBM Plex Mono",
+ "sourceCodePro": "Source Code Pro",
+ "cascadiaCode": "Cascadia Code",
+ "robotoMono": "Roboto Mono",
+ "ubuntuMono": "Ubuntu Mono"
+ },
+ "languages": {
+ "en": "English",
+ "fr": "Français"
+ },
+ "shortcuts": {
+ "title": "Raccourcis Clavier",
+ "save": "Sauvegarder la note",
+ "search": "Ouvrir la recherche",
+ "daily": "Créer/ouvrir la note du jour",
+ "sidebar": "Basculer la barre latérale",
+ "help": "Afficher cette aide",
+ "newNote": "Nouvelle note",
+ "close": "Fermer"
+ },
+ "errors": {
+ "methodNotAllowed": "Méthode non autorisée",
+ "internalError": "Erreur interne",
+ "renderError": "Erreur de rendu",
+ "invalidForm": "Formulaire invalide",
+ "pathRequired": "Chemin requis",
+ "fileNotFound": "Fichier/dossier introuvable",
+ "loadError": "Erreur de chargement",
+ "saveError": "Erreur de sauvegarde",
+ "deleteError": "Erreur de suppression",
+ "alreadyExists": "Une note avec ce nom existe déjà",
+ "invalidPath": "Chemin invalide",
+ "invalidFilename": "Nom de fichier invalide",
+ "invalidName": "Nom invalide. Évitez les caractères \\ et ..",
+ "invalidFolderName": "Nom de dossier invalide. Évitez les caractères \\ et ..",
+ "enterNoteName": "Veuillez entrer un nom de note",
+ "enterFolderName": "Veuillez entrer un nom de dossier",
+ "moveFailed": "Erreur lors du déplacement du fichier",
+ "createFolderFailed": "Erreur lors de la création du dossier",
+ "nothingSelected": "Aucun élément sélectionné",
+ "cannotMoveIntoSelf": "Impossible de déplacer un dossier dans lui-même ou dans un de ses sous-dossiers",
+ "jsonInvalid": "JSON invalide",
+ "readRequestError": "Erreur lecture requête",
+ "parseRequestError": "Erreur parsing requête",
+ "formReadError": "Lecture du formulaire impossible",
+ "filenameMissing": "Nom de fichier manquant",
+ "frontMatterError": "Erreur lors de la génération du front matter"
+ },
+ "vim": {
+ "notAvailable": "❌ Le mode Vim n'est pas disponible.\n\nLe package @replit/codemirror-vim n'est pas installé.\n\nPour l'installer, exécutez :\ncd frontend\nnpm install\nnpm run build",
+ "enabled": "Mode Vim activé",
+ "disabled": "Mode Vim désactivé"
+ },
+ "slashCommands": {
+ "h1": "Titre 1",
+ "h2": "Titre 2",
+ "h3": "Titre 3",
+ "bold": "Texte en gras",
+ "italic": "Texte en italique",
+ "code": "Code en ligne",
+ "codeblock": "Bloc de code",
+ "quote": "Citation",
+ "list": "Liste à puces",
+ "hr": "Ligne horizontale",
+ "table": "Tableau",
+ "link": "Lien",
+ "ilink": "Lien interne",
+ "date": "Insérer la date"
+ },
+ "about": {
+ "title": "À propos de Personotes",
+ "version": "Version",
+ "description": "Application légère de prise de notes Markdown",
+ "features": "Fonctionnalités",
+ "github": "GitHub",
+ "documentation": "Documentation"
+ },
+ "home": {
+ "autoUpdate": "Mise à jour automatique",
+ "allNotes": "Toutes les notes",
+ "recentlyModified": "Récemment modifiés"
+ },
+ "calendar": {
+ "monday": "Lundi",
+ "tuesday": "Mardi",
+ "wednesday": "Mercredi",
+ "thursday": "Jeudi",
+ "friday": "Vendredi",
+ "saturday": "Samedi",
+ "sunday": "Dimanche",
+ "mon": "Lun",
+ "tue": "Mar",
+ "wed": "Mer",
+ "thu": "Jeu",
+ "fri": "Ven",
+ "sat": "Sam",
+ "sun": "Dim",
+ "january": "Janvier",
+ "february": "Février",
+ "march": "Mars",
+ "april": "Avril",
+ "may": "Mai",
+ "june": "Juin",
+ "july": "Juillet",
+ "august": "Août",
+ "september": "Septembre",
+ "october": "Octobre",
+ "november": "Novembre",
+ "december": "Décembre",
+ "today": "Aujourd'hui",
+ "thisMonth": "Ce mois",
+ "prevMonth": "Mois précédent",
+ "nextMonth": "Mois suivant",
+ "noNote": "Pas de note",
+ "noteOf": "Note du"
+ }
+}
diff --git a/notes/personal/learning-goals.md b/notes/personal/learning-goals.md
index 125d7d9..8de2d0b 100644
--- a/notes/personal/learning-goals.md
+++ b/notes/personal/learning-goals.md
@@ -1,7 +1,7 @@
---
title: 2025 Learning Goals
date: 10-11-2025
-last_modified: 12-11-2025:10:28
+last_modified: 12-11-2025:20:55
tags:
- personal
- learning
@@ -11,7 +11,7 @@ tags:
## Technical
-- [ ] Master Go concurrency patterns
+- [x] Master Go concurrency patterns
- [ ] Learn Rust basics
- [ ] Deep dive into databases
- [ ] System design courses
diff --git a/notes/test-delete-2.md b/notes/test-delete-2.md
index 398fc69..5df6305 100644
--- a/notes/test-delete-2.md
+++ b/notes/test-delete-2.md
@@ -1,8 +1,10 @@
---
title: Test Delete 2
date: 11-11-2025
-last_modified: 11-11-2025:17:15
+last_modified: 12-11-2025:20:42
---
test file 2
This is the Vim Mode
+
+[Go Performance Optimization](research/tech/go-performance.md)
\ No newline at end of file
diff --git a/start.sh b/start.sh
new file mode 100644
index 0000000..e1bd42c
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+
+# Personotes - Startup Script
+# Ce script construit le frontend et démarre le serveur
+
+set -e # Exit on error
+
+echo "🚀 Personotes Startup"
+echo "===================="
+echo ""
+
+# Check if npm is installed
+if ! command -v npm &> /dev/null; then
+ echo "❌ npm n'est pas installé"
+ echo " Installez Node.js depuis https://nodejs.org/"
+ exit 1
+fi
+
+# Check if go is installed
+if ! command -v go &> /dev/null; then
+ echo "❌ Go n'est pas installé"
+ echo " Installez Go depuis https://go.dev/doc/install"
+ exit 1
+fi
+
+# Build frontend
+echo "📦 Building frontend..."
+cd frontend
+
+if [ ! -d "node_modules" ]; then
+ echo " Installing dependencies..."
+ npm install
+fi
+
+echo " Compiling JavaScript modules..."
+npm run build
+
+cd ..
+
+echo "✅ Frontend built successfully"
+echo ""
+
+# Start server
+echo "🔥 Starting server..."
+echo " Server will be available at: http://localhost:8080"
+echo ""
+echo " Available languages:"
+echo " - 🇬🇧 English (EN)"
+echo " - 🇫🇷 Français (FR)"
+echo ""
+echo " Change language: Settings > Autre"
+echo ""
+echo " Press Ctrl+C to stop"
+echo ""
+
+go run ./cmd/server
diff --git a/templates/daily-calendar.html b/templates/daily-calendar.html
index abe1359..4e71df2 100644
--- a/templates/daily-calendar.html
+++ b/templates/daily-calendar.html
@@ -4,6 +4,7 @@
hx-get="/api/daily/calendar/{{.PrevMonth}}"
hx-target="#daily-calendar"
hx-swap="outerHTML"
+ data-i18n-title="calendar.prevMonth"
title="Mois précédent">
‹
@@ -12,6 +13,7 @@
hx-get="/api/daily/calendar/{{.NextMonth}}"
hx-target="#daily-calendar"
hx-swap="outerHTML"
+ data-i18n-title="calendar.nextMonth"
title="Mois suivant">
›
@@ -52,6 +54,7 @@
hx-target="#editor-container"
hx-swap="innerHTML"
hx-push-url="true"
+ data-i18n="calendar.today"
title="Ouvrir la note du jour (Ctrl/Cmd+D)"
style="flex: 1; padding: 0.5rem; font-size: 0.85rem;">
📅 Aujourd'hui
@@ -60,6 +63,7 @@
hx-get="/api/daily/calendar/{{.CurrentMonth}}"
hx-target="#daily-calendar"
hx-swap="outerHTML"
+ data-i18n="calendar.thisMonth"
title="Revenir au mois actuel"
style="flex: 1; padding: 0.5rem; font-size: 0.85rem;">
🗓️ Ce mois
diff --git a/templates/editor.html b/templates/editor.html
index 580f49d..8999b00 100644
--- a/templates/editor.html
+++ b/templates/editor.html
@@ -18,11 +18,11 @@
{{end}}
{{if .IsHome}}
-
+
🌍 Langue / Language
+ +
+
+
+
+
+
▶
-
⭐ Favoris
+⭐ Favoris
▶
-
📅 Daily Notes
+📅 Daily Notes
-
+
📁 Nouveau dossier
@@ -537,7 +573,7 @@
- Paramètres
+ Paramètres
diff --git a/test-i18n.sh b/test-i18n.sh
new file mode 100755
index 0000000..803a9f9
--- /dev/null
+++ b/test-i18n.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+
+# Test script - Check compilation
+set -e
+
+echo "🧪 Testing Personotes i18n implementation..."
+echo ""
+
+# Check Go files
+echo "✓ Checking Go syntax..."
+go fmt ./...
+echo " Go files formatted"
+
+# Check if locales exist
+echo ""
+echo "✓ Checking translation files..."
+if [ -f "locales/en.json" ]; then
+ echo " ✓ locales/en.json exists"
+else
+ echo " ✗ locales/en.json missing"
+ exit 1
+fi
+
+if [ -f "locales/fr.json" ]; then
+ echo " ✓ locales/fr.json exists"
+else
+ echo " ✗ locales/fr.json missing"
+ exit 1
+fi
+
+# Validate JSON files
+echo ""
+echo "✓ Validating JSON files..."
+if command -v jq &> /dev/null; then
+ jq empty locales/en.json && echo " ✓ en.json is valid JSON"
+ jq empty locales/fr.json && echo " ✓ fr.json is valid JSON"
+else
+ echo " ⚠ jq not installed, skipping JSON validation"
+fi
+
+# Check JavaScript files
+echo ""
+echo "✓ Checking JavaScript files..."
+if [ -f "frontend/src/i18n.js" ]; then
+ echo " ✓ frontend/src/i18n.js exists"
+else
+ echo " ✗ frontend/src/i18n.js missing"
+ exit 1
+fi
+
+if [ -f "frontend/src/language-manager.js" ]; then
+ echo " ✓ frontend/src/language-manager.js exists"
+else
+ echo " ✗ frontend/src/language-manager.js missing"
+ exit 1
+fi
+
+echo ""
+echo "✅ All checks passed!"
+echo ""
+echo "Next steps:"
+echo "1. cd frontend && npm run build"
+echo "2. go run ./cmd/server"
+echo "3. Open http://localhost:8080"
+echo "4. Click ⚙️ Settings > Autre tab > Select language"