feat: complete fr locale and fix i18n tooling

This commit is contained in:
callumalpass 2025-09-20 08:01:21 +10:00
parent eb7be73987
commit c721a67230
12 changed files with 4936 additions and 1279 deletions

View file

@ -8,7 +8,7 @@ User Fields extend TaskNotes' filtering system by allowing users to define custo
### Core Components
**Settings Interface** (`src/settings/settings.ts`)
**Settings Interface** (`src/settings/TaskNotesSettingTab.ts`)
- User field configuration UI in Advanced Settings
- Real-time validation and field management
- Immediate filter option refresh on changes

File diff suppressed because it is too large Load diff

View file

@ -117,6 +117,25 @@ function getAvailableLocales() {
// --- Commands ---
function normalizeStateEntry(entry) {
if (!entry) {
return { source: '', translation: '' };
}
if (typeof entry === 'string') {
return { source: entry, translation: '' };
}
if (typeof entry === 'object') {
return {
source: entry.source ?? '',
translation: entry.translation ?? ''
};
}
return { source: '', translation: '' };
}
/** `sync`: Updates manifest and state files. */
async function sync() {
console.log('Syncing i18n files...');
@ -158,23 +177,27 @@ async function sync() {
if (translatedValue === undefined) {
// Key is missing from translation file
newState[locale][key] = ''; // Mark as untranslated
} else if (translatedValue === sourceMap[key]) {
// Value is identical to source, likely untranslated
newState[locale][key] = ''; // Mark as untranslated
} else {
// Check if the translation is stale
const previousStateHash = currentState[locale]?.[key];
if (previousStateHash === sourceHash) {
// Translation is up-to-date
newState[locale][key] = sourceHash;
} else {
// Translation exists but might be stale. We assume it's stale.
// A more advanced system might check if the translation itself has changed.
// For now, we preserve the old hash to signify it's stale.
newState[locale][key] = previousStateHash || '';
}
newState[locale][key] = null;
continue;
}
const translationHash = hash(translatedValue);
const previousEntry = normalizeStateEntry(currentState[locale]?.[key]);
let confirmedSource = previousEntry.source;
if (previousEntry.source === sourceHash) {
confirmedSource = sourceHash;
} else if (previousEntry.translation && previousEntry.translation !== translationHash) {
confirmedSource = sourceHash;
} else if (!previousEntry.source) {
confirmedSource = sourceHash;
}
newState[locale][key] = {
source: confirmedSource,
translation: translationHash
};
}
}
saveJson(STATE_PATH, newState);
@ -202,13 +225,13 @@ async function verify() {
for (const key in manifest) {
const sourceHash = manifest[key];
const stateHash = state[locale]?.[key];
const entry = normalizeStateEntry(state[locale]?.[key]);
localeStats[locale].total++;
if (!stateHash || stateHash === '') {
if (!state[locale]?.[key] || !entry.translation) {
missingTranslations.push({ locale, key });
} else if (sourceHash !== stateHash) {
} else if (entry.source !== sourceHash) {
staleTranslations.push({ locale, key });
localeStats[locale].stale++;
} else {
@ -298,10 +321,10 @@ async function status() {
for (const key in manifest) {
const sourceHash = manifest[key];
const stateHash = state[locale]?.[key];
const entry = normalizeStateEntry(state[locale]?.[key]);
if (stateHash && stateHash !== '') {
if (sourceHash === stateHash) {
if (entry.translation) {
if (entry.source === sourceHash) {
translated++;
} else {
stale++;
@ -341,4 +364,4 @@ const command = process.argv[2];
}
process.exit(1);
}
})();
})();

View file

@ -23,7 +23,7 @@ export class RecurrenceContextMenu {
constructor(options: RecurrenceContextMenuOptions) {
this.menu = new Menu();
this.options = options;
this.translate = options.plugin.i18n.translate;
this.translate = options.plugin.i18n.translate.bind(options.plugin.i18n);
this.buildMenu();
}
@ -776,4 +776,4 @@ class CustomRecurrenceModal extends Modal {
const { contentEl } = this;
contentEl.empty();
}
}
}

View file

@ -2,7 +2,7 @@ import { TranslationTree } from '../types';
export const fr: TranslationTree = {
common: {
appName: 'TaskNotes',
appName: 'Notes de tâches',
cancel: 'Annuler',
confirm: 'Confirmer',
close: 'Fermer',
@ -39,7 +39,7 @@ export const fr: TranslationTree = {
},
views: {
agenda: {
title: 'Agenda',
title: 'Agenda quotidien',
today: 'Aujourd\'hui',
refreshCalendars: 'Actualiser les calendriers',
expandAllDays: 'Déplier tous les jours',
@ -61,7 +61,7 @@ export const fr: TranslationTree = {
noTasksFound: 'Aucune tâche trouvée pour les filtres sélectionnés.'
},
notes: {
title: 'Notes',
title: 'Bloc-notes',
refreshButton: 'Actualisation...',
notices: {
indexingDisabled: 'Indexation des notes désactivée'
@ -77,7 +77,7 @@ export const fr: TranslationTree = {
title: 'Calendrier avancé'
},
kanban: {
title: 'Kanban',
title: 'Tableau Kanban',
newTask: 'Nouvelle tâche',
addCard: '+ Ajouter une carte',
noTasks: 'Aucune tâche',
@ -90,9 +90,9 @@ export const fr: TranslationTree = {
}
},
pomodoro: {
title: 'Pomodoro',
title: 'Sessions Pomodoro',
status: {
focus: 'Focus',
focus: 'Concentration',
ready: 'Prêt à démarrer',
paused: 'En pause',
working: 'En travail',
@ -107,7 +107,7 @@ export const fr: TranslationTree = {
},
buttons: {
start: 'Démarrer',
pause: 'Pause',
pause: 'Mettre en pause',
stop: 'Arrêter',
resume: 'Reprendre',
startShortBreak: 'Commencer la pause courte',
@ -158,15 +158,15 @@ export const fr: TranslationTree = {
}
},
stats: {
pomodoros: 'Pomodoros',
pomodoros: 'Sessions',
streak: 'Série',
minutes: 'Minutes',
minutes: 'Minutes totales',
average: 'Durée moy.',
completion: 'Taux d\'achèvement'
},
recents: {
empty: 'Aucune session enregistrée pour le moment',
duration: '{minutes} min',
duration: 'Durée : {minutes} min',
status: {
completed: 'Terminée',
interrupted: 'Interrompue'
@ -508,7 +508,7 @@ export const fr: TranslationTree = {
name: 'Identifier les tâches par',
description: 'Choisissez d\'identifier les tâches par tag ou par une propriété frontmatter',
options: {
tag: 'Tag',
tag: 'Étiquette',
property: 'Propriété'
}
},
@ -614,7 +614,7 @@ export const fr: TranslationTree = {
weight: 'Poids :'
},
placeholders: {
value: 'high',
value: 'elevee',
label: 'Priorité élevée'
},
weightLabel: 'Poids : {weight}',
@ -655,7 +655,7 @@ export const fr: TranslationTree = {
archiveTag: 'Tag d\'archive',
timeEntries: 'Entrées de temps',
completeInstances: 'Instances complètes',
pomodoros: 'Pomodoros',
pomodoros: 'Sessions Pomodoro',
icsEventId: 'ID d\'événement ICS',
icsEventTag: 'Tag d\'événement ICS',
reminders: 'Rappels'
@ -684,7 +684,7 @@ export const fr: TranslationTree = {
text: 'Texte',
number: 'Nombre',
boolean: 'Booléen',
date: 'Date',
date: 'Date (AAAA-MM-JJ)',
list: 'Liste'
},
defaultNames: {
@ -720,7 +720,7 @@ export const fr: TranslationTree = {
modifiedDate: 'Date de modification',
projects: 'Projets',
contexts: 'Contextes',
tags: 'Tags'
tags: 'Étiquettes'
}
},
taskFilenames: {
@ -920,7 +920,7 @@ export const fr: TranslationTree = {
requiredPropertyKey: {
name: 'Clé de propriété requise',
description: 'Afficher seulement les notes où cette propriété frontmatter correspond à la valeur ci-dessous. Laisser vide pour ignorer.',
placeholder: 'type'
placeholder: 'type-projet'
},
requiredPropertyValue: {
name: 'Valeur de propriété requise',
@ -1358,9 +1358,9 @@ export const fr: TranslationTree = {
},
timeFormats: {
justNow: 'À l\'instant',
minutesAgo: '{minutes} minute{plural} ago',
hoursAgo: '{hours} heure{plural} ago',
daysAgo: '{days} jour{plural} ago'
minutesAgo: 'il y a {minutes} minute{plural}',
hoursAgo: 'il y a {hours} heure{plural}',
daysAgo: 'il y a {days} jour{plural}'
}
}
},
@ -1380,8 +1380,8 @@ export const fr: TranslationTree = {
projectsRemoveTooltip: 'Retirer le projet',
contextsLabel: 'Contextes',
contextsPlaceholder: 'contexte1, contexte2',
tagsLabel: 'Tags',
tagsPlaceholder: 'tag1, tag2',
tagsLabel: 'Étiquettes',
tagsPlaceholder: 'etiquette1, etiquette2',
timeEstimateLabel: 'Estimation (minutes)',
timeEstimatePlaceholder: '30',
customFieldsLabel: 'Champs personnalisés',
@ -1511,9 +1511,9 @@ export const fr: TranslationTree = {
contextMenus: {
task: {
status: 'Statut',
statusSelected: ' {label}',
statusSelected: 'Statut sélectionné : {label}',
priority: 'Priorité',
prioritySelected: ' {label}',
prioritySelected: 'Priorité sélectionnée : {label}',
dueDate: 'Échéance',
scheduledDate: 'Date planifiée',
reminders: 'Rappels',
@ -1609,7 +1609,7 @@ export const fr: TranslationTree = {
calendar: '**Calendrier :** {value}',
date: '**Date et heure :** {value}',
location: '**Lieu :** {value}',
descriptionHeading: '### Description',
descriptionHeading: '### Détails',
url: '**URL :** {value}',
at: ' à {time}'
}
@ -1629,12 +1629,12 @@ export const fr: TranslationTree = {
nextMonth: 'Le mois prochain'
},
weekdaysLabel: 'Jours de la semaine',
selected: ' {label}',
selected: 'Date sélectionnée : {label}',
pickDateTime: 'Choisir date et heure…',
clearDate: 'Effacer la date',
modal: {
title: 'Définir date et heure',
dateLabel: 'Date',
dateLabel: 'Date (AAAA-MM-JJ)',
timeLabel: 'Heure (optionnel)',
select: 'Sélectionner'
}
@ -1805,7 +1805,7 @@ export const fr: TranslationTree = {
priority: 'Priorité',
title: 'Titre',
createdDate: 'Date de création',
tags: 'Tags',
tags: 'Étiquettes',
ascending: 'Croissant',
descending: 'Décroissant'
},
@ -1817,7 +1817,7 @@ export const fr: TranslationTree = {
project: 'Projet',
dueDate: 'Date d\'échéance',
scheduledDate: 'Date planifiée',
tags: 'Tags'
tags: 'Étiquettes'
}
}
},
@ -1840,7 +1840,7 @@ export const fr: TranslationTree = {
modifiedDate: 'Date de modification',
projects: 'Projets',
contexts: 'Contextes',
tags: 'Tags'
tags: 'Étiquettes'
}
},
reminderContextMenu: {

View file

@ -21,7 +21,7 @@ export class DueDateModal extends Modal {
super(app);
this.task = task;
this.plugin = plugin;
this.translate = plugin.i18n.translate;
this.translate = plugin.i18n.translate.bind(plugin.i18n);
}
onOpen() {

View file

@ -16,7 +16,7 @@ export class MigrationModal extends Modal {
super(app);
this.migrationService = migrationService;
this.plugin = plugin;
this.translate = plugin.i18n.translate;
this.translate = plugin.i18n.translate.bind(plugin.i18n);
}
async onOpen() {

View file

@ -21,7 +21,7 @@ export class ScheduledDateModal extends Modal {
super(app);
this.task = task;
this.plugin = plugin;
this.translate = plugin.i18n.translate;
this.translate = plugin.i18n.translate.bind(plugin.i18n);
}
onOpen() {

View file

@ -16,7 +16,7 @@ export class TaskSelectorModal extends FuzzySuggestModal<TaskInfo> {
this.plugin = plugin;
this.tasks = tasks;
this.onChooseTask = onChooseTask;
this.translate = plugin.i18n.translate;
this.translate = plugin.i18n.translate.bind(plugin.i18n);
this.setPlaceholder(this.translate('modals.taskSelector.placeholder'));
this.setInstructions([

View file

@ -29,7 +29,7 @@ export class UnscheduledTasksSelectorModal extends FuzzySuggestModal<TaskInfo> {
this.plugin = plugin;
this.onScheduleTask = onScheduleTask;
this.defaultScheduleOptions = defaultScheduleOptions;
this.translate = plugin.i18n.translate;
this.translate = plugin.i18n.translate.bind(plugin.i18n);
this.setPlaceholder(this.translate('modals.unscheduledTasksSelector.placeholder'));
this.setInstructions([
@ -268,4 +268,4 @@ function renderProjectLinksForSelector(container: HTMLElement, projects: string[
container.appendChild(textNode);
}
});
}
}

View file

@ -30,7 +30,7 @@ describe('I18nService', () => {
getSystemLocale: () => 'fr'
});
expect(i18n.getCurrentLocale()).toBe('fr');
expect(i18n.translate('views.notes.title')).toBe('Notes');
expect(i18n.translate('views.notes.title')).toBe('Bloc-notes');
});
it('exposes available locales from resources', () => {

View file

@ -1,6 +1,12 @@
import TaskNotesPlugin from '../../../src/main';
import { TaskNotesSettingTab } from '../../../src/settings/settings';
import { TaskNotesSettingTab } from '../../../src/settings/TaskNotesSettingTab';
import { DEFAULT_SETTINGS } from '../../../src/settings/defaults';
import { createI18nService } from '../../../src/i18n';
// Obsidian's HTMLElement shim exposes appendText; add minimal polyfill for jsdom
(HTMLElement.prototype as any).appendText ??= function (text: string) {
this.appendChild(document.createTextNode(text));
};
// Lightweight DOM test for the settings section
@ -13,21 +19,22 @@ describe('Settings UI - User Fields (optional)', () => {
const plugin = new TaskNotesPlugin(app);
// inject defaults
(plugin as any).settings = { ...DEFAULT_SETTINGS };
(plugin as any).i18n = createI18nService();
(plugin as any).registerEvent = jest.fn();
const tab = new TaskNotesSettingTab(app, plugin);
tab.display();
// Switch to field-mapping tab
(tab as any).switchTab('field-mapping');
(tab as any).switchTab('task-properties');
const container = (tab as any).tabContents['field-mapping'];
const container = (tab as any).tabContents['task-properties'];
// Heading text may not render in mock; verify controls/descriptions instead
expect(container.textContent).toContain('User Fields (optional)');
expect(container.textContent).toContain('Define one or more custom frontmatter properties');
expect(container.textContent).toContain('Property Name');
expect(container.textContent).toContain('Custom User Fields');
expect(container.textContent).toContain('Define custom frontmatter properties');
expect(container.textContent).toContain('Display Name');
expect(container.textContent).toContain('Property Name');
expect(container.textContent).toContain('Type');
expect(container.textContent).toContain('Add field');
expect(container.textContent).toContain('Add user field');
});
});