diff --git a/package-lock.json b/package-lock.json
index 553197b..1b23799 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"devDependencies": {
"autoprefixer": "^10.4.17",
"handlebars": "^4.7.8",
+ "js-yaml": "^4.1.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"vite": "^5.1.0"
@@ -729,6 +730,13 @@
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
"node_modules/autoprefixer": {
"version": "10.4.18",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz",
@@ -1297,6 +1305,19 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
diff --git a/package.json b/package.json
index 454aa50..6f876cc 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"devDependencies": {
"autoprefixer": "^10.4.17",
"handlebars": "^4.7.8",
+ "js-yaml": "^4.1.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"vite": "^5.1.0"
diff --git a/roadmap/index.html b/roadmap/index.html
index 1b331c8..19b6a27 100644
--- a/roadmap/index.html
+++ b/roadmap/index.html
@@ -31,6 +31,7 @@
À propos
Architecture
Roadmap
+
Événements
Communauté
Architecture
Roadmap
+
Événements
Communauté
diff --git a/scripts/events.js b/scripts/events.js
new file mode 100644
index 0000000..c720b05
--- /dev/null
+++ b/scripts/events.js
@@ -0,0 +1,228 @@
+const yaml = require('js-yaml');
+const fs = require('fs');
+
+/**
+ * Get a localized field value with fallback support
+ * @param {string|object} field - The field to localize (can be string or object with locale keys)
+ * @param {string} locale - Target locale (e.g., 'en', 'fr')
+ * @param {string} fallbackLocale - Fallback locale if target not found
+ * @returns {string} Localized value
+ */
+function getLocalizedField(field, locale, fallbackLocale = 'en') {
+ if (!field) return '';
+ if (typeof field === 'string') return field; // Direct string fallback
+ return field[locale] || field[fallbackLocale] || '';
+}
+
+/**
+ * Get compact ISO date string for display
+ * @param {string|object} dateInfo - Date or date range
+ * @returns {string} Compact ISO representation
+ */
+function getISODate(dateInfo) {
+ if (typeof dateInfo === 'string') {
+ return dateInfo;
+ }
+
+ if (dateInfo && typeof dateInfo === 'object' && dateInfo.start) {
+ if (!dateInfo.end) return dateInfo.start;
+
+ // Same month: 2026-06-11 → 12
+ if (dateInfo.start.slice(0, 7) === dateInfo.end.slice(0, 7)) {
+ return `${dateInfo.start} → ${dateInfo.end.slice(8)}`;
+ }
+
+ // Different month/year: 2026-06-11 → 2026-06-12
+ return `${dateInfo.start} → ${dateInfo.end}`;
+ }
+
+ return '';
+}
+
+/**
+ * Check if an event is in the past
+ * @param {object} event - Event object
+ * @returns {boolean} True if event has passed
+ */
+function isPastEvent(event) {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ let compareDate;
+ if (typeof event.date === 'string') {
+ compareDate = new Date(event.date);
+ } else if (event.date && event.date.end) {
+ compareDate = new Date(event.date.end);
+ } else if (event.date && event.date.start) {
+ compareDate = new Date(event.date.start);
+ } else {
+ return false;
+ }
+
+ return compareDate < today;
+}
+
+/**
+ * Get sort date for an event
+ * @param {object} event - Event object
+ * @returns {Date} Date to use for sorting
+ */
+function getEventSortDate(event) {
+ if (typeof event.date === 'string') {
+ return new Date(event.date);
+ }
+ if (event.date && event.date.start) {
+ return new Date(event.date.start);
+ }
+ return new Date(0);
+}
+
+/**
+ * Localize events array for a specific language and separate into upcoming/past
+ * @param {Array} events - Array of event objects
+ * @param {string} locale - Target locale
+ * @returns {Object} Object with upcoming, past, and pastGrouped arrays
+ */
+function localizeEvents(events, locale) {
+ if (!events || !Array.isArray(events)) {
+ return { upcoming: [], past: [], pastGrouped: [] };
+ }
+
+ const localized = events.map(event => ({
+ id: event.id,
+ date: event.date,
+ dateISO: getISODate(event.date),
+ source: event.source || null,
+ title: getLocalizedField(event.title, locale),
+ description: getLocalizedField(event.description, locale) || null,
+ location: getLocalizedField(event.location, locale) || null,
+ isPast: isPastEvent(event),
+ year: getEventSortDate(event).getFullYear(),
+ }));
+
+ // Separate and sort
+ const upcoming = localized
+ .filter(e => !e.isPast)
+ .sort((a, b) => getEventSortDate(a) - getEventSortDate(b));
+
+ const past = localized
+ .filter(e => e.isPast)
+ .sort((a, b) => getEventSortDate(b) - getEventSortDate(a));
+
+ // Group past events by year
+ const pastByYear = new Map();
+ for (const event of past) {
+ const year = event.year;
+ if (!pastByYear.has(year)) {
+ pastByYear.set(year, []);
+ }
+ pastByYear.get(year).push(event);
+ }
+
+ const pastGrouped = Array.from(pastByYear.entries())
+ .sort((a, b) => b[0] - a[0])
+ .map(([year, items]) => ({ year, items }));
+
+ return { upcoming, past, pastGrouped };
+}
+
+/**
+ * Validate events data structure
+ * @param {Array} events - Array of event objects
+ * @param {Array} supportedLocales - Array of supported locale codes
+ */
+function validateEvents(events, supportedLocales = ['en', 'fr']) {
+ if (!events || !Array.isArray(events)) {
+ console.warn('⚠️ No events array found');
+ return;
+ }
+
+ const errors = [];
+ const warnings = [];
+
+ events.forEach((event, index) => {
+ // Required fields
+ if (!event.title) {
+ errors.push(`Event ${event.id || index} missing required field: title`);
+ }
+ if (!event.date) {
+ errors.push(`Event ${event.id || index} missing required field: date`);
+ }
+
+ // Date validation
+ if (event.date && typeof event.date === 'object') {
+ if (!event.date.start) {
+ errors.push(`Event ${event.id || index} has date range but missing 'start'`);
+ }
+ if (!event.date.end) {
+ errors.push(`Event ${event.id || index} has date range but missing 'end'`);
+ }
+
+ if (event.date.start && event.date.end) {
+ const start = new Date(event.date.start);
+ const end = new Date(event.date.end);
+ if (start > end) {
+ errors.push(`Event ${event.id || index} has end date before start date`);
+ }
+ }
+ }
+
+ // Translation validation
+ ['title', 'description', 'location'].forEach(field => {
+ if (event[field] && typeof event[field] === 'object') {
+ supportedLocales.forEach(locale => {
+ if (!event[field][locale]) {
+ warnings.push(`Event ${event.id || index} missing ${locale} translation for: ${field}`);
+ }
+ });
+ }
+ });
+ });
+
+ if (warnings.length > 0) {
+ console.warn('⚠️ Event warnings:');
+ warnings.forEach(w => console.warn(` ${w}`));
+ }
+
+ if (errors.length > 0) {
+ console.error('❌ Event validation failed:');
+ errors.forEach(e => console.error(` ${e}`));
+ throw new Error('Event validation failed');
+ }
+
+ console.log(`✅ Validated ${events.length} event(s)`);
+}
+
+/**
+ * Load and parse events from YAML file
+ * @param {string} eventsPath - Path to events YAML file
+ * @returns {Array} Array of event objects
+ */
+function loadEvents(eventsPath) {
+ try {
+ if (!fs.existsSync(eventsPath)) {
+ console.warn(`⚠️ Events file not found at ${eventsPath}`);
+ return [];
+ }
+
+ const fileContent = fs.readFileSync(eventsPath, 'utf8');
+ const data = yaml.load(fileContent);
+
+ if (!data || !data.events) {
+ console.warn('⚠️ No events found in YAML file');
+ return [];
+ }
+
+ validateEvents(data.events);
+
+ return data.events;
+ } catch (error) {
+ console.error(`❌ Error loading events: ${error.message}`);
+ return [];
+ }
+}
+
+module.exports = {
+ loadEvents,
+ localizeEvents,
+};
diff --git a/scripts/generate.js b/scripts/generate.js
index e1e2f74..5667cfd 100644
--- a/scripts/generate.js
+++ b/scripts/generate.js
@@ -1,12 +1,14 @@
const Handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
+const { loadEvents, localizeEvents } = require('./events');
const srcDir = path.join(__dirname, '../src');
const localesDir = path.join(srcDir, 'locales');
const partialsDir = path.join(srcDir, 'partials');
const templatePath = path.join(srcDir, 'template.html');
const roadmapTemplatePath = path.join(srcDir, 'roadmap-template.html');
+const eventsPath = path.join(srcDir, 'events.yaml');
console.log('Generating sites...');
@@ -39,6 +41,9 @@ Handlebars.registerHelper('computeStatus', function (features) {
const mainTemplate = Handlebars.compile(fs.readFileSync(templatePath, 'utf8'));
const roadmapTemplate = Handlebars.compile(fs.readFileSync(roadmapTemplatePath, 'utf8'));
+// Load events
+const events = loadEvents(eventsPath);
+
const languages = [
{ code: 'fr', isDefault: true },
{ code: 'en', isDefault: false }
@@ -57,6 +62,7 @@ languages.forEach(lang => {
const context = {
...content,
+ eventsList: localizeEvents(events, lang.code),
currentLang: lang.code,
frUrl: lang.code === 'fr' ? '#' : '../',
enUrl: lang.code === 'en' ? '#' : (lang.isDefault ? 'en/' : '../en/'),
diff --git a/src/events.yaml b/src/events.yaml
new file mode 100644
index 0000000..508bf35
--- /dev/null
+++ b/src/events.yaml
@@ -0,0 +1,67 @@
+events:
+ - date:
+ start: "2025-12-10"
+ end: "2025-12-11"
+ title: OSXP (Open Source eXPérience)
+ location: Cité des Sciences et de l'Industrie, Paris
+
+ - date: "2026-02-03"
+ title:
+ fr: Cloud Native Days France 2026
+ en: Cloud Native Days France 2026
+ description:
+ fr: avec stand TOSIT
+ en: with TOSIT booth
+ location: CENTQUATRE-PARIS, Paris
+
+ - date: "2026-02-12"
+ title:
+ fr: Présentation d'OKDP à Wescale (ESN)
+ en: OKDP presentation at Wescale
+ location: remote
+
+ - date: "2026-02-18"
+ title:
+ fr: Présentation d'OKDP à Tasmane / Agirc-Arrco
+ en: OKDP presentation at Tasmane / Agirc-Arrco
+ location: remote
+
+ - date: "2026-03-27"
+ title:
+ fr: Présentation d'OKDP à Kiira
+ en: OKDP presentation at Kiira
+ location: remote
+
+ - date: "2026-04-03"
+ title:
+ fr: Présentation d'OKDP à Arkéa
+ en: OKDP presentation at Arkéa
+ location: remote
+
+ - date: "2026-04-21"
+ title:
+ fr: Présentation d'OKDP à l'INERIS
+ en: OKDP presentation at INERIS
+ location: remote
+
+ - date: "2026-04-22"
+ title:
+ fr: Présentation d'OKDP à Devoxx Paris
+ en: OKDP presentation at Devoxx Paris
+ location: Palais des Congrès, Paris
+
+ - date: "2026-05-05"
+ title:
+ fr: Présentation d'OKDP au Ministère de l'Agriculture, de l'Agro-alimentaire et de la Souveraineté alimentaire
+ en: OKDP presentation at the French Ministry of Agriculture and Food Sovereignty
+ location: remote
+
+ - date: "2026-05-07"
+ title:
+ fr: Présentation d'OKDP à Docaposte
+ en: OKDP presentation at Docaposte
+ location: remote
+
+ - date: "2026-06-04"
+ title: TOSIT-Day
+ location: Caisse des Dépôts et Consignations
diff --git a/src/locales/en.json b/src/locales/en.json
index 75bf042..512cbaf 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -8,6 +8,7 @@
"about": "About",
"architecture": "Architecture",
"roadmap": "Roadmap",
+ "events": "Events",
"community": "Community"
},
"hero": {
@@ -185,6 +186,14 @@
"desc2": "The association brings together numerous companies and administrations, including BPCE (Banque Populaire, Caisse d'Epargne et Natixis), Société Générale, among others. It also hosts the TDP project, initiated by DGFiP and EDF.",
"desc3": "Participation in TOSIT projects is open to all."
},
+ "events": {
+ "title": "Events",
+ "subtitle": "OKDP presentations, conferences, and community gatherings.",
+ "upcoming": "Upcoming",
+ "past": "Past Events",
+ "noUpcoming": "No upcoming events at the moment.",
+ "noEvents": "No events at the moment."
+ },
"footer": {
"copyright": "© 2026 TOSIT — The Open Source I Trust.",
"license": "Crafted by the community • Licensed under Apache License v2.0"
diff --git a/src/locales/fr.json b/src/locales/fr.json
index bf18f74..4534369 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -8,6 +8,7 @@
"about": "À propos",
"architecture": "Architecture",
"roadmap": "Roadmap",
+ "events": "Événements",
"community": "Communauté"
},
"hero": {
@@ -185,6 +186,14 @@
"desc2": "L'association rassemble de nombreuses entreprises et administrations, dont BPCE (Banque Populaire, Caisse d'Epargne et Natixis), Société Générale, entre autres. Elle héberge également le projet TDP, initié par la DGFiP et EDF.",
"desc3": "La participation aux projets TOSIT est ouverte à tous."
},
+ "events": {
+ "title": "Événements",
+ "subtitle": "Présentations OKDP, conférences et rassemblements communautaires.",
+ "upcoming": "À venir",
+ "past": "Événements passés",
+ "noUpcoming": "Aucun événement à venir pour le moment.",
+ "noEvents": "Aucun événement pour le moment."
+ },
"footer": {
"copyright": "© 2026 TOSIT — The Open Source I Trust.",
"license": "Façonné par la communauté • Sous licence Apache V2.0"
diff --git a/src/partials/events.html b/src/partials/events.html
new file mode 100644
index 0000000..3b138ab
--- /dev/null
+++ b/src/partials/events.html
@@ -0,0 +1,92 @@
+
+
+
+
{{ events.title }}
+
{{ events.subtitle }}
+
+
+ {{#if eventsList}}
+ {{#if eventsList.upcoming}}
+
+
{{ events.upcoming }}
+
+ {{#each eventsList.upcoming}}
+
+
{{this.dateISO}}
+
+ {{#if this.location}}
+
{{this.location}}
+ {{/if}}
+
+ {{/each}}
+
+ {{else}}
+
{{ events.upcoming }}
+
+
{{ events.noUpcoming }}
+
+ {{/if}}
+
+ {{#if eventsList.pastGrouped}}
+
+
+
+
+ {{ events.past }} ({{eventsList.past.length}})
+
+
+ {{#each eventsList.pastGrouped}}
+
+ ── {{this.year}}
+
+
+ {{#each this.items}}
+
+
{{this.dateISO}}
+
+
{{this.title}}
+ {{#if this.description}}
+
{{this.description}}
+ {{/if}}
+ {{#if this.source}}
+
+
+ {{this.source}}
+
+ {{/if}}
+
+ {{#if this.location}}
+
{{this.location}}
+ {{/if}}
+
+ {{/each}}
+ {{/each}}
+
+
+ {{/if}}
+ {{else}}
+
+
{{ events.noEvents }}
+
+ {{/if}}
+
+
diff --git a/src/partials/header.html b/src/partials/header.html
index ac72d94..a344d05 100644
--- a/src/partials/header.html
+++ b/src/partials/header.html
@@ -12,6 +12,7 @@
{{ nav.about }}
{{ nav.architecture }}
{{ nav.roadmap }}
+
{{ nav.events }}
{{ nav.community }}
{{ nav.architecture }}
{{ nav.roadmap }}
+
{{ nav.events }}
{{ nav.community }}
diff --git a/src/template.html b/src/template.html
index b42b933..fc2f6e1 100644
--- a/src/template.html
+++ b/src/template.html
@@ -267,6 +267,9 @@ {{ community.contribute.title }}
+
+ {{> events }}
+