Commit avant changement d'agent vers devstral

This commit is contained in:
2025-11-13 17:00:47 +01:00
parent a09b73e4f1
commit cc1d6880a7
25 changed files with 2903 additions and 89 deletions

240
frontend/src/i18n.js Normal file
View File

@ -0,0 +1,240 @@
import { debug, debugError } from './debug.js';
/**
* I18n - Internationalization manager for client-side translations
*/
class I18n {
constructor() {
this.translations = {};
this.currentLang = this.getStoredLanguage() || this.detectBrowserLanguage() || 'en';
this.fallbackLang = 'en';
this.isLoaded = false;
this.onLanguageChangeCallbacks = [];
}
/**
* Get stored language from localStorage
*/
getStoredLanguage() {
try {
return localStorage.getItem('language');
} catch (e) {
debugError('Failed to get stored language:', e);
return null;
}
}
/**
* Detect browser language
*/
detectBrowserLanguage() {
const browserLang = navigator.language || navigator.userLanguage;
// Extract language code (e.g., "fr-FR" -> "fr")
const langCode = browserLang.split('-')[0];
debug(`Detected browser language: ${langCode}`);
return langCode;
}
/**
* Load translations from server
*/
async loadTranslations(lang = this.currentLang) {
try {
const response = await fetch(`/api/i18n/${lang}`);
if (!response.ok) {
throw new Error(`Failed to load translations for ${lang}`);
}
const data = await response.json();
this.translations[lang] = data;
this.isLoaded = true;
debug(`✅ Loaded translations for language: ${lang}`);
return true;
} catch (error) {
debugError(`Failed to load translations for ${lang}:`, error);
// Try to load fallback language if current language fails
if (lang !== this.fallbackLang) {
debug(`Attempting to load fallback language: ${this.fallbackLang}`);
return this.loadTranslations(this.fallbackLang);
}
return false;
}
}
/**
* Initialize i18n system
*/
async init() {
await this.loadTranslations(this.currentLang);
// Load fallback language if different from current
if (this.currentLang !== this.fallbackLang && !this.translations[this.fallbackLang]) {
await this.loadTranslations(this.fallbackLang);
}
debug(`I18n initialized with language: ${this.currentLang}`);
}
/**
* Translate a key with optional arguments for interpolation
* @param {string} key - Translation key in dot notation (e.g., "menu.home")
* @param {object} args - Optional arguments for variable interpolation
* @returns {string} Translated string
*/
t(key, args = {}) {
if (!this.isLoaded) {
debug(`⚠️ Translations not loaded yet, returning key: ${key}`);
return key;
}
// Try current language first
let translation = this.getTranslation(this.currentLang, key);
// Fallback to default language
if (!translation && this.currentLang !== this.fallbackLang) {
translation = this.getTranslation(this.fallbackLang, key);
}
// Return key if no translation found
if (!translation) {
debug(`⚠️ Translation not found for key: ${key}`);
return key;
}
// Interpolate variables
return this.interpolate(translation, args);
}
/**
* Get translation by key using dot notation
*/
getTranslation(lang, key) {
const langTranslations = this.translations[lang];
if (!langTranslations) {
return null;
}
const parts = key.split('.');
let current = langTranslations;
for (const part of parts) {
if (current && typeof current === 'object' && part in current) {
current = current[part];
} else {
return null;
}
}
return typeof current === 'string' ? current : null;
}
/**
* Interpolate variables in translation string
* Replaces {{variable}} with actual values
*/
interpolate(str, args) {
return str.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return args[key] !== undefined ? args[key] : match;
});
}
/**
* Change current language
*/
async setLanguage(lang) {
if (lang === this.currentLang) {
debug(`Language already set to: ${lang}`);
return;
}
debug(`Changing language from ${this.currentLang} to ${lang}`);
// Load translations if not already loaded
if (!this.translations[lang]) {
const loaded = await this.loadTranslations(lang);
if (!loaded) {
debugError(`Failed to change language to ${lang}`);
return;
}
}
this.currentLang = lang;
// Store in localStorage
try {
localStorage.setItem('language', lang);
} catch (e) {
debugError('Failed to store language:', e);
}
// Update HTML lang attribute
document.documentElement.lang = lang;
// Notify all registered callbacks
this.notifyLanguageChange(lang);
debug(`✅ Language changed to: ${lang}`);
}
/**
* Register a callback to be called when language changes
*/
onLanguageChange(callback) {
this.onLanguageChangeCallbacks.push(callback);
}
/**
* Notify all callbacks about language change
*/
notifyLanguageChange(lang) {
this.onLanguageChangeCallbacks.forEach(callback => {
try {
callback(lang);
} catch (error) {
debugError('Error in language change callback:', error);
}
});
}
/**
* Get current language
*/
getCurrentLanguage() {
return this.currentLang;
}
/**
* Get available languages
*/
getAvailableLanguages() {
return Object.keys(this.translations);
}
/**
* Translate all elements with data-i18n attribute
*/
translatePage() {
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(element => {
const key = element.getAttribute('data-i18n');
const translation = this.t(key);
// Check if we should set text content or placeholder
if (element.hasAttribute('data-i18n-placeholder')) {
element.placeholder = translation;
} else {
element.textContent = translation;
}
});
}
}
// Create singleton instance
export const i18n = new I18n();
// Export convenience function
export const t = (key, args) => i18n.t(key, args);
// Initialize on import
i18n.init().then(() => {
debug('I18n system ready');
});

