feat: improved locale selection (made button, use i18n)

This commit is contained in:
Aidan 2025-03-26 23:13:38 -04:00
parent bf4ed43f50
commit e98a80666f
9 changed files with 291 additions and 44 deletions

20
app/i18n.ts Normal file
View File

@ -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

View File

@ -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

View File

@ -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) => (
</div>
);
const LanguageSelector = () => {
const { i18n } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(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 = (
<>
<Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
{languages.find(lang => lang.code === i18n.language)?.name || 'English'}
{!isMobile && (
<ChevronDown className={`w-4 h-4 ml-1 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={20} />
)}
</>
);
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
className={`flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 ${isMobile ? 'w-full' : ''}`}
aria-expanded={isOpen}
aria-haspopup="true"
>
{buttonContent}
</button>
{isOpen && (
<div
className={`${
isMobile
? 'relative mt-1 w-full bg-gray-800 rounded-md shadow-lg'
: 'absolute right-0 mt-2 w-48 bg-gray-800 rounded-md shadow-lg z-50'
}`}
role="menu"
aria-orientation="vertical"
aria-labelledby="language-menu"
>
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => changeLanguage(lang.code)}
className={`block w-full text-left px-4 py-2 text-sm ${
i18n.language === lang.code
? 'text-white bg-gray-700'
: 'text-gray-300 hover:text-white hover:bg-gray-700'
}`}
role="menuitem"
>
{lang.name}
</button>
))}
</div>
)}
</div>
);
};
export default function Header() {
const [isOpen, setIsOpen] = useState(false);
@ -41,7 +139,13 @@ export default function Header() {
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
<NavItem href="/music" icon={Music}>Music</NavItem>
<NavItem href="https://disfunction.blog" icon={Rss}>Blog</NavItem>
<div className="lg:hidden">
<LanguageSelector />
</div>
</ul>
<div className="hidden lg:block">
<LanguageSelector />
</div>
</nav>
</header>
);

View File

@ -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 (
<div className="max-w-2xl mx-auto text-center">
<div className='mb-6 flex justify-center'>
<Phone size={60} />
</div>
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
Contact
{t('contact.title')}
</h1>
<div className="p-6 space-y-4">
{contactButtonLabels.map((label, index) => (
<ContactButton key={index} label={label} href={contactButtonHrefs[index]} icon={contactButtonIcons[index]} className='mr-3'></ContactButton>
))
}
<ContactButton
key={index}
label={label}
href={contactButtonHrefs[index]}
icon={contactButtonIcons[index]}
className='mr-3'
/>
))}
</div>
{sections.map((section, sectionIndex) => (
@ -35,8 +65,7 @@ export default function Contact() {
<p key={index} className="text-gray-300 mb-4">{text}</p>
))}
</div>
))
}
))}
</div>
)
}

View File

@ -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 (
<div className="max-w-2xl mx-auto">
@ -23,8 +31,8 @@ export default function Home() {
height={150}
className="rounded-full mx-auto mb-6 border-4 border-gray-700"
/>
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">{myName}</h1>
<p className="text-gray-400 text-xl">{myDescription}</p>
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">{t('home.profile.name')}</h1>
<p className="text-gray-400 text-xl">{t('home.profile.description')}</p>
</div>
<LastPlayed />
@ -32,20 +40,28 @@ export default function Home() {
{mainSections.map((section, secIndex) => (
<section key={secIndex} id="about" className="mb-12">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{section}</h2>
{mainStrings[secIndex].map((text, index) => (
{mainStrings[secIndex].map((text: string, index: number) => (
<p key={index} className="text-gray-300 leading-relaxed mt-2">
{text}
{secIndex === 2 && index === 1 && (
<>
<Link href="https://nvd.nist.gov/vuln/detail/CVE-2025-29927" className="text-blue-400 hover:underline">
CVE-2025-29927
</Link>
.
</>
)}
</p>
))}
</section>
))}
<section id="contact">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{sendMessage}</h2>
<p className="text-gray-300 mb-6">{contactString}</p>
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{t('home.contact.title')}</h2>
<p className="text-gray-300 mb-6">{t('home.contact.description')}</p>
<Button
href={'/contact'}
label="Contact Me"
label={t('home.contact.button')}
/>
</section>
</div>

20
i18n.ts Normal file
View File

@ -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;

View File

@ -1,8 +0,0 @@
{
"home": {
"intro": "SysAdmin, Developer, and Student",
"whoami": "Hey there! I&apos;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&apos;m not programming, I can be found re-flashing my phone with a new custom ROM and telling everyone I use Arch."
}
}

View File

@ -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"
},

62
public/locales/en-US.json Normal file
View File

@ -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"
}
}
}