diff --git a/src/components/code/CareerTimeline.astro b/src/components/code/CareerTimeline.astro
new file mode 100644
index 0000000..871f75b
--- /dev/null
+++ b/src/components/code/CareerTimeline.astro
@@ -0,0 +1,141 @@
+---
+import { getCollection, render } from "astro:content";
+import { Image } from "astro:assets";
+import { t, getDateLocale, type Locale } from "../../utils/i18n";
+import "../../styles/exp-card.css";
+
+interface Props {
+ locale: Locale;
+}
+
+const { locale } = Astro.props;
+
+const logos = import.meta.glob<{ default: ImageMetadata }>('../../assets/images/companies/*.png', { eager: true });
+
+function getLogo(filename?: string) {
+ if (!filename) return null;
+ const key = `../../assets/images/companies/${filename}`;
+ return logos[key]?.default ?? null;
+}
+
+function getInitials(company: string) {
+ return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
+}
+
+const typeColors: Record = {
+ employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' },
+ freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' },
+ teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' },
+ community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' },
+ entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' },
+};
+
+function formatMonth(dateStr: string) {
+ const [year, month] = dateStr.split('-');
+ return new Date(parseInt(year), parseInt(month) - 1)
+ .toLocaleDateString(getDateLocale(locale), { year: 'numeric', month: 'short' });
+}
+
+function formatDuration(years: number, months: number) {
+ const yearLabel = years > 1 ? t('career', 'durationYears', locale) : t('career', 'durationYear', locale);
+ const monthLabel = t('career', 'durationMonth', locale);
+ if (years === 0) return `${months} ${monthLabel}`;
+ if (months === 0) return `${years} ${yearLabel}`;
+ return `${years} ${yearLabel} ${months} ${monthLabel}`;
+}
+
+function computeDuration(startStr: string, endStr?: string) {
+ const [sy, sm] = startStr.split('-').map(Number);
+ const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1];
+ const totalMonths = Math.max(1, (end[0] - sy) * 12 + (end[1] - sm));
+ const years = Math.floor(totalMonths / 12);
+ const months = totalMonths % 12;
+ return formatDuration(years, months);
+}
+
+const experiences = (await getCollection("experiences"))
+ .filter((e) => e.data.lang === locale && !e.data.draft)
+ .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
+
+const presentLabel = t('career', 'present', locale);
+const logoAltPrefix = t('career', 'logoAlt', locale);
+---
+
+
+ {experiences.map(async (exp) => {
+ const { Content } = await render(exp);
+ const isOngoing = !exp.data.endDate;
+ const start = formatMonth(exp.data.startDate);
+ const end = exp.data.endDate ? formatMonth(exp.data.endDate) : presentLabel;
+ const duration = computeDuration(exp.data.startDate, exp.data.endDate);
+ const colors = typeColors[exp.data.type] || typeColors.employment;
+ const label = t('career', exp.data.type, locale);
+
+ return (
+
+
+
+
+ {getLogo(exp.data.logo) ? (
+
+
+
+ ) : (
+
+ {getInitials(exp.data.company)}
+
+ )}
+
+
+
+ {label}
+
+
+ {start} — {end}
+
+ · {duration}
+ {exp.data.location && · {exp.data.location}}
+
+
+
{exp.data.role}
+
+ {exp.data.companyUrl ? (
+ {exp.data.company}
+ ) : exp.data.company}
+
+
+
+
+
+
+ {exp.data.technologies && exp.data.technologies.length > 0 && (
+
+ {exp.data.technologies.map((tech: string) => (
+
+ {tech}
+
+ ))}
+
+ )}
+
+
+ );
+ })}
+
diff --git a/src/pages/ar/برمجة/مسار.astro b/src/pages/ar/برمجة/مسار.astro
index 9922699..22dd508 100644
--- a/src/pages/ar/برمجة/مسار.astro
+++ b/src/pages/ar/برمجة/مسار.astro
@@ -1,56 +1,6 @@
---
-import { getCollection, render } from "astro:content";
-import { Image } from "astro:assets";
import Layout from "../../../layouts/main.astro";
-import "../../../styles/exp-card.css";
-
-const logos = import.meta.glob<{ default: ImageMetadata }>('../../../assets/images/companies/*.png', { eager: true });
-
-function getLogo(filename?: string) {
- if (!filename) return null;
- const key = `../../../assets/images/companies/${filename}`;
- return logos[key]?.default ?? null;
-}
-
-function getInitials(company: string) {
- return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
-}
-
-const locale = "ar";
-
-const experiences = (await getCollection("experiences"))
- .filter((e) => e.data.lang === locale && !e.data.draft)
- .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
-
-const typeLabels: Record = {
- employment: 'وظيفة', freelance: 'مستقل', teaching: 'تدريس',
- community: 'مجتمع', entrepreneurship: 'ريادة أعمال',
-};
-
-const typeColors: Record = {
- employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' },
- freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' },
- teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' },
- community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' },
- entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' },
-};
-
-function formatMonth(dateStr: string) {
- const [year, month] = dateStr.split('-');
- return new Date(parseInt(year), parseInt(month) - 1)
- .toLocaleDateString('ar-SA', { year: 'numeric', month: 'short' });
-}
-
-function computeDuration(startStr: string, endStr?: string) {
- const [sy, sm] = startStr.split('-').map(Number);
- const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1];
- const totalMonths = (end[0] - sy) * 12 + (end[1] - sm);
- const years = Math.floor(totalMonths / 12);
- const months = totalMonths % 12;
- if (years === 0) return `${months} أشهر`;
- if (months === 0) return `${years} سنة`;
- return `${years} سنة ${months} أشهر`;
-}
+import CareerTimeline from "../../../components/code/CareerTimeline.astro";
---
أكثر من 20 سنة من الخبرة في تطوير البرمجيات.
-
- {experiences.map(async (exp) => {
- const { Content } = await render(exp);
- const isOngoing = !exp.data.endDate;
- const start = formatMonth(exp.data.startDate);
- const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'الحاضر';
- const duration = computeDuration(exp.data.startDate, exp.data.endDate);
- const colors = typeColors[exp.data.type] || typeColors.employment;
- const label = typeLabels[exp.data.type] || exp.data.type;
-
- return (
-
-
-
-
- {getLogo(exp.data.logo) ? (
-
-
-
- ) : (
-
- {getInitials(exp.data.company)}
-
- )}
-
-
-
- {label}
-
-
- {start} — {end}
-
- · {duration}
- {exp.data.location && · {exp.data.location}}
-
-
-
{exp.data.role}
-
- {exp.data.companyUrl ? (
- {exp.data.company}
- ) : exp.data.company}
-
-
-
-
-
-
- {exp.data.technologies && exp.data.technologies.length > 0 && (
-
- {exp.data.technologies.map((tech: string) => (
-
- {tech}
-
- ))}
-
- )}
-
-
- );
- })}
-
+
diff --git a/src/pages/code/parcours.astro b/src/pages/code/parcours.astro
index dc2531e..83c223b 100644
--- a/src/pages/code/parcours.astro
+++ b/src/pages/code/parcours.astro
@@ -1,56 +1,6 @@
---
-import { getCollection, render } from "astro:content";
-import { Image } from "astro:assets";
import Layout from "../../layouts/main.astro";
-import "../../styles/exp-card.css";
-
-const logos = import.meta.glob<{ default: ImageMetadata }>('../../assets/images/companies/*.png', { eager: true });
-
-function getLogo(filename?: string) {
- if (!filename) return null;
- const key = `../../assets/images/companies/${filename}`;
- return logos[key]?.default ?? null;
-}
-
-function getInitials(company: string) {
- return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
-}
-
-const locale = "fr";
-
-const experiences = (await getCollection("experiences"))
- .filter((e) => e.data.lang === locale && !e.data.draft)
- .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
-
-const typeLabels: Record = {
- employment: 'Emploi', freelance: 'Freelance', teaching: 'Enseignement',
- community: 'Communauté', entrepreneurship: 'Entrepreneuriat',
-};
-
-const typeColors: Record = {
- employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' },
- freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' },
- teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' },
- community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' },
- entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' },
-};
-
-function formatMonth(dateStr: string) {
- const [year, month] = dateStr.split('-');
- return new Date(parseInt(year), parseInt(month) - 1)
- .toLocaleDateString('fr-FR', { year: 'numeric', month: 'short' });
-}
-
-function computeDuration(startStr: string, endStr?: string) {
- const [sy, sm] = startStr.split('-').map(Number);
- const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1];
- const totalMonths = (end[0] - sy) * 12 + (end[1] - sm);
- const years = Math.floor(totalMonths / 12);
- const months = totalMonths % 12;
- if (years === 0) return `${months} mois`;
- if (months === 0) return `${years} an${years > 1 ? 's' : ''}`;
- return `${years} an${years > 1 ? 's' : ''} ${months} mois`;
-}
+import CareerTimeline from "../../components/code/CareerTimeline.astro";
---
Plus de 20 ans d'expérience en développement logiciel.
-
- {experiences.map(async (exp) => {
- const { Content } = await render(exp);
- const isOngoing = !exp.data.endDate;
- const start = formatMonth(exp.data.startDate);
- const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Présent';
- const duration = computeDuration(exp.data.startDate, exp.data.endDate);
- const colors = typeColors[exp.data.type] || typeColors.employment;
- const label = typeLabels[exp.data.type] || exp.data.type;
-
- return (
-
-
-
-
- {getLogo(exp.data.logo) ? (
-
-
-
- ) : (
-
- {getInitials(exp.data.company)}
-
- )}
-
-
-
- {label}
-
-
- {start} — {end}
-
- · {duration}
- {exp.data.location && · {exp.data.location}}
-
-
-
{exp.data.role}
-
- {exp.data.companyUrl ? (
- {exp.data.company}
- ) : exp.data.company}
-
-
-
-
-
-
- {exp.data.technologies && exp.data.technologies.length > 0 && (
-
- {exp.data.technologies.map((tech: string) => (
-
- {tech}
-
- ))}
-
- )}
-
-
- );
- })}
-
+
diff --git a/src/pages/en/code/career.astro b/src/pages/en/code/career.astro
index c1d0e63..667a4da 100644
--- a/src/pages/en/code/career.astro
+++ b/src/pages/en/code/career.astro
@@ -1,56 +1,6 @@
---
-import { getCollection, render } from "astro:content";
-import { Image } from "astro:assets";
import Layout from "../../../layouts/main.astro";
-import "../../../styles/exp-card.css";
-
-const logos = import.meta.glob<{ default: ImageMetadata }>('../../../assets/images/companies/*.png', { eager: true });
-
-function getLogo(filename?: string) {
- if (!filename) return null;
- const key = `../../../assets/images/companies/${filename}`;
- return logos[key]?.default ?? null;
-}
-
-function getInitials(company: string) {
- return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
-}
-
-const locale = "en";
-
-const experiences = (await getCollection("experiences"))
- .filter((e) => e.data.lang === locale && !e.data.draft)
- .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
-
-const typeLabels: Record = {
- employment: 'Employment', freelance: 'Freelance', teaching: 'Teaching',
- community: 'Community', entrepreneurship: 'Entrepreneurship',
-};
-
-const typeColors: Record = {
- employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' },
- freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' },
- teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' },
- community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' },
- entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' },
-};
-
-function formatMonth(dateStr: string) {
- const [year, month] = dateStr.split('-');
- return new Date(parseInt(year), parseInt(month) - 1)
- .toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
-}
-
-function computeDuration(startStr: string, endStr?: string) {
- const [sy, sm] = startStr.split('-').map(Number);
- const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1];
- const totalMonths = (end[0] - sy) * 12 + (end[1] - sm);
- const years = Math.floor(totalMonths / 12);
- const months = totalMonths % 12;
- if (years === 0) return `${months} mo`;
- if (months === 0) return `${years} yr${years > 1 ? 's' : ''}`;
- return `${years} yr${years > 1 ? 's' : ''} ${months} mo`;
-}
+import CareerTimeline from "../../../components/code/CareerTimeline.astro";
---
Over 20 years of experience in software development.
-
- {experiences.map(async (exp) => {
- const { Content } = await render(exp);
- const isOngoing = !exp.data.endDate;
- const start = formatMonth(exp.data.startDate);
- const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Present';
- const duration = computeDuration(exp.data.startDate, exp.data.endDate);
- const colors = typeColors[exp.data.type] || typeColors.employment;
- const label = typeLabels[exp.data.type] || exp.data.type;
-
- return (
-
-
-
-
- {getLogo(exp.data.logo) ? (
-
-
-
- ) : (
-
- {getInitials(exp.data.company)}
-
- )}
-
-
-
- {label}
-
-
- {start} — {end}
-
- · {duration}
- {exp.data.location && · {exp.data.location}}
-
-
-
{exp.data.role}
-
- {exp.data.companyUrl ? (
- {exp.data.company}
- ) : exp.data.company}
-
-
-
-
-
-
- {exp.data.technologies && exp.data.technologies.length > 0 && (
-
- {exp.data.technologies.map((tech: string) => (
-
- {tech}
-
- ))}
-
- )}
-
-
- );
- })}
-
+
diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts
index 9de4640..3532022 100644
--- a/src/utils/i18n.ts
+++ b/src/utils/i18n.ts
@@ -93,6 +93,18 @@ export const translations = {
nextImage: { fr: 'Image suivante', en: 'Next image', ar: 'الصورة التالية' },
goToImage: { fr: "Aller à l'image", en: 'Go to image', ar: 'انتقل إلى الصورة' },
},
+ career: {
+ present: { fr: 'Présent', en: 'Present', ar: 'الحاضر' },
+ logoAlt: { fr: 'Logo', en: 'Logo', ar: 'شعار' },
+ employment: { fr: 'Emploi', en: 'Employment', ar: 'وظيفة' },
+ freelance: { fr: 'Freelance', en: 'Freelance', ar: 'مستقل' },
+ teaching: { fr: 'Enseignement', en: 'Teaching', ar: 'تدريس' },
+ community: { fr: 'Communauté', en: 'Community', ar: 'مجتمع' },
+ entrepreneurship: { fr: 'Entrepreneuriat', en: 'Entrepreneurship', ar: 'ريادة أعمال' },
+ durationYear: { fr: 'an', en: 'yr', ar: 'سنة' },
+ durationYears: { fr: 'ans', en: 'yrs', ar: 'سنة' },
+ durationMonth: { fr: 'mois', en: 'mo', ar: 'أشهر' },
+ },
projects: {
visitSite: { fr: 'Voir le site', en: 'Visit site', ar: 'زيارة الموقع' },
viewOnGithub: { fr: 'Voir sur GitHub', en: 'View on GitHub', ar: 'عرض على GitHub' },