Make the resume bilingual and part of the shared site chrome
Deploy to GitHub Pages / build (push) Canceled after 0s
Deploy to GitHub Pages / deploy (push) Canceled after 0s

Adds a Russian version (data/resume.ru.json, default language) with an RU/EN
switch stored under the key shared with the front page and the portfolio.
The top bar becomes the site-wide one (brand, Resume, Portfolio, Gitea,
Contact, language, theme) with a second row of anchors for this page's
sections, and the fonts move to Unbounded and Onest so the three pages share
one type system. Portfolio links now point at /portfolio/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XtcZtnG4GQA1d6mW5PNrS9
This commit is contained in:
Emil
2026-09-20 23:48:38 +03:00
co-authored by Claude Sonnet 5
parent 4d485fc19d
commit 04c1cc5877
27 changed files with 425 additions and 176 deletions
+88 -50
View File
@@ -1,78 +1,108 @@
import { Bi } from '@/lib/i18n'
import type { Resume } from '@/lib/types'
import styles from './sections.module.css'
export function Skills({ resume }: Readonly<{ resume: Resume }>) {
// The English resume gives the structure; the Russian one is paired with it by position, and anything the
// Russian file lacks falls back to the English text.
type Pair = { en: Resume; ru: Resume }
export function Skills({ en, ru }: Readonly<Pair>) {
return (
<section className={styles.section} id="skills">
<div className={styles.shell}>
<h2 className={styles.title}>What I work with</h2>
<h2 className={styles.title}><Bi ru="Чем я занимаюсь" en="What I work with" /></h2>
<div className={styles.skills}>
{resume.skills.map((skill) => (
<article className={styles.skill} key={skill.title}>
<h3>{skill.title}</h3>
<p>{skill.text}</p>
<ul className={styles.tags}>{skill.stack.map((tag) => <li key={tag}>{tag}</li>)}</ul>
</article>
))}
{en.skills.map((skill, i) => {
const r = ru.skills?.[i]
return (
<article className={styles.skill} key={skill.title}>
<h3><Bi ru={r?.title ?? skill.title} en={skill.title} /></h3>
<p><Bi ru={r?.text ?? skill.text} en={skill.text} /></p>
<ul className={styles.tags}>
{skill.stack.map((tag, t) => (
<li key={tag}><Bi ru={r?.stack?.[t] ?? tag} en={tag} /></li>
))}
</ul>
</article>
)
})}
</div>
</div>
</section>
)
}
export function Experience({ resume }: Readonly<{ resume: Resume }>) {
export function Experience({ en, ru }: Readonly<Pair>) {
return (
<section className={styles.section} id="experience">
<div className={styles.shell}>
<h2 className={styles.title}>Experience</h2>
<h2 className={styles.title}><Bi ru="Опыт" en="Experience" /></h2>
<div className={styles.entries}>
{resume.experience.map((entry) => (
<article className={styles.entry} key={entry.title}>
<p className={styles.meta}>{entry.meta}</p>
<div>
<h3>{entry.title}</h3>
<ul className={styles.list}>{entry.points.map((point) => <li key={point}>{point}</li>)}</ul>
</div>
</article>
))}
{en.experience.map((entry, i) => {
const r = ru.experience?.[i]
return (
<article className={styles.entry} key={entry.title}>
<p className={styles.meta}><Bi ru={r?.meta ?? entry.meta} en={entry.meta} /></p>
<div>
<h3><Bi ru={r?.title ?? entry.title} en={entry.title} /></h3>
<ul className={styles.list}>
{entry.points.map((point, p) => (
<li key={point}><Bi ru={r?.points?.[p] ?? point} en={point} /></li>
))}
</ul>
</div>
</article>
)
})}
</div>
</div>
</section>
)
}
export function Education({ resume }: Readonly<{ resume: Resume }>) {
export function Education({ en, ru }: Readonly<Pair>) {
return (
<section className={styles.section} id="education">
<div className={styles.shell}>
<h2 className={styles.title}>Education and languages</h2>
<h2 className={styles.title}><Bi ru="Образование и языки" en="Education and languages" /></h2>
<div className={styles.columns}>
<div>
<h3 className={styles.label}>Education</h3>
<h3 className={styles.label}><Bi ru="Образование" en="Education" /></h3>
<div className={styles.stack}>
{resume.education.map((entry) => (
<div key={entry.institution}>
<p className={styles.strong}>{entry.institution}</p>
<p>{entry.program}</p>
<p className={styles.meta}>{entry.period}</p>
</div>
))}
{en.education.map((entry, i) => {
const r = ru.education?.[i]
return (
<div key={entry.institution}>
<p className={styles.strong}><Bi ru={r?.institution ?? entry.institution} en={entry.institution} /></p>
<p><Bi ru={r?.program ?? entry.program} en={entry.program} /></p>
<p className={styles.meta}><Bi ru={r?.period ?? entry.period} en={entry.period} /></p>
</div>
)
})}
</div>
</div>
<div>
<h3 className={styles.label}>Languages</h3>
<h3 className={styles.label}><Bi ru="Языки" en="Languages" /></h3>
<dl className={styles.languages}>
{resume.languages.map((language) => (
<div key={language.name}>
<dt>{language.name}</dt>
<dd>{language.level}</dd>
</div>
))}
{en.languages.map((language, i) => {
const r = ru.languages?.[i]
return (
<div key={language.name}>
<dt><Bi ru={r?.name ?? language.name} en={language.name} /></dt>
<dd><Bi ru={r?.level ?? language.level} en={language.level} /></dd>
</div>
)
})}
</dl>
</div>
<div>
<h3 className={styles.label}>Interests</h3>
<p>{resume.interests.join(', ')}.</p>
<h3 className={styles.label}><Bi ru="Интересы" en="Interests" /></h3>
<p>
<Bi
ru={`${(ru.interests?.length ? ru.interests : en.interests).join(', ')}.`}
en={`${en.interests.join(', ')}.`}
/>
</p>
</div>
</div>
</div>
@@ -80,23 +110,31 @@ export function Education({ resume }: Readonly<{ resume: Resume }>) {
)
}
export function Contact({ resume }: Readonly<{ resume: Resume }>) {
const [primary, ...others] = resume.contacts
export function Contact({ en, ru }: Readonly<Pair>) {
const [primary, ...others] = en.contacts
return (
<footer className={styles.contact} id="contact">
<div className={styles.shell}>
<h2 className={styles.contactTitle}>Get in touch</h2>
<p className={styles.contactLead}>Email is the fastest way to reach me.</p>
<h2 className={styles.contactTitle}><Bi ru="Связаться" en="Get in touch" /></h2>
<p className={styles.contactLead}><Bi ru="Почта — самый быстрый способ." en="Email is the fastest way to reach me." /></p>
<a className={styles.mail} href={primary.href}>{primary.value}</a>
<ul className={styles.contacts}>
{others.map((contact) => (
<li key={contact.value}>
<span>{contact.label}</span>
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer">{contact.value}</a>
</li>
))}
{others.map((contact, i) => {
const label = ru.contacts?.[i + 1]?.label ?? contact.label
return (
<li key={contact.value}>
<span>{label === contact.label ? contact.label : <Bi ru={label} en={contact.label} />}</span>
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer">{contact.value}</a>
</li>
)
})}
</ul>
<p className={styles.legal}>© 2026 {resume.name}. The text and the projects on this page are read live from Gitea.</p>
<p className={styles.legal}>
<Bi
ru="© 2026 Эмиль Шанаев. Сайт работает на собственном сервере: Gitea, Caddy и Docker. Резюме и проекты подтягиваются из репозиториев."
en="© 2026 Emil Shanayev. This site runs on my own server: Gitea, Caddy and Docker. The resume and projects are read from repositories."
/>
</p>
</div>
</footer>
)