diff --git a/src/App.jsx b/src/App.jsx index c08da90..27717b1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,27 +1,43 @@ -import AboutMe from "./components/AboutMe.jsx"; -import Education from "./components/Education.jsx"; -import Experience from "./components/Experience.jsx"; -import Skills from "./components/Skills.jsx"; -import Projects from "./components/Projects.jsx"; -import Navbar from "./components/Navbar.jsx"; - +import Navbar from './components/Navbar' +import AboutMe from './components/AboutMe' +import Skills from './components/Skills' +import Education from './components/Education' +import Experience from './components/Experience' +import Projects from './components/Projects' +import {LanguageProvider} from "./contexts/LanguageProvider.jsx"; function App() { + return ( + <> + + {/* Mobile: no grid, Tablet+: grid with sidebar column */} + {/* Using minmax(0, 1fr) for content column to allow proper shrinking */} +
+ + {/* Sidebar Column - only exists on desktop */} + + + {/* Main Content Column */} +
+ {/* Mobile navbar (shows outside grid) */} +
+ +
+ + + + + + +
+ +
+
+ -return ( - <> -
- -
- - - - - -
-
- -) + ) } -export default App +export default App \ No newline at end of file diff --git a/src/assets/images/cimf_logo.png b/src/assets/images/cimf_logo.png new file mode 100644 index 0000000..3c73f45 Binary files /dev/null and b/src/assets/images/cimf_logo.png differ diff --git a/src/components/AboutMe.jsx b/src/components/AboutMe.jsx index 30e70ef..40baff4 100644 --- a/src/components/AboutMe.jsx +++ b/src/components/AboutMe.jsx @@ -1,54 +1,56 @@ -import profileImg from '../assets/images/cropped-profile.jpg' -import resumePdf from '../assets/resume.pdf' - import { FaLinkedin, FaGithub, FaFileDownload } from 'react-icons/fa' -import SectionArrow from "./SectionArrow.jsx"; - +import SectionArrow from "./SectionArrow.jsx" +import { useLanguage } from '../contexts/useLanguage.js' +import { getText } from '../utils/translationHelpers.js' +import { aboutData } from './data/about.js' function AboutMe() { + const { language } = useLanguage() return ( -
+
-
-
+
+
-

Luan Tran

-

- I'm a Master's student in Applied Computer Science with industry experience in automation and web development at Broadsign, Ericsson, and Matrox. My projects include deploying automated CI/CD workflows, multimodal medical imaging applications, and automated language proficiency assessment systems. +

+ {aboutData.name} +

+

+ {getText(aboutData.description1, language)}

-

- I'm currently researching LLM-based agents for linguistic education and seeking opportunities in software development and machine learning. +

+ {getText(aboutData.description2, language)}

- -
+
Luan Tran profile picture
diff --git a/src/components/Education.jsx b/src/components/Education.jsx index 722e1b3..ea18a46 100644 --- a/src/components/Education.jsx +++ b/src/components/Education.jsx @@ -1,72 +1,98 @@ import SectionArrow from "./SectionArrow.jsx"; -import {educationData} from "./data/education.js" +import { educationData } from "./data/education.js"; +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText } from '../utils/translationHelpers'; -const EducationItem = ({ data }) => { - const { university, logo, degree, date, description, gpa } = data +const EducationItem = ({ data, index, language }) => { + const { university, logo, degree, date } = data; return ( -
+
0 ? 'my-[5px] sm:-mt-10 md:-mt-12 lg:-mt-14 xl:-mt-16' : 'my-[5px] sm:my-[3px]'} flex w-full sm:w-1/2 justify-start sm:justify-end sm:pr-[22px] sm:odd:justify-start sm:odd:self-end sm:odd:pl-[22px] sm:odd:pr-0 md:pr-[30px] md:odd:pl-[30px]`}>
+ sm:group-odd:after:left-[-7.5px] + sm:group-odd:after:right-auto + sm:group-odd:after:border-l + sm:group-odd:after:border-t + sm:group-odd:after:border-r-0 + sm:group-odd:after:border-b-0"> {/* University Logo */} {`${university} {/* Content */} -
-

{university}

-

{degree}

- +
+

+ {getText(university, language)} +

+

+ {getText(degree, language)} +

+
{/* Timeline dot */} - +
- ) + ); } function Education() { + const { language } = useLanguage(); + + const sectionTitle = { + en: "Education", + fr: "Éducation" + }; + return (
-
-

- Education +
+

+ {getText(sectionTitle, language)}

-
+ {/* Timeline */} +
{educationData.map((data, idx) => ( - + ))}

- ) + ); } export default Education \ No newline at end of file diff --git a/src/components/Experience.jsx b/src/components/Experience.jsx index 2f15419..a694f5b 100644 --- a/src/components/Experience.jsx +++ b/src/components/Experience.jsx @@ -1,32 +1,42 @@ import SectionArrow from "./SectionArrow.jsx"; -import {experienceData} from "./data/experience.js"; - -const ExperienceItem = ({ data }) => { +import { experienceData } from "./data/experience.js"; +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText, getArray } from '../utils/translationHelpers'; +const ExperienceItem = ({ data, language }) => { return ( -
-
- -
-
-
- {data.hash} -

{data.title}

-

{data.company} -

+
+
+ +
+
+
+ {data.hash} +

+ {getText(data.title, language)} +

+

+ + {data.company} + +

- {data.period} + + {getText(data.period, language)} +
-
- {data.achievements.map((achievementPoint, index) => ( -
+
+ {getArray(data.achievements, language).map((achievementPoint, index) => ( +
+{achievementPoint}
))}
-
+
{data.tech.map((techPoint, index) => ( - {techPoint} + + {techPoint} + ))}
@@ -35,19 +45,26 @@ const ExperienceItem = ({ data }) => { } function Experience() { + const { language } = useLanguage(); + + const sectionTitle = { + en: "Work Experience", + fr: "Expérience Professionnelle" + }; + return (
-
-
-

- Work Experience +
+
+

+ {getText(sectionTitle, language)}

-
+
{experienceData.map((experience) => ( - + ))}
diff --git a/src/components/LanguageSwitcher.jsx b/src/components/LanguageSwitcher.jsx new file mode 100644 index 0000000..fad2527 --- /dev/null +++ b/src/components/LanguageSwitcher.jsx @@ -0,0 +1,59 @@ +import { useLanguage } from '../contexts/useLanguage'; + +function LanguageSwitcher() { + const { language, setLanguage } = useLanguage(); + + return ( +
+ {/* Mobile */} +
+ + +
+ + {/* Desktop */} +
+ + +
+
+ ); +} + +export default LanguageSwitcher; diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 31a1e96..f69b6fe 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,8 +1,13 @@ import { useState, useEffect } from 'react' -import { FaUser, FaLightbulb, FaGraduationCap, FaBriefcase, FaFolderOpen } from 'react-icons/fa' +import { FaUser, FaLightbulb, FaGraduationCap, FaBriefcase, FaFolderOpen, FaBars, FaTimes } from 'react-icons/fa' +import LanguageSwitcher from './LanguageSwitcher' +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText } from '../utils/translationHelpers' function Navbar() { const [activeSection, setActiveSection] = useState('about') + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) + const { language } = useLanguage() useEffect(() => { const handleScroll = () => { @@ -24,61 +29,102 @@ function Navbar() { return () => window.removeEventListener('scroll', handleScroll) }, []) + const handleNavClick = () => { + setIsMobileMenuOpen(false) + } + + // Navigation labels with translations + const navLabels = { + about: { en: 'About', fr: 'À propos' }, + skills: { en: 'Skills', fr: 'Compétences' }, + education: { en: 'Education', fr: 'Éducation' }, + experience: { en: 'Experience', fr: 'Expérience' }, + projects: { en: 'Projects', fr: 'Projets' } + } + + const navItems = [ + { id: 'about', icon: FaUser, label: navLabels.about }, + { id: 'skills', icon: FaLightbulb, label: navLabels.skills }, + { id: 'education', icon: FaGraduationCap, label: navLabels.education }, + { id: 'experience', icon: FaBriefcase, label: navLabels.experience }, + { id: 'projects', icon: FaFolderOpen, label: navLabels.projects } + ] + return ( - + <> + {/* Mobile Top Bar */} +
+
+ LT + + {/* Mobile: Language Switcher + Menu Button */} +
+ + +
+
+ + {/* Mobile Dropdown Menu */} + {isMobileMenuOpen && ( +
+ +
+ )} +
+ + {/* Desktop Side Navigation */} +
+ + + {/* Desktop: Language Switcher at bottom */} +
+ +
+
+ ) } diff --git a/src/components/Projects.jsx b/src/components/Projects.jsx index 8372f70..fd74e02 100644 --- a/src/components/Projects.jsx +++ b/src/components/Projects.jsx @@ -1,10 +1,13 @@ -import { ExternalLink, Code } from 'lucide-react'; +import { ExternalLink } from 'lucide-react'; import { projectData } from './data/projects.js'; -import {useState} from "react"; +import { useState } from "react"; +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText, getArray } from '../utils/translationHelpers'; -const ProjectCard = ({ project }) => { +const ProjectCard = ({ project, language }) => { const [showGif, setShowGif] = useState(false); const [imageLoaded, setImageLoaded] = useState(false); + return (
{/* Animated Glow Border */} @@ -19,17 +22,15 @@ const ProjectCard = ({ project }) => { onMouseEnter={() => setShowGif(true)} onMouseLeave={() => setShowGif(false)} > - {/* Placeholder - shows while image loads */} {!imageLoaded && (
Loading...
)} - {/* Actual Image */} {project.title} setImageLoaded(true)} @@ -40,44 +41,40 @@ const ProjectCard = ({ project }) => {
{/* Card Header */} -
- - {/* Category Badge */} -
- {project.category} -
- - {/* Title + Description */} -
-

- {project.title} -

-

- {project.description} -

-
+
+ {/* Category Badge */} +
+ {getText(project.category, language)} +
+ {/* Title + Description */} +
+

+ {getText(project.title, language)} +

+

+ {getText(project.description, language)} +

+
{/* Card Body */} -
- +
{/* Tech Stack Tags */} -
+
{project.techStack.map((tech, index) => (
{tech}
))} -
{/* Feature List */} -
); }; function Projects() { + const { language } = useLanguage(); + + const sectionTitle = { + en: "Projects", + fr: "Projets" + }; + return ( -
+
-

- Projects +

+ {getText(sectionTitle, language)}

-
+
{projectData.map((project, index) => ( - + ))}
- ) + ); } export default Projects \ No newline at end of file diff --git a/src/components/Skills.jsx b/src/components/Skills.jsx index 5156231..8f21f55 100644 --- a/src/components/Skills.jsx +++ b/src/components/Skills.jsx @@ -1,32 +1,44 @@ import SectionArrow from "./SectionArrow.jsx"; -import {skillsData} from "./data/skills.js"; +import { skillsData } from "./data/skills.js"; import { iconMap } from "../utils/iconMap.js"; +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText } from '../utils/translationHelpers'; function Skills() { + const { language } = useLanguage(); + + const sectionTitle = { + en: "Skills", + fr: "Compétences" + }; + const renderDomainCard = (domain) => { const MainIcon = iconMap[domain.icon]; return ( -
-
- {/* Column 1: Domain Name with Icon (25%) */} -
- -

- {domain.name} +
+
+ + {/* Domain Name with Icon */} +
+ +

+ {getText(domain.name, language)}

- {/* Column 3: Related Skills Icons (50%) */} -
+ {/* Related Skills Icons */} +
{domain.relatedSkills.map((skill) => { const SkillIcon = iconMap[skill.icon]; return (
-
- +
+
-

{skill.displayName}

+

+ {skill.displayName} +

); })} @@ -41,13 +53,13 @@ function Skills() { return (
-
-

- Skills +
+

+ {getText(sectionTitle, language)}

-
-
+
+
{domains.map((domain) => renderDomainCard(domain))}
diff --git a/src/components/data/about.js b/src/components/data/about.js new file mode 100644 index 0000000..bcd999c --- /dev/null +++ b/src/components/data/about.js @@ -0,0 +1,26 @@ +import resumePdf from '../../assets/resume.pdf' +import profileImg from '../../assets/images/cropped-profile.jpg' + +export const aboutData = { + name: "Luan Tran", // Name doesn't need translation + description1: { + en: "I'm a Master's student in Applied Computer Science with industry experience in automation and web development " + + "at Broadsign, Ericsson, and Matrox. My projects include deploying automated CI/CD workflows," + + " multimodal medical imaging applications, and automated language proficiency assessment systems.", + fr: "Je suis étudiant en maîtrise en informatique appliquée avec une expérience industrielle en automatisation et développement web " + + "chez Broadsign, Ericsson et Matrox. Mes projets incluent le déploiement de workflows CI/CD automatisés," + + " des applications d'imagerie médicale multimodale et des systèmes d'évaluation automatique de compétence linguistique." + }, + description2: { + en: "I'm currently researching LLM-based agents for linguistic education and seeking opportunities in " + + "software development and machine learning.", + fr: "Je fais actuellement de la recherche sur les agents basés sur les LLM pour l'enseignement linguistique et je recherche des opportunités en " + + "développement logiciel et apprentissage automatique." + }, + downloadCV: { + en: "Download CV", + fr: "Télécharger CV" + }, + resume: resumePdf, + image: profileImg +} \ No newline at end of file diff --git a/src/components/data/education.js b/src/components/data/education.js index 97dc0ff..020afd6 100644 --- a/src/components/data/education.js +++ b/src/components/data/education.js @@ -1,18 +1,43 @@ // Import logos import mcgillLogo from '../../assets/images/mcgill_logo.png' import concordiaLogo from '../../assets/images/concordia_logo.png' +import cimfLogo from '../../assets/images/cimf_logo.png' export const educationData = [ { - university: 'Concordia University', + university: { + en: 'Concordia University', + fr: 'Université Concordia' + }, date: '2025 - 2027', logo: concordiaLogo, - degree: 'Master\'s of Applied Computer Science' + degree: { + en: "Master's of Applied Computer Science", + fr: "Maîtrise en informatique appliquée" + } }, { - university: 'McGill University', + university: { + en: 'McGill University', + fr: 'Université McGill' + }, date: '2014 - 2019', logo: mcgillLogo, - degree: 'Bachelors\'s of Computer Engineering' + degree: { + en: "Bachelor's of Computer Engineering", + fr: "Baccalauréat en génie informatique" + } + }, + { + university: { + en: 'Collège Marie de France', + fr: 'Collège Marie de France' + }, + date: '2012-2014', + logo: cimfLogo, + degree: { + en: 'French Baccalaureate - Highest Honors', + fr: 'Baccalauréat français - Mention très bien' + } } ] \ No newline at end of file diff --git a/src/components/data/experience.js b/src/components/data/experience.js index 69e453a..66df1a4 100644 --- a/src/components/data/experience.js +++ b/src/components/data/experience.js @@ -1,56 +1,81 @@ export const experienceData = [ { - hash: "a3f52b9", // or auto-generate - title: "Web Automation Developer Intern", - company: "Broadsign", - period: "Jan - May 2019", - // description: "Brief overview...", - achievements: [ - "Designed a new automation framework that accurately reproduce a live environment against which integration\n" + - "and system tests can be run", - "Automated the clean deployment and database population of a web server using Docker and Bitbucket Pipelines", - "Developed a Python REST tool library which developers could use to write their unit and integration tests" - ], - // projects: [ // optional array - // { name: "Dashboard Redesign", link: "..." } - // ], - // impact: "40% increase in engagement", // optional + hash: "a3f52b9", + title: { + en: "Web Automation Developer Intern", + fr: "Stagiaire développeur en automatisation web" + }, + company: "Broadsign", // Company name stays the same + period: { + en: "Jan - May 2019", + fr: "Jan - Mai 2019" + }, + achievements: { + en: [ + "Designed a new automation framework that accurately reproduce a live environment against which integration and system tests can be run", + "Automated the clean deployment and database population of a web server using Docker and Bitbucket Pipelines", + "Developed a Python REST tool library which developers could use to write their unit and integration tests" + ], + fr: [ + "Conçu un nouveau framework d'automatisation qui reproduit fidèlement un environnement réel pour exécuter des tests d'intégration et système", + "Automatisé le déploiement propre et le peuplement de base de données d'un serveur web avec Docker et Bitbucket Pipelines", + "Développé une bibliothèque d'outils REST en Python que les développeurs peuvent utiliser pour écrire leurs tests unitaires et d'intégration" + ] + }, tech: ["Python", "Docker", "Bitbucket"], }, { - hash: "7c2d81e", // or auto-generate - title: "Software Developer Intern", + hash: "7c2d81e", + title: { + en: "Software Developer Intern", + fr: "Stagiaire développeur logiciel" + }, company: "Ericsson", - period: "May - Dec 2018", - // description: "Brief overview...", - achievements: [ - "Automated software installation processes to decrease runtime by 15% using Ansible", - "Implemented a build certification process in custom Concourse to improve testing team’s efficiency by 75%", - "Developed automated Oracle backup and restore solution to decrease Concourse jobs’ runtime by 20%", - "Proposed and implemented new dispatch process for CI infrastructure by integrating Mantis API to Concourse" - ], - // projects: [ // optional array - // { name: "Dashboard Redesign", link: "..." } - // ], - // impact: "40% increase in engagement", // optional + period: { + en: "May - Dec 2018", + fr: "Mai - Déc 2018" + }, + achievements: { + en: [ + "Automated software installation processes to decrease runtime by 15% using Ansible", + "Implemented a build certification process in custom Concourse to improve testing team's efficiency by 75%", + "Developed automated Oracle backup and restore solution to decrease Concourse jobs' runtime by 20%", + "Proposed and implemented new dispatch process for CI infrastructure by integrating Mantis API to Concourse" + ], + fr: [ + "Automatisé les processus d'installation logicielle pour réduire le temps d'exécution de 15% avec Ansible", + "Implémenté un processus de certification de build dans Concourse personnalisé pour améliorer l'efficacité de l'équipe de test de 75%", + "Développé une solution automatisée de sauvegarde et restauration Oracle pour réduire le temps d'exécution des jobs Concourse de 20%", + "Proposé et implémenté un nouveau processus de dispatch pour l'infrastructure CI en intégrant l'API Mantis à Concourse" + ] + }, tech: ["Python", "Mantis", "Concourse", "Ansible"], }, { - hash: "1f9a3bc", // or auto-generate - title: "Automation SQA Intern", + hash: "1f9a3bc", + title: { + en: "Automation SQA Intern", + fr: "Stagiaire SQA en automatisation" + }, company: "Matrox", - period: "Jan - Aug 2017", - // description: "Brief overview...", - achievements: [ - "Created a new automated testing workflow to improve reduce weekly manual testing by 10 hours", - "Integrated Silktest, Jenkins and Testrail API using Python scripts to 100% automate log parsing, reporting and archiving", - "Set up, configured and managed Jenkins servers and slaves", - "Developed Python and 4Test test suites to perform functional and performance tests" - ], - // projects: [ // optional array - // { name: "Dashboard Redesign", link: "..." } - // ], - // impact: "40% increase in engagement", // optional + period: { + en: "Jan - Aug 2017", + fr: "Jan - Août 2017" + }, + achievements: { + en: [ + "Created a new automated testing workflow to improve reduce weekly manual testing by 10 hours", + "Integrated Silktest, Jenkins and Testrail API using Python scripts to 100% automate log parsing, reporting and archiving", + "Set up, configured and managed Jenkins servers and slaves", + "Developed Python and 4Test test suites to perform functional and performance tests" + ], + fr: [ + "Créé un nouveau workflow de tests automatisés pour réduire les tests manuels hebdomadaires de 10 heures", + "Intégré Silktest, Jenkins et l'API Testrail avec des scripts Python pour automatiser à 100% l'analyse de logs, les rapports et l'archivage", + "Configuré et géré des serveurs Jenkins et leurs agents", + "Développé des suites de tests Python et 4Test pour effectuer des tests fonctionnels et de performance" + ] + }, tech: ["Python", "Bash", "Silktest", "Jenkins", "Testrail"], } ] \ No newline at end of file diff --git a/src/components/data/skills.js b/src/components/data/skills.js index c219102..08fcb80 100644 --- a/src/components/data/skills.js +++ b/src/components/data/skills.js @@ -1,204 +1,98 @@ export const skillsData = { + // These don't need translation - they're universal languages: [ - { - name: "Python", - icon: "FaPython" - }, - { - name: "Java", - icon: "FaJava" - }, - { - name: "Bash", - icon: "FaTerminal" - }, - { - name: "JavaScript", - icon: "FaJs" - } + { name: "Python", icon: "FaPython" }, + { name: "Java", icon: "FaJava" }, + { name: "Bash", icon: "FaTerminal" }, + { name: "JavaScript", icon: "FaJs" } ], environments: [ - { - name: "Windows", - icon: "FaWindows" - }, - { - name: "Linux", - icon: "FaLinux" - }, - { - name: "Ubuntu", - icon: "FaUbuntu" - } + { name: "Windows", icon: "FaWindows" }, + { name: "Linux", icon: "FaLinux" }, + { name: "Ubuntu", icon: "FaUbuntu" } ], virtualization: [ - { - name: "VirtualBox", - icon: "SiVirtualbox" - }, - { - name: "Docker", - icon: "FaDocker" - }, - { - name: "Hyper-V", - icon: "FaTerminal" - } + { name: "VirtualBox", icon: "SiVirtualbox" }, + { name: "Docker", icon: "FaDocker" }, + { name: "Hyper-V", icon: "FaTerminal" } ], frameworks: [ - { - name: "Flask", - icon: "SiFlask" - }, - { - name: "Node.js", - icon: "FaNode" - }, - { - name: "WordPress", - icon: "FaWordpress" - }, - { - name: "React", - icon: "FaReact" - }, - { - name: "Google Cloud", - icon: "FaGoogle" - }, - { - name: "Play", - icon: "FaPlay" - } + { name: "Flask", icon: "SiFlask" }, + { name: "Node.js", icon: "FaNode" }, + { name: "WordPress", icon: "FaWordpress" }, + { name: "React", icon: "FaReact" }, + { name: "Google Cloud", icon: "FaGoogle" }, + { name: "Play", icon: "FaPlay" } ], "CI/CD": [ - { - name: "Git", - icon: "FaGitAlt" - }, - { - name: "Github", - icon: "FaGithub" - }, - { - name: "Jenkins", - icon: "FaJenkins" - }, - { - name: "Bitbucket", - icon: "FaBitbucket" - }, - { - name: "JIRA", - icon: "FaJira" - }, - { - name: "Concourse", - icon: "SiConcourse" - } + { name: "Git", icon: "FaGitAlt" }, + { name: "Github", icon: "FaGithub" }, + { name: "Jenkins", icon: "FaJenkins" }, + { name: "Bitbucket", icon: "FaBitbucket" }, + { name: "JIRA", icon: "FaJira" }, + { name: "Concourse", icon: "SiConcourse" } ], "Machine Learning": [ - { - name: "PyTorch", - icon: "SiPytorch" - }, - { - name: "Scikit-learn", - icon: "SiScikitlearn" - }, - { - name: "Pandas", - icon: "SiPandas" - }, - { - name: "NumPy", - icon: "SiNumpy" - }, - { - name: "Jupyter", - icon: "SiJupyter" - } + { name: "PyTorch", icon: "SiPytorch" }, + { name: "Scikit-learn", icon: "SiScikitlearn" }, + { name: "Pandas", icon: "SiPandas" }, + { name: "NumPy", icon: "SiNumpy" }, + { name: "Jupyter", icon: "SiJupyter" } ], + // Domain sections need translation domains: [ { - name: "Web Development", - description: "Full-stack applications with modern frameworks and RESTful APIs", + name: { + en: "Web Development", + fr: "Développement Web" + }, + description: { + en: "Full-stack applications with modern frameworks and RESTful APIs", + fr: "Applications full-stack avec des frameworks modernes et des API RESTful" + }, icon: "FaCode", relatedSkills: [ - { - icon: "FaReact", - displayName: "React" - }, - { - icon: "FaNode", - displayName: "Node.js" - }, - { - icon: "SiFlask", - displayName: "Flask" - }, - { - icon: "FaJs", - displayName: "JavaScript" - }, - { - icon: "FaPython", - displayName: "Python" - } + { icon: "FaReact", displayName: "React" }, + { icon: "FaNode", displayName: "Node.js" }, + { icon: "SiFlask", displayName: "Flask" }, + { icon: "FaJs", displayName: "JavaScript" }, + { icon: "FaPython", displayName: "Python" } ] }, { - name: "Machine Learning", - description: "ML models, data analysis, and NLP solutions", + name: { + en: "Machine Learning", + fr: "Apprentissage Automatique" + }, + description: { + en: "ML models, data analysis, and NLP solutions", + fr: "Modèles ML, analyse de données et solutions NLP" + }, icon: "FaBrain", relatedSkills: [ - { - icon: "SiPytorch", - displayName: "PyTorch" - }, - { - icon: "SiScikitlearn", - displayName: "Scikit-learn" - }, - { - icon: "SiPandas", - displayName: "Pandas" - }, - { - icon: "SiNumpy", - displayName: "NumPy" - }, - { - icon: "SiJupyter", - displayName: "Jupyter" - } + { icon: "SiPytorch", displayName: "PyTorch" }, + { icon: "SiScikitlearn", displayName: "Scikit-learn" }, + { icon: "SiPandas", displayName: "Pandas" }, + { icon: "SiNumpy", displayName: "NumPy" }, + { icon: "SiJupyter", displayName: "Jupyter" } ] }, { - name: "DevOps & Cloud", - description: "CI/CD pipelines, containerization, and cloud infrastructure", + name: { + en: "DevOps & Cloud", + fr: "DevOps et Cloud" + }, + description: { + en: "CI/CD pipelines, containerization, and cloud infrastructure", + fr: "Pipelines CI/CD, conteneurisation et infrastructure cloud" + }, icon: "FaCloud", relatedSkills: [ - { - icon: "FaDocker", - displayName: "Docker" - }, - { - icon: "FaGitAlt", - displayName: "Git" - }, - { - icon: "FaJenkins", - displayName: "Jenkins" - }, - { - icon: "FaGoogle", - displayName: "Google Cloud" - }, - { - icon: "SiConcourse", - displayName: "Concourse" - } + { icon: "FaDocker", displayName: "Docker" }, + { icon: "FaGitAlt", displayName: "Git" }, + { icon: "FaJenkins", displayName: "Jenkins" }, + { icon: "FaGoogle", displayName: "Google Cloud" }, + { icon: "SiConcourse", displayName: "Concourse" } ] } ] diff --git a/src/contexts/LanguageContext.js b/src/contexts/LanguageContext.js new file mode 100644 index 0000000..6fd746b --- /dev/null +++ b/src/contexts/LanguageContext.js @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const LanguageContext = createContext(null); diff --git a/src/contexts/LanguageProvider.jsx b/src/contexts/LanguageProvider.jsx new file mode 100644 index 0000000..85745cf --- /dev/null +++ b/src/contexts/LanguageProvider.jsx @@ -0,0 +1,31 @@ +import React, { useState, useEffect } from 'react'; +import { LanguageContext } from './LanguageContext'; + +export const LanguageProvider = ({ children }) => { + const [language, setLanguage] = useState(() => { + const savedLang = localStorage.getItem('preferred-language'); + return savedLang || 'en'; + }); + + useEffect(() => { + localStorage.setItem('preferred-language', language); + }, [language]); + + const toggleLanguage = () => { + setLanguage(prev => (prev === 'en' ? 'fr' : 'en')); + }; + + const value = { + language, + setLanguage, + toggleLanguage, + isEnglish: language === 'en', + isFrench: language === 'fr', + }; + + return ( + + {children} + + ); +}; diff --git a/src/contexts/useLanguage.js b/src/contexts/useLanguage.js new file mode 100644 index 0000000..2bb3aa3 --- /dev/null +++ b/src/contexts/useLanguage.js @@ -0,0 +1,10 @@ +import {useContext} from "react"; +import {LanguageContext} from "./LanguageContext.js"; + +export const useLanguage = () => { + const context = useContext(LanguageContext); + if (!context) { + throw new Error('useLanguage must be used within a LanguageProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/utils/translationHelpers.js b/src/utils/translationHelpers.js new file mode 100644 index 0000000..a5fd8b5 --- /dev/null +++ b/src/utils/translationHelpers.js @@ -0,0 +1,39 @@ +/** + * Helper function to get translated text from bilingual data + * @param {string|object} value - Either a string or an object with 'en' and 'fr' keys + * @param {string} language - Current language ('en' or 'fr') + * @returns {string} - The translated text + */ +export const getText = (value, language = 'en') => { + // If it's already a string (not translated), return as is + if (typeof value === 'string') { + return value; + } + + // If it's an object with language keys, return the appropriate one + if (value && typeof value === 'object') { + return value[language] || value.en || ''; + } + + return ''; +}; + +/** + * Helper function to get translated array from bilingual data + * @param {array|object} value - Either an array or an object with 'en' and 'fr' keys containing arrays + * @param {string} language - Current language ('en' or 'fr') + * @returns {array} - The translated array + */ +export const getArray = (value, language = 'en') => { + // If it's already an array (not translated), return as is + if (Array.isArray(value)) { + return value; + } + + // If it's an object with language keys, return the appropriate array + if (value && typeof value === 'object') { + return value[language] || value.en || []; + } + + return []; +}; \ No newline at end of file