diff --git a/app/i18n.ts b/app/i18n.ts new file mode 100644 index 0000000..854e9d8 --- /dev/null +++ b/app/i18n.ts @@ -0,0 +1,20 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import LanguageDetector from 'i18next-browser-languagedetector' + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + 'en-US': { + translation: require('../public/locales/en-US.json') + } + }, + fallbackLng: 'en-US', + interpolation: { + escapeValue: false + } + }); + +export default i18n \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 6245241..a682861 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,10 +1,11 @@ "use client" -import React, { useEffect } from 'react'; +import React, { useEffect } from 'react' import './globals.css' import '@fortawesome/fontawesome-svg-core/styles.css' import { config } from '@fortawesome/fontawesome-svg-core' -import { GeistSans } from 'geist/font/sans'; +import { GeistSans } from 'geist/font/sans' +import '../i18n' config.autoAddCss = false diff --git a/components/Header.tsx b/components/Header.tsx index 53d3038..4c8efbd 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,8 +1,9 @@ -"use client"; +"use client" -import React, { useState } from 'react'; -import Link from 'next/link'; -import { House, Link as LinkIcon, User, Phone, BookOpen, Music, Rss, X, Menu } from 'lucide-react'; +import React, { useState, useRef, useEffect } from 'react' +import Link from 'next/link' +import { House, Link as LinkIcon, User, Phone, BookOpen, Music, Rss, X, Menu, Globe, ChevronDown } from 'lucide-react' +import { useTranslation } from 'react-i18next' interface NavItemProps { href: string; @@ -19,6 +20,103 @@ const NavItem = ({ href, icon, children }: NavItemProps) => ( ); +const LanguageSelector = () => { + const { i18n } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const dropdownRef = useRef(null); + + const languages = [ + { code: 'en-US', name: 'English' }, + ]; + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < 1024); + }; + + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + const changeLanguage = (lng: string) => { + i18n.changeLanguage(lng); + setIsOpen(false); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setIsOpen(false); + } + if (e.key === 'Enter' || e.key === ' ') { + setIsOpen(!isOpen); + } + }; + + const buttonContent = ( + <> + + {languages.find(lang => lang.code === i18n.language)?.name || 'English'} + {!isMobile && ( + + )} + + ); + + return ( +
+ + {isOpen && ( +
+ {languages.map((lang) => ( + + ))} +
+ )} +
+ ); +}; + export default function Header() { const [isOpen, setIsOpen] = useState(false); @@ -41,7 +139,13 @@ export default function Header() { Manifesto Music Blog +
+ +
+
+ +
); diff --git a/components/pages/Contact.tsx b/components/pages/Contact.tsx index 42bc0fc..dc90868 100644 --- a/components/pages/Contact.tsx +++ b/components/pages/Contact.tsx @@ -1,31 +1,61 @@ +"use client" + import { faPhone, faEnvelope } from '@fortawesome/free-solid-svg-icons' import { faGithub, faTelegram, faBluesky } from '@fortawesome/free-brands-svg-icons' import { Phone } from 'lucide-react' import ContactButton from '../objects/ContactButton' +import { useTranslation } from 'react-i18next' export default function Contact() { - const firstSectionStrings = ["I do a lot of things during the day and I'm not always able to respond to messages right away. Please be patient and remember not to demand things from me... Somehow this is an issue for people :(", "For the best chance of a response, please send me a message on Telegram. If you've made a pull request on one of my repos, I will most likely respond by the next day. If you've sent me an email, I will most likely respond within three days or less."] - const secondSectionStrings = ["I have a phone number listed above. Please do not call or text me unless you absolutely need to. I will likely not respond, or use an automated recording system to handle your call. No, I haven't provided you my real phone number. I may be able to respond to your call/text, just know this is not checked/used often.", "If you need to get in touch with me, please send me a message on Telegram or an email."] + const { t } = useTranslation(); + const sections = [ - { title: "I'm a busy person", texts: firstSectionStrings }, - { title: "A note about calling and texting", texts: secondSectionStrings }, - ] - const contactButtonLabels = ["ihatenodejs", "@p0ntu5", "@aidxn.cc", "(802) 416-9516", "aidan@p0ntus.com"] - const contactButtonHrefs = ["https://github.com/ihatenodejs", "https://t.me/p0ntu5", "https://bsky.app/profile/aidxn.cc", "tel:+18024169516", "mailto:aidan@p0ntus.com"] - const contactButtonIcons = [faGithub, faTelegram, faBluesky, faPhone, faEnvelope] + { + title: t('contact.sections.busyPerson.title'), + texts: t('contact.sections.busyPerson.texts', { returnObjects: true }) as string[] + }, + { + title: t('contact.sections.callingNote.title'), + texts: t('contact.sections.callingNote.texts', { returnObjects: true }) as string[] + } + ]; + + const contactButtonLabels = [ + t('contact.buttons.github'), + t('contact.buttons.telegram'), + t('contact.buttons.bluesky'), + t('contact.buttons.phone'), + t('contact.buttons.email') + ]; + + const contactButtonHrefs = [ + "https://github.com/ihatenodejs", + "https://t.me/p0ntu5", + "https://bsky.app/profile/aidxn.cc", + "tel:+18024169516", + "mailto:aidan@p0ntus.com" + ]; + + const contactButtonIcons = [faGithub, faTelegram, faBluesky, faPhone, faEnvelope]; + return (

- Contact + {t('contact.title')}

{contactButtonLabels.map((label, index) => ( - - )) - } + + ))}
{sections.map((section, sectionIndex) => ( @@ -35,8 +65,7 @@ export default function Contact() {

{text}

))}
- )) - } + ))} ) } diff --git a/components/pages/Home.tsx b/components/pages/Home.tsx index 57ec50f..003247d 100644 --- a/components/pages/Home.tsx +++ b/components/pages/Home.tsx @@ -1,17 +1,25 @@ +"use client" + import Image from 'next/image' import Button from '../objects/Button' -import LastPlayed from '@/components/widgets/LastPlayed'; +import LastPlayed from '@/components/widgets/LastPlayed' +import { useTranslation } from 'react-i18next' +import Link from 'next/link' export default function Home() { - const whoAmIStrings = ["Hey there! I'm Aidan, a systems administrator, web developer, and student from the United States. I primarily work with Node.js and Linux.", "I am most interested in backend development and have experience with Node.js, Express, and Tailwind CSS. Despite my best efforts, I am no designer", "When I'm not programming, I can be found re-flashing my phone with a new custom ROM and telling everyone I use Arch."] - const whatIDoStrings = ["I am at my best when I am doing system administration, but I also enjoy working on web development projects. I enjoy contributing under open licenses more than anything. I have never felt much of a draw to profiting off my work.", "I host a few public services and websites on my VPS, most of which can be found on the \"Domains\" page with a short description.", "I'm most proud of LibreCloud/p0ntus mail, which is a cloud services provider that I self-host and maintain, free of charge.", "I frequently write and work on a website hosted on a public Linux server, known as a \"tilde\" You can check it out by clicking the link \"Tilde\" in the header, or \"what\" if you are still confused!"] - const whereYouAreStrings = ["My website is my home, not my business. I am not here to brag about my accomplishments or plug my cool SaaS product. That's why I've made every effort to make this website as personal and fun as possible.", "From a technical perspective, you're being served this website by Vercel."] - const mainStrings = [whoAmIStrings, whatIDoStrings, whereYouAreStrings] - const contactString = "Feel free to reach out for collaborations or just a hello :)" - const mainSections = ["Who I am", "What I do", "Where you are"] - const sendMessage = "Send me a message" - const myName = "Aidan" - const myDescription = "SysAdmin, Developer, and Student" + const { t } = useTranslation(); + + const mainStrings: string[][] = [ + t('home.whoAmI', { returnObjects: true }) as string[], + t('home.whatIDo', { returnObjects: true }) as string[], + t('home.whereYouAre', { returnObjects: true }) as string[] + ]; + + const mainSections = [ + t('home.sections.whoIAm'), + t('home.sections.whatIDo'), + t('home.sections.whereYouAre') + ]; return (
@@ -23,8 +31,8 @@ export default function Home() { height={150} className="rounded-full mx-auto mb-6 border-4 border-gray-700" /> -

{myName}

-

{myDescription}

+

{t('home.profile.name')}

+

{t('home.profile.description')}

@@ -32,20 +40,28 @@ export default function Home() { {mainSections.map((section, secIndex) => (

{section}

- {mainStrings[secIndex].map((text, index) => ( + {mainStrings[secIndex].map((text: string, index: number) => (

{text} + {secIndex === 2 && index === 1 && ( + <> + + CVE-2025-29927 + + . + + )}

))}
))}
-

{sendMessage}

-

{contactString}

+

{t('home.contact.title')}

+

{t('home.contact.description')}

diff --git a/i18n.ts b/i18n.ts new file mode 100644 index 0000000..a13d7bf --- /dev/null +++ b/i18n.ts @@ -0,0 +1,20 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + 'en-US': { + translation: require('./public/locales/en-US.json') + } + }, + fallbackLng: 'en-US', + interpolation: { + escapeValue: false + } + }); + +export default i18n; \ No newline at end of file diff --git a/locale/en-us.json b/locale/en-us.json deleted file mode 100644 index 16fae72..0000000 --- a/locale/en-us.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "home": { - "intro": "SysAdmin, Developer, and Student", - "whoami": "Hey there! I'm Aidan, a systems administrator, web developer, and student from the United States. I primarily work with Node.js and Linux.", - "interests": "I am most interested in backend development and have experience with Node.js, Express, and Tailwind CSS. Despite my best efforts, I am no designer", - "arch":"When I'm not programming, I can be found re-flashing my phone with a new custom ROM and telling everyone I use Arch." - } -} \ No newline at end of file diff --git a/package.json b/package.json index 6c02b77..331d120 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,14 @@ "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "geist": "^1.3.1", + "i18next": "^24.2.3", + "i18next-browser-languagedetector": "^8.0.4", "lucide-react": "^0.469.0", "next": "^15.2.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-fast-marquee": "^1.6.5", + "react-i18next": "^15.4.1", "react-icons": "^5.5.0", "tailwindcss-animate": "^1.0.7" }, diff --git a/public/locales/en-US.json b/public/locales/en-US.json new file mode 100644 index 0000000..35a596e --- /dev/null +++ b/public/locales/en-US.json @@ -0,0 +1,62 @@ +{ + "home": { + "whoAmI": [ + "Hey there! I'm Aidan, a systems administrator, web developer, and student from the United States. I primarily work with Node.js and Linux.", + "I am most interested in backend development and have experience with Node.js, Express, and Tailwind CSS. Despite my best efforts, I am no designer", + "When I'm not programming, I can be found re-flashing my phone with a new custom ROM and telling everyone I use Arch." + ], + "whatIDo": [ + "I am at my best when I am doing system administration, but I also enjoy working on web development projects. I enjoy contributing under open licenses more than anything. I have never felt much of a draw to profiting off my work.", + "I host a few public services and websites on my VPS, most of which can be found on the \"Domains\" page with a short description.", + "I'm most proud of LibreCloud/p0ntus mail, which is a cloud services provider that I self-host and maintain, free of charge.", + "I frequently write and work on a website hosted on a public Linux server, known as a \"tilde\" You can check it out" + ], + "whereYouAre": [ + "My website is my home, not my business. I am not here to brag about my accomplishments or plug my cool SaaS product. That's why I've made every effort to make this website as personal and fun as possible.", + "This page is currently hosted on Cloudflare Workers, after what happened with " + ], + "sections": { + "whoIAm": "Who I am", + "whatIDo": "What I do", + "whereYouAre": "Where you are" + }, + "contact": { + "title": "Send me a message", + "description": "Feel free to reach out for collaborations or just a hello :)", + "button": "Contact Me" + }, + "profile": { + "name": "Aidan", + "description": "SysAdmin, Developer, and Student" + }, + "miscWords": { + "here": "here" + } + }, + "contact": { + "title": "Contact", + "sections": { + "busyPerson": { + "title": "I'm a busy person", + "texts": [ + "I do a lot of things during the day and I'm not always able to respond to messages right away. Please be patient and remember not to demand things from me... Somehow this is an issue for people :(", + "For the best chance of a response, please send me a message on Telegram. If you've made a pull request on one of my repos, I will most likely respond by the next day. If you've sent me an email, I will most likely respond within three days or less." + ] + }, + "callingNote": { + "title": "A note about calling and texting", + "texts": [ + "I have a phone number listed above. Please do not call or text me unless you absolutely need to. I will likely not respond, or use an automated recording system to handle your call. No, I haven't provided you my real phone number. I may be able to respond to your call/text, just know this is not checked/used often.", + "If you need to get in touch with me, please send me a message on Telegram or an email." + ] + } + }, + "buttons": { + "github": "ihatenodejs", + "telegram": "@p0ntu5", + "bluesky": "@aidxn.cc", + "phone": "(802) 416-9516", + "email": "aidan@p0ntus.com" + } + } +} \ No newline at end of file