Finnish transaltions

This commit is contained in:
2026-08-16 21:07:36 +03:00
parent d2cd0e37d4
commit 00c08f6544
15 changed files with 377 additions and 149 deletions

View File

@@ -1,35 +1,35 @@
import { useAtomValue } from 'jotai';
import { languageState } from '../state/atoms';
import { getTranslations } from '../translations';
import styles from '../styles/components/About.module.scss'; import styles from '../styles/components/About.module.scss';
const STACK = ['Azure', '.NET', 'C#', 'JavaScript', 'React', 'Node.js', 'SQL'];
export default function About() { export default function About() {
const language = useAtomValue(languageState);
const t = getTranslations(language);
return ( return (
<section id="about" className={styles.about}> <section id="about" className={styles.about}>
<div className={styles.inner}> <div className={styles.inner}>
<div className={styles.header}> <div className={styles.header}>
<span className={styles.sectionNum}>01.</span> <span className={styles.sectionNum}>{t.about.sectionNum}</span>
<h2 className={styles.sectionTitle}>About me</h2> <h2 className={styles.sectionTitle}>{t.about.sectionTitle}</h2>
<div className={styles.line} /> <div className={styles.line} />
</div> </div>
<div className={styles.body}> <div className={styles.body}>
<div className={styles.text}> <div className={styles.text}>
<p> <p>
I'm a curious software engineer with a passion for building scalable and efficient {t.about.paragraph1}
applications. With deep knowledge in cloud, backend, and frontend development, I enjoy
working on projects that challenge me to learn new technologies and improve my skills.
</p> </p>
<p> <p>
I tend to build always something that is useful to others, and I find joy in creating tools {t.about.paragraph2}
and applications that solve real-world problems. I believe that technology has the power to
make a positive impact on people's lives, and I strive to contribute to that impact through my work.
</p> </p>
<p> <p>
Here are some technologies I've been working with recently: {t.about.paragraph3}
</p> </p>
<ul className={styles.stack}> <ul className={styles.stack}>
{STACK.map((item) => ( {t.about.stack.map((item) => (
<li key={item}> <li key={item}>
<span className={styles.arrow}>▸</span> {item} <span className={styles.arrow}>▸</span> {item}
</li> </li>
@@ -43,15 +43,15 @@ export default function About() {
</div> </div>
<div className={styles.meta}> <div className={styles.meta}>
<p className={styles.metaLine}> <p className={styles.metaLine}>
<span className={styles.metaKey}>location</span> <span className={styles.metaKey}>{t.about.meta.location}</span>
<span className={styles.metaValue}>Turku Archipelago, Finland</span> <span className={styles.metaValue}>{t.about.meta.locationValue}</span>
</p> </p>
<p className={styles.metaLine}> <p className={styles.metaLine}>
<span className={styles.metaKey}>favorite book</span> <span className={styles.metaKey}>{t.about.meta.favoriteBook}</span>
<span className={styles.metaValue}>The Lord of the Rings</span> <span className={styles.metaValue}>{t.about.meta.favoriteBookValue}</span>
</p> </p>
<div className={styles.metaLine}> <div className={styles.metaLine}>
<span className={styles.metaKey}>favorite songs</span> <span className={styles.metaKey}>{t.about.meta.favoriteSongs}</span>
<div className={styles.metaLinks}> <div className={styles.metaLinks}>
<a href="https://www.youtube.com/watch?v=EqDcigWxZuU" target="_blank" rel="noreferrer">Rücksturz – Agitation Free</a> <a href="https://www.youtube.com/watch?v=EqDcigWxZuU" target="_blank" rel="noreferrer">Rücksturz – Agitation Free</a>
<a href="https://www.youtube.com/watch?v=2i2oJJwgLTk" target="_blank" rel="noreferrer">Year of Love – Jenny Hval</a> <a href="https://www.youtube.com/watch?v=2i2oJJwgLTk" target="_blank" rel="noreferrer">Year of Love – Jenny Hval</a>

View File

@@ -1,35 +1,42 @@
import { useAtomValue } from 'jotai';
import { languageState } from '../state/atoms';
import { getTranslations } from '../translations';
import styles from '../styles/components/Contact.module.scss'; import styles from '../styles/components/Contact.module.scss';
export default function Contact() { export default function Contact() {
const language = useAtomValue(languageState);
const t = getTranslations(language);
const SOCIALS = [
{ label: t.contact.socials.gitea, href: 'https://gitea.tietokonepaja.fi/' },
{ label: t.contact.socials.linkedin, href: 'https://www.linkedin.com/in/veikko-lintuj%C3%A4rvi-5888a6175/' },
{ label: t.contact.socials.company, href: 'https://tietokonepaja.fi/' },
];
return ( return (
<section id="contact" className={styles.contact}> <section id="contact" className={styles.contact}>
<div className={styles.inner}> <div className={styles.inner}>
<div className={styles.header}> <div className={styles.header}>
<span className={styles.sectionNum}>03.</span> <span className={styles.sectionNum}>{t.contact.sectionNum}</span>
<h2 className={styles.sectionTitle}>Get in touch</h2> <h2 className={styles.sectionTitle}>{t.contact.sectionTitle}</h2>
<div className={styles.line} /> <div className={styles.line} />
</div> </div>
<div className={styles.body}> <div className={styles.body}>
<p className={styles.lede}> <p className={styles.lede}>
I'm currently employed, but my inbox is always open. Whether you have a question or {t.contact.lede}
just want to say hi, I'll try my best to get back to you!
</p> </p>
<p>I also have a small company if you are interested in collaboration or services.</p> <p dangerouslySetInnerHTML={{ __html: t.contact.paragraph }} />
<a <a
href="mailto:veikko@lintujarvi.fi" href="mailto:veikko@lintujarvi.fi"
className={styles.emailBtn} className={styles.emailBtn}
> >
Say hello ↗ {t.contact.emailBtn} ↗
</a> </a>
<ul className={styles.socials}> <ul className={styles.socials}>
{[ {SOCIALS.map(({ label, href }) => (
{ label: 'Gitea', href: 'https://gitea.tietokonepaja.fi/' },
{ label: 'LinkedIn', href: 'https://www.linkedin.com/in/veikko-lintuj%C3%A4rvi-5888a6175/' },
{ label: 'Company', href: 'https://www.tietokonepaja.fi/' },
].map(({ label, href }) => (
<li key={label}> <li key={label}>
<a href={href} target="_blank" rel="noreferrer"> <a href={href} target="_blank" rel="noreferrer">
{label} {label}

View File

@@ -1,13 +1,19 @@
import { useAtomValue } from 'jotai';
import { languageState } from '../state/atoms';
import { getTranslations } from '../translations';
import styles from '../styles/components/Footer.module.scss'; import styles from '../styles/components/Footer.module.scss';
export default function Footer() { export default function Footer() {
const language = useAtomValue(languageState);
const t = getTranslations(language);
return ( return (
<footer className={styles.footer}> <footer className={styles.footer}>
<p> <p>
Designed &amp; built by{' '} {t.footer.designedBy}{' '}
<a href="#home">Veikko Lintujärvi</a>{' '} <a href="#home">Veikko Lintujärvi</a>{' '}
<span className={styles.separator}>·</span>{' '} <span className={styles.separator}>{t.footer.separator}</span>{' '}
<span className={styles.stack}>React + Vite</span> <span className={styles.stack}>{t.footer.stack}</span>
</p> </p>
</footer> </footer>
); );

View File

@@ -1,38 +1,38 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useAtomValue } from 'jotai';
import { languageState } from '../state/atoms';
import { getTranslations } from '../translations';
import styles from '../styles/components/Hero.module.scss'; import styles from '../styles/components/Hero.module.scss';
export default function Hero() { export default function Hero() {
const language = useAtomValue(languageState);
const t = getTranslations(language);
return ( return (
<section id="home" className={styles.hero}> <section id="home" className={styles.hero}>
<div className={styles.grid} aria-hidden="true" /> <div className={styles.grid} aria-hidden="true" />
<div className={styles.content}> <div className={styles.content}>
<p className={styles.greeting}>Hey, I'm</p> <p className={styles.greeting}>{t.hero.greeting}</p>
<h1 className={styles.name}> <h1 className={styles.name}>
Veikko Lintujärvi<span className={styles.dot}>.</span> {t.hero.name}<span className={styles.dot}>.</span>
</h1> </h1>
<p className={styles.tagline}> <p className={styles.tagline}>
Making world a better place —{' '} {t.hero.tagline}
<span className={styles.highlight}>one line at a time.</span>
</p> </p>
<p className={styles.sub}> <p className={styles.sub}>
I'm a curious software engineer from Turku Archipelago, Finland. Right now {t.hero.sub}
I'm excited about cloud, Linux and AI.
</p> </p>
<div className={styles.cta}> <div className={styles.cta}>
<Link to="/projects" className={styles.btnPrimary}> <Link to="/projects" className={styles.btnPrimary}>
See my work {t.hero.ctaPrimary}
</Link> </Link>
<Link to="/contact" className={styles.btnGhost}> <Link to="/contact" className={styles.btnGhost}>
Get in touch {t.hero.ctaSecondary}
</Link> </Link>
</div> </div>
</div> </div>
<div className={styles.badge} aria-hidden="true">
<span>open to work</span>
</div>
</section> </section>
); );
} }

View File

@@ -0,0 +1,23 @@
import { useAtom } from 'jotai';
import { languageState } from '../state/atoms';
import { getTranslations } from '../translations';
import styles from '../styles/components/LanguagePicker.module.scss';
export default function LanguagePicker() {
const [language, setLanguage] = useAtom(languageState);
const t = getTranslations(language);
const toggleLanguage = () => {
setLanguage(language === 'en' ? 'fi' : 'en');
};
return (
<button
className={styles.picker}
onClick={toggleLanguage}
aria-label={`Switch to ${language === 'en' ? t.languages.fi : t.languages.en}`}
>
<span className={styles.label}>{language === 'en' ? 'FI' : 'EN'}</span>
</button>
);
}

View File

@@ -1,19 +1,23 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { navOpenState, activeSectionState } from '../state/atoms'; import { navOpenState, activeSectionState, languageState } from '../state/atoms';
import { getTranslations } from '../translations';
import ThemeToggle from './ThemeToggle'; import ThemeToggle from './ThemeToggle';
import LanguagePicker from './LanguagePicker';
import styles from '../styles/components/Navbar.module.scss'; import styles from '../styles/components/Navbar.module.scss';
const NAV_LINKS = [
{ label: 'Home', section: 'home' },
{ label: 'About', section: 'about' },
{ label: 'Projects', section: 'projects' },
{ label: 'Contact', section: 'contact' },
];
export default function Navbar() { export default function Navbar() {
const [isOpen, setIsOpen] = useAtom(navOpenState); const [isOpen, setIsOpen] = useAtom(navOpenState);
const activeSection = useAtomValue(activeSectionState); const activeSection = useAtomValue(activeSectionState);
const language = useAtomValue(languageState);
const t = getTranslations(language);
const NAV_LINKS = [
{ label: t.nav.home, section: 'home' },
{ label: t.nav.about, section: 'about' },
{ label: t.nav.projects, section: 'projects' },
{ label: t.nav.contact, section: 'contact' },
];
return ( return (
<header className={styles.header}> <header className={styles.header}>
@@ -39,12 +43,13 @@ export default function Navbar() {
</ul> </ul>
<div className={styles.navActions}> <div className={styles.navActions}>
<LanguagePicker />
<ThemeToggle /> <ThemeToggle />
<button <button
className={`${styles.burger} ${isOpen ? styles.burgerOpen : ''}`} className={`${styles.burger} ${isOpen ? styles.burgerOpen : ''}`}
onClick={() => setIsOpen((v) => !v)} onClick={() => setIsOpen((v) => !v)}
aria-label="Toggle menu" aria-label={t.theme.switchToLight}
aria-expanded={isOpen} aria-expanded={isOpen}
> >
<span /> <span />

View File

@@ -1,14 +1,20 @@
import { useAtomValue } from 'jotai';
import { languageState } from '../state/atoms';
import { getTranslations } from '../translations';
import styles from '../styles/components/NotFound.module.scss'; import styles from '../styles/components/NotFound.module.scss';
export default function NotFound() { export default function NotFound() {
const language = useAtomValue(languageState);
const t = getTranslations(language);
return ( return (
<section id="not-found" className={styles.notFound}> <section id="not-found" className={styles.notFound}>
<div className={styles.inner}> <div className={styles.inner}>
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
<img src="/not-found.png" alt="Not found" className={styles.image} /> <img src="/not-found.png" alt="Not found" className={styles.image} />
</div> </div>
<h1 className={styles.title}>Page Not Found</h1> <h1 className={styles.title}>{t.notFound.title}</h1>
<p className={styles.message}>I didn't find what you were looking for :{'('}</p> <p className={styles.message}>{t.notFound.message}</p>
</div> </div>
</section> </section>
); );

View File

@@ -1,29 +1,8 @@
import { useAtomValue } from 'jotai';
import { languageState } from '../state/atoms';
import { getTranslations } from '../translations';
import styles from '../styles/components/Projects.module.scss'; import styles from '../styles/components/Projects.module.scss';
const PROJECTS = [
{
name: 'Klapi - Unified content management panel',
desc: 'Web interface to view and manage data held by Livonsaaren Tietokonepaja. Easy to customize serving various customer solutions.',
tags: ['.NET', 'React', 'SQLite'],
href: 'https://klapi.tietokonepaja.fi/',
sourceHref: 'https://github.com',
},
{
name: 'Hattara - Shared online office',
desc: 'Why to code your own when there is working open source solution to host on your own server? Hattara is a self-hosted cloud office solution for small teams and individuals.',
tags: ['Apache2', 'Ubuntu Server', 'NextCloud'],
href: 'https://hattara.tietokonepaja.fi/',
sourceHref: 'https://github.com',
},
{
name: 'Runosaari festival website - Pro bono project',
desc: "Supporting local culture initiatives have always been close to my heart. I'm proudly helping Runosaari festival with their website and online presence already for the sixth year!",
tags: ['React', 'Vite', 'Apache2', 'Ubuntu Server'],
href: 'https://www.runosaari.fi/',
sourceHref: 'https://github.com',
},
];
const getTagColor = (tag) => { const getTagColor = (tag) => {
const colorMap = { const colorMap = {
'.NET': 'tagDotnet', '.NET': 'tagDotnet',
@@ -38,25 +17,28 @@ const getTagColor = (tag) => {
}; };
export default function Projects() { export default function Projects() {
const language = useAtomValue(languageState);
const t = getTranslations(language);
return ( return (
<section id="projects" className={styles.projects}> <section id="projects" className={styles.projects}>
<div className={styles.inner}> <div className={styles.inner}>
<div className={styles.header}> <div className={styles.header}>
<span className={styles.sectionNum}>02.</span> <span className={styles.sectionNum}>{t.projects.sectionNum}</span>
<h2 className={styles.sectionTitle}>Projects</h2> <h2 className={styles.sectionTitle}>{t.projects.sectionTitle}</h2>
<div className={styles.line} /> <div className={styles.line} />
</div> </div>
<div className={styles.grid}> <div className={styles.grid}>
{PROJECTS.map((p) => ( {t.projects.projects.map((p) => (
<article key={p.name} className={styles.card}> <article key={p.name} className={styles.card}>
<div className={styles.cardTop}> <div className={styles.cardTop}>
<span className={styles.folderIcon}>⬡</span> <span className={styles.folderIcon}>{t.projects.folderIcon}</span>
<div className={styles.cardLinks}> <div className={styles.cardLinks}>
<a href={p.href} className={styles.externalLink} aria-label={`Open ${p.name}`}> <a href={p.href} className={styles.externalLink} aria-label={`${t.projects.externalLinkAria} ${p.name}`}>
↗ ↗
</a> </a>
<a href={p.sourceHref} className={styles.sourceLink} aria-label={`View source code for ${p.name}`}> <a href={p.sourceHref} className={styles.sourceLink} aria-label={`${t.projects.sourceLinkAria} ${p.name}`}>
📦 📦
</a> </a>
</div> </div>
@@ -64,9 +46,9 @@ export default function Projects() {
<h3 className={styles.cardName}>{p.name}</h3> <h3 className={styles.cardName}>{p.name}</h3>
<p className={styles.cardDesc}>{p.desc}</p> <p className={styles.cardDesc}>{p.desc}</p>
<ul className={styles.tags}> <ul className={styles.tags}>
{p.tags.map((t) => ( {p.tags.map((tag) => (
<li key={t} className={styles.tagItem}> <li key={tag} className={styles.tagItem}>
<span className={`${styles.tagPill} ${styles[getTagColor(t)]}`}>{t}</span> <span className={`${styles.tagPill} ${styles[getTagColor(tag)]}`}>{tag}</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -74,9 +56,9 @@ export default function Projects() {
))} ))}
</div> </div>
<div className={styles.giteaLink}> <div className={styles.giteaLink}>
<p className={styles.giteaText}>Check out here the rest of my code</p> <p className={styles.giteaText}>{t.projects.giteaText}</p>
<a href="https://gitea.tietokonepaja.fi/" className={styles.giteaBtn} target="_blank" rel="noreferrer"> <a href="https://gitea.tietokonepaja.fi/" className={styles.giteaBtn} target="_blank" rel="noreferrer">
View on Gitea {t.projects.giteaBtn}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,16 +1,19 @@
import { useAtom } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { themeState } from '../state/atoms'; import { themeState, languageState } from '../state/atoms';
import { getTranslations } from '../translations';
import styles from '../styles/components/ThemeToggle.module.scss'; import styles from '../styles/components/ThemeToggle.module.scss';
export default function ThemeToggle() { export default function ThemeToggle() {
const [theme, setTheme] = useAtom(themeState); const [theme, setTheme] = useAtom(themeState);
const language = useAtomValue(languageState);
const t = getTranslations(language);
const isDark = theme === 'dark'; const isDark = theme === 'dark';
return ( return (
<button <button
className={styles.toggle} className={styles.toggle}
onClick={() => setTheme(isDark ? 'light' : 'dark')} onClick={() => setTheme(isDark ? 'light' : 'dark')}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'} aria-label={isDark ? t.theme.switchToLight : t.theme.switchToDark}
> >
{isDark ? <SunIcon /> : <MoonIcon />} {isDark ? <SunIcon /> : <MoonIcon />}
</button> </button>

View File

@@ -1,13 +1,33 @@
import { StrictMode } from 'react'; import { StrictMode, useEffect } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { Provider } from 'jotai';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { useAtomValue } from 'jotai';
import { languageState } from './state/atoms';
import './styles/main.scss'; import './styles/main.scss';
import App from './App.jsx'; import App from './App.jsx';
createRoot(document.getElementById('root')).render( export function LanguageSync() {
const language = useAtomValue(languageState);
useEffect(() => {
document.documentElement.lang = language;
}, [language]);
return null;
}
export function Root() {
return (
<StrictMode> <StrictMode>
<Provider>
<LanguageSync />
<Router> <Router>
<App /> <App />
</Router> </Router>
</StrictMode>, </Provider>
); </StrictMode>
);
}
createRoot(document.getElementById('root')).render(<Root />);

View File

@@ -5,3 +5,5 @@ export const navOpenState = atom(false);
export const activeSectionState = atom('home'); export const activeSectionState = atom('home');
export const themeState = atom('dark'); export const themeState = atom('dark');
export const languageState = atom('en');

View File

@@ -127,39 +127,3 @@
transform: translateY(-2px); transform: translateY(-2px);
} }
} }
// ── "Open to work" badge ───────────────────────────────────────
.badge {
position: absolute;
bottom: $space-8;
right: $space-6;
background: rgb(var(--color-bg-card) / 0.9);
border: 1px solid rgba(34, 197, 94, 0.35);
border-radius: 100px;
padding: $space-2 $space-4;
font-size: $text-xs;
font-family: $font-mono;
letter-spacing: 0.06em;
color: #4ade80;
display: flex;
align-items: center;
gap: $space-2;
&::before {
content: '';
width: 7px;
height: 7px;
border-radius: 50%;
background: #4ade80;
animation: pulse 2s ease infinite;
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
@media (max-width: 640px) {
.badge { display: none; }
}

View File

@@ -0,0 +1,27 @@
@use '../variables' as *;
.picker {
background: transparent;
border: 1px solid var(--text-secondary);
border-radius: 4px;
padding: 0.4rem 0.8rem;
color: var(--text-primary);
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.2s ease;
&:hover {
border-color: var(--accent);
color: var(--accent);
}
@media (max-width: 768px) {
padding: 0.35rem 0.6rem;
font-size: 0.8rem;
}
}
.label {
display: inline-block;
}

View File

@@ -89,19 +89,6 @@
} }
} }
.featuredBadge {
position: absolute;
top: $space-4;
right: $space-4;
font-size: $text-xs;
font-family: $font-mono;
color: $color-accent-warm;
background: rgb(var(--color-accent-warm) / 0.1);
border: 1px solid rgb(var(--color-accent-warm) / 0.25);
border-radius: 100px;
padding: 2px $space-3;
}
.folderIcon { .folderIcon {
font-size: $text-2xl; font-size: $text-2xl;
color: $color-accent; color: $color-accent;

196
src/translations.js Normal file
View File

@@ -0,0 +1,196 @@
export const translations = {
en: {
// Navbar
nav: {
home: 'Home',
about: 'About',
projects: 'Projects',
contact: 'Contact',
},
// Hero
hero: {
greeting: 'Hey, I\'m',
name: 'Veikko Lintujärvi',
tagline: 'Making world a better place — one line at a time.',
sub: 'I\'m a curious software engineer from Turku Archipelago, Finland. Right now I\'m excited about cloud, Linux and AI.',
ctaPrimary: 'See my work',
ctaSecondary: 'Get in touch',
},
// About
about: {
sectionNum: '01.',
sectionTitle: 'About me',
paragraph1: 'I\'m a curious software engineer with a passion for building scalable and efficient applications. With deep knowledge in cloud, backend, and frontend development, I enjoy working on projects that challenge me to learn new technologies and improve my skills.',
paragraph2: 'I tend to build always something that is useful to others, and I find joy in creating tools and applications that solve real-world problems. I believe that technology has the power to make a positive impact on people\'s lives, and I strive to contribute to that impact through my work.',
paragraph3: 'Here are some technologies I\'ve been working with recently:',
stack: ['Azure', '.NET', 'C#', 'JavaScript', 'React', 'Node.js', 'SQL'],
meta: {
location: 'location',
locationValue: 'Turku Archipelago, Finland',
favoriteBook: 'favorite book',
favoriteBookValue: 'The Name of the Rose',
favoriteSongs: 'favorite songs',
},
},
// Projects
projects: {
sectionNum: '02.',
sectionTitle: 'Projects',
projects: [
{
name: 'Klapi - Unified content management panel',
desc: 'Web interface to view and manage data held by Livonsaaren Tietokonepaja. Easy to customize serving various customer solutions.',
tags: ['.NET', 'React', 'SQLite'],
},
{
name: 'Hattara - Shared online office',
desc: 'Why to code your own when there is working open source solution to host on your own server? Hattara is a self-hosted cloud office solution for small teams and individuals.',
tags: ['Apache2', 'Ubuntu Server', 'NextCloud'],
},
{
name: 'Runosaari festival website - Pro bono project',
desc: "Supporting local culture initiatives have always been close to my heart. I'm proudly helping Runosaari festival with their website and online presence already for the sixth year!",
tags: ['React', 'Vite', 'Apache2', 'Ubuntu Server'],
},
],
giteaText: 'Check out here the rest of my code',
giteaBtn: 'View on Gitea',
folderIcon: '⬡',
externalLinkAria: 'Open',
sourceLinkAria: 'View source code for',
},
// Contact
contact: {
sectionNum: '03.',
sectionTitle: 'Get in touch',
lede: "I'm currently employed, but my inbox is always open. Whether you have a question or just want to say hi, I'll try my best to get back to you!",
paragraph: 'I also have a small company if you are interested in collaboration or services.',
emailBtn: 'Say hello',
socials: {
gitea: 'Gitea',
linkedin: 'LinkedIn',
company: 'Company',
},
},
// Footer
footer: {
designedBy: 'Designed & built by',
separator: '·',
stack: 'React + Vite',
},
// NotFound
notFound: {
title: 'Page Not Found',
message: "I didn't find what you were looking for :(",
},
// ThemeToggle
theme: {
switchToLight: 'Switch to light mode',
switchToDark: 'Switch to dark mode',
},
// Language names
languages: {
en: 'English',
fi: 'Suomi',
},
},
fi: {
// Navbar
nav: {
home: 'Etusivu',
about: 'Minä',
projects: 'Projektit',
contact: 'Yhteystiedot',
},
// Hero
hero: {
greeting: 'Hei, olen',
name: 'Veikko Lintujärvi',
tagline: 'Tehdään maailmasta parempi paikka kaikille — koodirivi kerrallaan.',
sub: 'Olen utelias insinööri Turun saaristosta. Tällä hetkellä inspiroidun pilvipalveluista, Linuxista ja tekoälystä.',
ctaPrimary: 'Töitäni',
ctaSecondary: 'Ota yhteyttä',
},
// About
about: {
sectionNum: '01.',
sectionTitle: 'Minä',
paragraph1: 'Intohimonani on rakentaa tehokasta ja skaalautuvaa internettiä. Nautin haastavista projekteista, joissa pääsen syventämään osaamistani pilvi-, fullstack- ja tekoälykehityksessä. Haluan jatkuvasti oppia uusia teknologioita ja sovelluskehityksen toimintatapoja tekemiseni ohessa.',
paragraph2: 'Pyrin aina rakentamaan jotain, mistä on hyötyä muille. Innostun kun saan rakentaa työkaluja, jotka ratkovat aitoja ongelmia. Uskon, että tekniikalla on kyky vaikuttaa positiivisesti ihmisiin ja yhteiskuntaan.',
paragraph3: 'Tässä muutamia teknologioita, joiden parissa olen viime aikoina työskennellyt:',
stack: ['Azure', '.NET', 'C#', 'Mistral AI', 'JavaScript', 'React', 'Node.js', 'SQL'],
meta: {
location: 'sijainti',
locationValue: 'Turun saaristo, Suomi',
favoriteBook: 'lempi kirja',
favoriteBookValue: 'Ruusun nimi',
favoriteSongs: 'lempi biisit',
},
},
// Projects
projects: {
sectionNum: '02.',
sectionTitle: 'Projektit',
projects: [
{
name: 'Klapi - Yhtenäinen sisällönhallintapaneeli',
desc: 'Verkkokäyttöliittymä Livonsaaren Tietokonepajan datan esittämiseen ja hallintaan. Helppo mukauttaa eri asiakasratkaisuihin.',
tags: ['.NET', 'React', 'SQLite'],
},
{
name: 'Hattara - Jaettu pilvitoimisto',
desc: 'Miksi koodata omaa, kun on olemassa toimiva avoimen lähdekoodin ratkaisu, jonka voi hostata omalla palvelimella? Hattara on itsenäinen pilvitoimistoratkaisu pienille tiimeille ja yksityishenkilöille.',
tags: ['Apache2', 'Ubuntu Server', 'NextCloud'],
},
{
name: 'Runosaaren festivaalin verkkosivut - Pro bono -projekti',
desc: 'Paikallisten kulttuurihankkeiden tukeminen on aina ollut minulle tärkeää. Olen ollut mukana auttamassa Runosaaren festivaalia heidän verkkosivuissaan ja verkkonäkyvyydessään jo kuudennetta vuotta!',
tags: ['React', 'Vite', 'Apache2', 'Ubuntu Server'],
},
],
giteaText: 'Tutustu täältä muihin projekteihini',
giteaBtn: 'Katso Giteassa',
folderIcon: '⬡',
externalLinkAria: 'Avaa',
sourceLinkAria: 'Näytä lähdekoodi',
},
// Contact
contact: {
sectionNum: '03.',
sectionTitle: 'Ota yhteyttä',
lede: 'Olen tällä hetkellä työsuhteessa, mutta luen mielenkiinnolla kaikki yhteydenotot. Olipa sinulla kysymys, ehdotus tai tervehdys, yritän parhaani mukaan vastata!',
paragraph: 'Minulla on myös yritys: <a href="https://tietokonepaja.fi/" target="_blank" rel="noreferrer">Livonsaaren Tietokonepaja</a>. Jos olet kiinnostunut yhteistyöstä tai palveluistamme, ota rohkeasti yhteyttä.',
emailBtn: 'Laita mailia',
socials: {
gitea: 'Gitea',
linkedin: 'LinkedIn',
company: 'Yritykseni',
},
},
// Footer
footer: {
designedBy: 'Suunnitellut & toteuttanut',
separator: '·',
stack: 'React + Vite',
},
// NotFound
notFound: {
title: 'Sivua ei löydy',
message: 'En löytänyt etsimääsi sivua :(',
},
// ThemeToggle
theme: {
switchToLight: 'Vaihda vaaleaan teemaan',
switchToDark: 'Vaihda tummaan teemaan',
},
// Language names
languages: {
en: 'English',
fi: 'Suomi',
},
},
};
export const getTranslations = (lang) => {
return translations[lang] || translations.en;
};