View File

@ -0,0 +1,344 @@
import { debug } from './debug.js';
import { i18n, t } from './i18n.js';
/**
* LanguageManager - Manages language selection UI and persistence
*/
class LanguageManager {
constructor() {
this.init();
}
init() {
debug('LanguageManager initialized');
// Listen for language changes to update UI
i18n.onLanguageChange((lang) => {
this.updateUI(lang);
this.reloadPageContent();
});
// Listen to HTMX events to translate content after dynamic loads
document.body.addEventListener('htmx:afterSwap', () => {
debug('HTMX content swapped, translating UI...');
// Wait a bit for DOM to be ready
setTimeout(() => this.translateStaticUI(), 50);
});
// Setup event listeners after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.setupEventListeners();
// Translate UI on initial load once i18n is ready
if (i18n.isLoaded) {
this.translateStaticUI();
} else {
// Wait for i18n to load
setTimeout(() => this.translateStaticUI(), 500);
}
});
} else {
this.setupEventListeners();
// Translate UI on initial load once i18n is ready
if (i18n.isLoaded) {
this.translateStaticUI();
} else {
// Wait for i18n to load
setTimeout(() => this.translateStaticUI(), 500);
}
}
}
setupEventListeners() {
// Language selector in settings modal
document.addEventListener('change', (e) => {
if (e.target.name === 'language') {
const selectedLang = e.target.value;
debug(`Language selected: ${selectedLang}`);
i18n.setLanguage(selectedLang);
}
});
// Initialize language selector state
this.updateUI(i18n.getCurrentLanguage());
}
/**
* Update UI to reflect current language
*/
updateUI(lang) {
// Update radio buttons in settings
const languageRadios = document.querySelectorAll('input[name="language"]');
languageRadios.forEach(radio => {
radio.checked = (radio.value === lang);
});
// Update HTML lang attribute
document.documentElement.lang = lang;
debug(`UI updated for language: ${lang}`);
}
/**
* Reload page content when language changes
* This triggers HTMX to re-fetch content with new language
*/
reloadPageContent() {
debug('Reloading page content with new language...');
// Translate all static UI elements immediately
this.translateStaticUI();
// Reload the current view by triggering HTMX
const editorContainer = document.getElementById('editor-container');
if (editorContainer && window.htmx) {
// Get current path from URL or default to home
const currentPath = window.location.pathname;
if (currentPath === '/' || currentPath === '') {
// Reload home view
window.htmx.ajax('GET', '/api/home', {
target: '#editor-container',
swap: 'innerHTML'
});
} else if (currentPath.startsWith('/notes/')) {
// Reload current note
window.htmx.ajax('GET', `/api${currentPath}`, {
target: '#editor-container',
swap: 'innerHTML'
});
}
}
// Reload file tree
const fileTree = document.getElementById('file-tree');
if (fileTree && window.htmx) {
window.htmx.ajax('GET', '/api/tree', {
target: '#file-tree',
swap: 'outerHTML'
});
}
// Reload favorites
const favoritesContent = document.getElementById('favorites-content');
if (favoritesContent && window.htmx) {
window.htmx.ajax('GET', '/api/favorites', {
target: '#favorites-content',
swap: 'innerHTML'
});
}
// Translate all elements with data-i18n attributes
i18n.translatePage();
debug('✅ Page content reloaded');
}
/**
* Translate all static UI elements (buttons, labels, etc.)
*/
translateStaticUI() {
debug('Translating static UI elements...');
// 1. Translate all elements with data-i18n attributes
const elementsWithI18n = document.querySelectorAll('[data-i18n]');
elementsWithI18n.forEach(element => {
const key = element.getAttribute('data-i18n');
const translation = t(key);
if (translation && translation !== key) {
// Preserve emojis and icons at the start
const currentText = element.textContent.trim();
const emojiMatch = currentText.match(/^[\u{1F300}-\u{1F9FF}✨🏠📁📝⚙️🔍🎨🔤⌨️🌍]+/u);
if (emojiMatch) {
element.textContent = `${emojiMatch[0]} ${translation}`;
} else {
element.textContent = translation;
}
}
});
// 2. Translate placeholders with data-i18n-placeholder
const elementsWithPlaceholder = document.querySelectorAll('[data-i18n-placeholder]');
elementsWithPlaceholder.forEach(element => {
const key = element.getAttribute('data-i18n-placeholder');
const translation = t(key);
if (translation && translation !== key) {
element.placeholder = translation;
}
});
// 3. Translate titles with data-i18n-title
const elementsWithTitle = document.querySelectorAll('[data-i18n-title]');
elementsWithTitle.forEach(element => {
const key = element.getAttribute('data-i18n-title');
const translation = t(key);
if (translation && translation !== key) {
element.title = translation;
}
});
// Legacy: Direct element translation for backwards compatibility
// Header buttons
const homeButton = document.querySelector('button[hx-get="/api/home"]');
if (homeButton && !homeButton.hasAttribute('data-i18n')) {
homeButton.innerHTML = `🏠 ${t('menu.home')}`;
}
const newNoteButton = document.querySelector('header button[onclick="showNewNoteModal()"]');
if (newNoteButton && !newNoteButton.hasAttribute('data-i18n')) {
newNoteButton.innerHTML = `${t('menu.newNote')}`;
}
// Search placeholder
const searchInput = document.querySelector('input[type="search"]');
if (searchInput && !searchInput.hasAttribute('data-i18n-placeholder')) {
searchInput.placeholder = t('search.placeholder');
}
// New note modal
const newNoteModal = document.getElementById('new-note-modal');
if (newNoteModal) {
const title = newNoteModal.querySelector('h2');
if (title) title.textContent = `📝 ${t('newNoteModal.title')}`;
const label = newNoteModal.querySelector('label[for="note-name"]');
if (label) label.textContent = t('newNoteModal.label');
const input = newNoteModal.querySelector('#note-name');
if (input) input.placeholder = t('newNoteModal.placeholder');
const submitBtn = newNoteModal.querySelector('button[type="submit"]');
if (submitBtn) submitBtn.textContent = t('newNoteModal.create');
const cancelBtn = newNoteModal.querySelector('button.secondary');
if (cancelBtn) cancelBtn.textContent = t('newNoteModal.cancel');
}
// New folder modal
const newFolderModal = document.getElementById('new-folder-modal');
if (newFolderModal) {
const title = newFolderModal.querySelector('h2');
if (title) title.textContent = `📁 ${t('newFolderModal.title')}`;
const label = newFolderModal.querySelector('label[for="folder-name"]');
if (label) label.textContent = t('newFolderModal.label');
const input = newFolderModal.querySelector('#folder-name');
if (input) input.placeholder = t('newFolderModal.placeholder');
const submitBtn = newFolderModal.querySelector('button[type="submit"]');
if (submitBtn) submitBtn.textContent = t('newFolderModal.create');
const cancelBtn = newFolderModal.querySelector('button.secondary');
if (cancelBtn) cancelBtn.textContent = t('newFolderModal.cancel');
}
// Selection toolbar
const deleteButton = document.querySelector('button[onclick="deleteSelected()"]');
if (deleteButton) {
const span = deleteButton.querySelector('svg + text') || deleteButton.lastChild;
if (span && span.nodeType === Node.TEXT_NODE) {
deleteButton.childNodes[deleteButton.childNodes.length - 1].textContent = t('selectionToolbar.delete');
} else {
// Si c'est dans un span ou autre
const textNode = Array.from(deleteButton.childNodes).find(n => n.nodeType === Node.TEXT_NODE);
if (textNode) {
textNode.textContent = ` ${t('selectionToolbar.delete')}`;
}
}
}
const cancelSelectionButton = document.querySelector('button[onclick="cancelSelection()"]');
if (cancelSelectionButton) {
cancelSelectionButton.textContent = t('selectionToolbar.cancel');
}
// Theme modal
const modalTitle = document.querySelector('.theme-modal-content h2');
if (modalTitle) {
modalTitle.textContent = `⚙️ ${t('settings.title')}`;
}
// Translate tabs
const tabs = document.querySelectorAll('.settings-tab');
if (tabs.length >= 4) {
tabs[0].innerHTML = `🎨 ${t('tabs.themes')}`;
tabs[1].innerHTML = `🔤 ${t('tabs.fonts')}`;
tabs[2].innerHTML = `⌨️ ${t('tabs.shortcuts')}`;
tabs[3].innerHTML = `⚙️ ${t('tabs.other')}`;
}
// Translate close button in settings
const closeButtons = document.querySelectorAll('.theme-modal-footer button');
closeButtons.forEach(btn => {
if (btn.getAttribute('onclick') === 'closeThemeModal()') {
btn.textContent = t('settings.close');
}
});
// Translate language section heading
const langSection = document.getElementById('other-section');
if (langSection) {
const heading = langSection.querySelector('h3');
if (heading) {
heading.textContent = `🌍 ${t('languages.title')}`;
}
}
// Sidebar sections
const searchSectionTitle = document.querySelector('.sidebar-section-title');
if (searchSectionTitle && searchSectionTitle.textContent.includes('🔍')) {
searchSectionTitle.textContent = `🔍 ${t('search.title') || 'Recherche'}`;
}
// Sidebar "Nouveau dossier" button
const newFolderBtn = document.querySelector('.folder-create-btn');
if (newFolderBtn && !newFolderBtn.hasAttribute('data-i18n')) {
newFolderBtn.innerHTML = `📁 ${t('fileTree.newFolder')}`;
}
// Sidebar "Paramètres" button span
const settingsSpan = document.querySelector('#theme-settings-btn span');
if (settingsSpan && !settingsSpan.hasAttribute('data-i18n')) {
settingsSpan.textContent = t('settings.title');
}
// Sidebar section titles with data-i18n
const sidebarTitles = document.querySelectorAll('.sidebar-section-title[data-i18n]');
sidebarTitles.forEach(title => {
const key = title.getAttribute('data-i18n');
const translation = t(key);
if (translation && translation !== key) {
const currentText = title.textContent.trim();
const emojiMatch = currentText.match(/^[\u{1F300}-\u{1F9FF}⭐📅🔍]+/u);
if (emojiMatch) {
title.textContent = `${emojiMatch[0]} ${translation}`;
} else {
title.textContent = translation;
}
}
});
debug('✅ Static UI translated');
}
/**
* Get current language
*/
getCurrentLanguage() {
return i18n.getCurrentLanguage();
}
/**
* Get available languages
*/
getAvailableLanguages() {
return i18n.getAvailableLanguages();
}
}
// Create singleton instance
const languageManager = new LanguageManager();
export default languageManager;
export { languageManager };

View File

@ -1,3 +1,5 @@
import './i18n.js';
import './language-manager.js';
import './editor.js';
import './file-tree.js';
import './ui.js';

View File

@ -174,6 +174,10 @@ window.switchSettingsTab = function(tabName) {
document.getElementById('themes-section').style.display = 'none';
document.getElementById('fonts-section').style.display = 'none';
document.getElementById('editor-section').style.display = 'none';
const otherSection = document.getElementById('other-section');
if (otherSection) {
otherSection.style.display = 'none';
}
// Activer l'onglet cliqué
const activeTab = Array.from(tabs).find(tab => {
@ -181,6 +185,7 @@ window.switchSettingsTab = function(tabName) {
if (tabName === 'themes') return text.includes('thème');
if (tabName === 'fonts') return text.includes('police');
if (tabName === 'editor') return text.includes('éditeur');
if (tabName === 'other') return text.includes('autre');
return false;
});
if (activeTab) {