mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
feat: complete fr locale and fix i18n tooling
This commit is contained in:
parent
eb7be73987
commit
c721a67230
12 changed files with 4936 additions and 1279 deletions
|
|
@ -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
|
||||
|
|
|
|||
6045
i18n.state.json
6045
i18n.state.json
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue