Commit avant changement d'agent vers devstral
This commit is contained in:
@ -58,6 +58,53 @@ func (h *Handler) getDailyNoteAbsolutePath(date time.Time) string {
|
||||
return filepath.Join(h.notesDir, relativePath)
|
||||
}
|
||||
|
||||
// translateWeekday traduit un jour de la semaine
|
||||
func (h *Handler) translateWeekday(r *http.Request, weekday time.Weekday) string {
|
||||
dayKeys := map[time.Weekday]string{
|
||||
time.Monday: "calendar.monday",
|
||||
time.Tuesday: "calendar.tuesday",
|
||||
time.Wednesday: "calendar.wednesday",
|
||||
time.Thursday: "calendar.thursday",
|
||||
time.Friday: "calendar.friday",
|
||||
time.Saturday: "calendar.saturday",
|
||||
time.Sunday: "calendar.sunday",
|
||||
}
|
||||
return h.t(r, dayKeys[weekday])
|
||||
}
|
||||
|
||||
// translateWeekdayShort traduit un jour de la semaine (version courte)
|
||||
func (h *Handler) translateWeekdayShort(r *http.Request, weekday time.Weekday) string {
|
||||
dayKeys := map[time.Weekday]string{
|
||||
time.Monday: "calendar.mon",
|
||||
time.Tuesday: "calendar.tue",
|
||||
time.Wednesday: "calendar.wed",
|
||||
time.Thursday: "calendar.thu",
|
||||
time.Friday: "calendar.fri",
|
||||
time.Saturday: "calendar.sat",
|
||||
time.Sunday: "calendar.sun",
|
||||
}
|
||||
return h.t(r, dayKeys[weekday])
|
||||
}
|
||||
|
||||
// translateMonth traduit un nom de mois
|
||||
func (h *Handler) translateMonth(r *http.Request, month time.Month) string {
|
||||
monthKeys := map[time.Month]string{
|
||||
time.January: "calendar.january",
|
||||
time.February: "calendar.february",
|
||||
time.March: "calendar.march",
|
||||
time.April: "calendar.april",
|
||||
time.May: "calendar.may",
|
||||
time.June: "calendar.june",
|
||||
time.July: "calendar.july",
|
||||
time.August: "calendar.august",
|
||||
time.September: "calendar.september",
|
||||
time.October: "calendar.october",
|
||||
time.November: "calendar.november",
|
||||
time.December: "calendar.december",
|
||||
}
|
||||
return h.t(r, monthKeys[month])
|
||||
}
|
||||
|
||||
// dailyNoteExists vérifie si une daily note existe pour une date donnée
|
||||
func (h *Handler) dailyNoteExists(date time.Time) bool {
|
||||
absPath := h.getDailyNoteAbsolutePath(date)
|
||||
@ -66,7 +113,7 @@ func (h *Handler) dailyNoteExists(date time.Time) bool {
|
||||
}
|
||||
|
||||
// createDailyNote crée une daily note avec un template par défaut
|
||||
func (h *Handler) createDailyNote(date time.Time) error {
|
||||
func (h *Handler) createDailyNote(r *http.Request, date time.Time) error {
|
||||
absPath := h.getDailyNoteAbsolutePath(date)
|
||||
|
||||
// Créer les dossiers parents si nécessaire
|
||||
@ -84,35 +131,9 @@ func (h *Handler) createDailyNote(date time.Time) error {
|
||||
dateStr := date.Format("02-01-2006")
|
||||
dateTimeStr := date.Format("02-01-2006:15:04")
|
||||
|
||||
// Noms des jours en français
|
||||
dayNames := map[time.Weekday]string{
|
||||
time.Monday: "Lundi",
|
||||
time.Tuesday: "Mardi",
|
||||
time.Wednesday: "Mercredi",
|
||||
time.Thursday: "Jeudi",
|
||||
time.Friday: "Vendredi",
|
||||
time.Saturday: "Samedi",
|
||||
time.Sunday: "Dimanche",
|
||||
}
|
||||
|
||||
// Noms des mois en français
|
||||
monthNames := map[time.Month]string{
|
||||
time.January: "janvier",
|
||||
time.February: "février",
|
||||
time.March: "mars",
|
||||
time.April: "avril",
|
||||
time.May: "mai",
|
||||
time.June: "juin",
|
||||
time.July: "juillet",
|
||||
time.August: "août",
|
||||
time.September: "septembre",
|
||||
time.October: "octobre",
|
||||
time.November: "novembre",
|
||||
time.December: "décembre",
|
||||
}
|
||||
|
||||
dayName := dayNames[date.Weekday()]
|
||||
monthName := monthNames[date.Month()]
|
||||
// Traduire le nom du jour et du mois
|
||||
dayName := h.translateWeekday(r, date.Weekday())
|
||||
monthName := h.translateMonth(r, date.Month())
|
||||
|
||||
// Template de la daily note
|
||||
template := fmt.Sprintf(`---
|
||||
@ -159,7 +180,7 @@ func (h *Handler) handleDailyToday(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Créer la note si elle n'existe pas
|
||||
if !h.dailyNoteExists(today) {
|
||||
if err := h.createDailyNote(today); err != nil {
|
||||
if err := h.createDailyNote(r, today); err != nil {
|
||||
h.logger.Printf("Erreur création daily note: %v", err)
|
||||
http.Error(w, "Erreur lors de la création de la note", http.StatusInternalServerError)
|
||||
return
|
||||
@ -190,7 +211,7 @@ func (h *Handler) handleDailyDate(w http.ResponseWriter, r *http.Request, dateSt
|
||||
|
||||
// Créer la note si elle n'existe pas
|
||||
if !h.dailyNoteExists(date) {
|
||||
if err := h.createDailyNote(date); err != nil {
|
||||
if err := h.createDailyNote(r, date); err != nil {
|
||||
h.logger.Printf("Erreur création daily note: %v", err)
|
||||
http.Error(w, "Erreur lors de la création de la note", http.StatusInternalServerError)
|
||||
return
|
||||
@ -232,7 +253,7 @@ func (h *Handler) handleDailyCalendar(w http.ResponseWriter, r *http.Request, ye
|
||||
}
|
||||
|
||||
// Créer les données du calendrier
|
||||
calendarData := h.buildCalendarData(year, time.Month(month))
|
||||
calendarData := h.buildCalendarData(r, year, time.Month(month))
|
||||
|
||||
// Rendre le template
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@ -243,7 +264,7 @@ func (h *Handler) handleDailyCalendar(w http.ResponseWriter, r *http.Request, ye
|
||||
}
|
||||
|
||||
// buildCalendarData construit les données du calendrier pour un mois donné
|
||||
func (h *Handler) buildCalendarData(year int, month time.Month) *CalendarData {
|
||||
func (h *Handler) buildCalendarData(r *http.Request, year int, month time.Month) *CalendarData {
|
||||
// Premier jour du mois
|
||||
firstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local)
|
||||
|
||||
@ -253,26 +274,10 @@ func (h *Handler) buildCalendarData(year int, month time.Month) *CalendarData {
|
||||
// Date d'aujourd'hui
|
||||
today := time.Now()
|
||||
|
||||
// Noms des mois en français
|
||||
monthNames := map[time.Month]string{
|
||||
time.January: "Janvier",
|
||||
time.February: "Février",
|
||||
time.March: "Mars",
|
||||
time.April: "Avril",
|
||||
time.May: "Mai",
|
||||
time.June: "Juin",
|
||||
time.July: "Juillet",
|
||||
time.August: "Août",
|
||||
time.September: "Septembre",
|
||||
time.October: "Octobre",
|
||||
time.November: "Novembre",
|
||||
time.December: "Décembre",
|
||||
}
|
||||
|
||||
data := &CalendarData{
|
||||
Year: year,
|
||||
Month: month,
|
||||
MonthName: monthNames[month],
|
||||
MonthName: h.translateMonth(r, month),
|
||||
Weeks: make([][7]CalendarDay, 0),
|
||||
}
|
||||
|
||||
@ -373,22 +378,12 @@ func (h *Handler) handleDailyRecent(w http.ResponseWriter, r *http.Request) {
|
||||
date := today.AddDate(0, 0, -i)
|
||||
|
||||
if h.dailyNoteExists(date) {
|
||||
dayNames := map[time.Weekday]string{
|
||||
time.Monday: "Lun",
|
||||
time.Tuesday: "Mar",
|
||||
time.Wednesday: "Mer",
|
||||
time.Thursday: "Jeu",
|
||||
time.Friday: "Ven",
|
||||
time.Saturday: "Sam",
|
||||
time.Sunday: "Dim",
|
||||
}
|
||||
|
||||
info := &DailyNoteInfo{
|
||||
Date: date,
|
||||
Path: h.getDailyNotePath(date),
|
||||
Exists: true,
|
||||
Title: date.Format("02/01/2006"),
|
||||
DayOfWeek: dayNames[date.Weekday()],
|
||||
DayOfWeek: h.translateWeekdayShort(r, date.Weekday()),
|
||||
DayOfMonth: date.Day(),
|
||||
}
|
||||
recentNotes = append(recentNotes, info)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/mathieu/personotes/internal/i18n"
|
||||
"github.com/mathieu/personotes/internal/indexer"
|
||||
)
|
||||
|
||||
@ -39,15 +41,17 @@ type Handler struct {
|
||||
idx *indexer.Indexer
|
||||
templates *template.Template
|
||||
logger *log.Logger
|
||||
i18n *i18n.Translator
|
||||
}
|
||||
|
||||
// NewHandler construit un handler unifié pour l'API.
|
||||
func NewHandler(notesDir string, idx *indexer.Indexer, tpl *template.Template, logger *log.Logger) *Handler {
|
||||
func NewHandler(notesDir string, idx *indexer.Indexer, tpl *template.Template, logger *log.Logger, translator *i18n.Translator) *Handler {
|
||||
return &Handler{
|
||||
notesDir: notesDir,
|
||||
idx: idx,
|
||||
templates: tpl,
|
||||
logger: logger,
|
||||
i18n: translator,
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,6 +59,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
h.logger.Printf("%s %s", r.Method, path)
|
||||
|
||||
// I18n endpoint - serve translation files
|
||||
if strings.HasPrefix(path, "/api/i18n/") {
|
||||
h.handleI18n(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// REST API v1 endpoints
|
||||
if strings.HasPrefix(path, "/api/v1/notes") {
|
||||
h.handleRESTNotes(w, r)
|
||||
@ -278,7 +288,7 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Générer le contenu Markdown avec la liste de toutes les notes
|
||||
content := h.generateHomeMarkdown()
|
||||
content := h.generateHomeMarkdown(r)
|
||||
|
||||
// Utiliser le template editor.html pour afficher la page d'accueil
|
||||
data := struct {
|
||||
@ -317,12 +327,12 @@ func (h *Handler) handleAbout(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// generateHomeMarkdown génère le contenu Markdown de la page d'accueil
|
||||
func (h *Handler) generateHomeMarkdown() string {
|
||||
func (h *Handler) generateHomeMarkdown(r *http.Request) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// En-tête
|
||||
sb.WriteString("# 📚 Index\n\n")
|
||||
sb.WriteString("_Mise à jour automatique • " + time.Now().Format("02/01/2006 à 15:04") + "_\n\n")
|
||||
sb.WriteString("_" + h.t(r, "home.autoUpdate") + " • " + time.Now().Format("02/01/2006 à 15:04") + "_\n\n")
|
||||
|
||||
// Construire l'arborescence
|
||||
tree, err := h.buildFileTree()
|
||||
@ -339,15 +349,15 @@ func (h *Handler) generateHomeMarkdown() string {
|
||||
h.generateTagsSection(&sb)
|
||||
|
||||
// Section des favoris (après les tags)
|
||||
h.generateFavoritesSection(&sb)
|
||||
h.generateFavoritesSection(&sb, r)
|
||||
|
||||
// Section des notes récemment modifiées (après les favoris)
|
||||
h.generateRecentNotesSection(&sb)
|
||||
h.generateRecentNotesSection(&sb, r)
|
||||
|
||||
// Section de toutes les notes avec accordéon
|
||||
sb.WriteString("<div class=\"home-section\">\n")
|
||||
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('all-notes')\">\n")
|
||||
sb.WriteString(fmt.Sprintf(" <h2 class=\"home-section-title\">📂 Toutes les notes (%d)</h2>\n", noteCount))
|
||||
sb.WriteString(fmt.Sprintf(" <h2 class=\"home-section-title\">📂 %s (%d)</h2>\n", h.t(r, "home.allNotes"), noteCount))
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-all-notes\">\n")
|
||||
|
||||
@ -390,7 +400,7 @@ func (h *Handler) generateTagsSection(sb *strings.Builder) {
|
||||
}
|
||||
|
||||
// generateFavoritesSection génère la section des favoris avec arborescence dépliable
|
||||
func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
|
||||
func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request) {
|
||||
favorites, err := h.loadFavorites()
|
||||
if err != nil || len(favorites.Items) == 0 {
|
||||
return
|
||||
@ -398,7 +408,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
|
||||
|
||||
sb.WriteString("<div class=\"home-section\">\n")
|
||||
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('favorites')\">\n")
|
||||
sb.WriteString(" <h2 class=\"home-section-title\">⭐ Favoris</h2>\n")
|
||||
sb.WriteString(" <h2 class=\"home-section-title\">⭐ " + h.t(r, "favorites.title") + "</h2>\n")
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-favorites\">\n")
|
||||
sb.WriteString(" <div class=\"note-tree favorites-tree\">\n")
|
||||
@ -423,7 +433,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
|
||||
} else {
|
||||
// Fichier
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"file indent-level-1\">\n"))
|
||||
sb.WriteString(fmt.Sprintf(" <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">", fav.Path))
|
||||
sb.WriteString(fmt.Sprintf(" <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\">", fav.Path))
|
||||
sb.WriteString(fmt.Sprintf("📄 %s", fav.Title))
|
||||
sb.WriteString("</a>\n")
|
||||
sb.WriteString(fmt.Sprintf(" </div>\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("<div class=\"home-section\">\n")
|
||||
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('recent')\">\n")
|
||||
sb.WriteString(" <h2 class=\"home-section-title\">🕒 Récemment modifiés</h2>\n")
|
||||
sb.WriteString(" <h2 class=\"home-section-title\">🕒 " + h.t(r, "home.recentlyModified") + "</h2>\n")
|
||||
sb.WriteString(" </div>\n")
|
||||
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-recent\">\n")
|
||||
sb.WriteString(" <div class=\"recent-notes-container\">\n")
|
||||
@ -464,7 +474,7 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
|
||||
}
|
||||
|
||||
sb.WriteString(" <div class=\"recent-note-card\">\n")
|
||||
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"recent-note-link\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">\n", doc.Path))
|
||||
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"recent-note-link\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\">\n", doc.Path))
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-title\">%s</div>\n", doc.Title))
|
||||
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-meta\">\n"))
|
||||
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\">📅 %s</span>\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<div class=\"file %s\">\n", indent, indentClass))
|
||||
sb.WriteString(fmt.Sprintf("%s <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">", indent, relativePath))
|
||||
sb.WriteString(fmt.Sprintf("%s <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\">", indent, relativePath))
|
||||
sb.WriteString(fmt.Sprintf("📄 %s", displayName))
|
||||
sb.WriteString("</a>\n")
|
||||
sb.WriteString(fmt.Sprintf("%s</div>\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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user