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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13 -8
View File
@@ -1,3 +1,9 @@
/* Bundled fonts (SIL Open Font License): Unbounded for headings, Onest for text; Latin and Cyrillic subsets. */
@font-face { font-family: 'Unbounded'; font-weight: 200 900; font-display: swap; src: url('./fonts/unbounded-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Unbounded'; font-weight: 200 900; font-display: swap; src: url('./fonts/unbounded-cyrillic.woff2') format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
@font-face { font-family: 'Onest'; font-weight: 100 900; font-display: swap; src: url('./fonts/onest-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Onest'; font-weight: 100 900; font-display: swap; src: url('./fonts/onest-cyrillic.woff2') format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
/* Design tokens: a studio-lit 3D viewport. Axis colours (X red, Y green, Z blue) are marks and lines, not text. */
:root {
color-scheme: light;
@@ -38,9 +44,6 @@
--band-bg: #15181d;
--band-ink: #f4f6f8;
/* Light type on a dark ground looks heavier, so the display weight is eased off there */
--name-weight: 800;
--name-tracking: -0.048em;
/* Derived from the palette above, so they follow the theme */
--line: color-mix(in srgb, var(--ink) 14%, transparent);
@@ -61,8 +64,8 @@
--gutter: clamp(20px, 3vw, 32px);
--section: clamp(84px, 11vw, 148px);
--font-body: var(--font-text), 'Segoe UI', system-ui, sans-serif;
--font-head: var(--font-display), 'Segoe UI', system-ui, sans-serif;
--font-body: 'Onest', 'Segoe UI', system-ui, sans-serif;
--font-head: 'Unbounded', 'Segoe UI', system-ui, sans-serif;
}
:root[data-theme='dark'] {
@@ -98,12 +101,10 @@
--band-bg: #e8ebef;
--band-ink: #15181d;
--name-weight: 700;
--name-tracking: -0.036em;
}
* { box-sizing: border-box; }
html { background: var(--paper); scroll-behavior: smooth; scroll-padding-top: 88px; }
html { background: var(--paper); scroll-behavior: smooth; scroll-padding-top: 124px; }
body {
margin: 0;
color: var(--ink);
@@ -121,3 +122,7 @@ ul { padding: 0; list-style: none; }
html { scroll-behavior: auto; }
*, *::before, *::after { animation-duration: 0.01ms !important; animation-delay: 0ms !important; transition-duration: 0.01ms !important; }
}
/* Bilingual text: both languages are in the markup, the html[data-lang] attribute picks one. */
html[data-lang='ru'] [data-en],
html[data-lang='en'] [data-ru] { display: none !important; }
+15 -12
View File
@@ -1,25 +1,28 @@
import type { Metadata } from 'next'
import localFont from 'next/font/local'
import './globals.css'
// Both fonts are bundled (SIL Open Font License), so building never depends on the network.
const display = localFont({ src: './fonts/BricolageGrotesque-Variable.woff2', variable: '--font-display', weight: '200 800', display: 'swap' })
const text = localFont({ src: './fonts/InstrumentSans-Variable.woff2', variable: '--font-text', weight: '400 700', display: 'swap' })
export const metadata: Metadata = {
title: 'Emil Shanayev — Resume',
description: 'Resume of Emil Shanayev: backend, DevOps, AI systems, scientific software and game technology.',
authors: [{ name: 'Emil Shanayev' }],
title: 'Эмиль Шанаев — Резюме',
description: 'Резюме Эмиля Шанаева: бэкенд, DevOps, системы на базе ИИ, научное ПО и игровые технологии.',
authors: [{ name: 'Эмиль Шанаев' }],
}
// Dark is the default theme; a stored choice of "light" is applied before the first paint so the page never flashes.
const THEME_SCRIPT = "try{if(localStorage.getItem('theme')==='light')document.documentElement.dataset.theme='light'}catch(e){}"
// Before the first paint: the language (the stored choice, otherwise Russian) and the theme (dark unless "light" was
// chosen). Both keys are shared with the rest of shanaty.ru, so a choice carries over between the pages.
const BOOT_SCRIPT = `try{var d=document.documentElement,l=localStorage.getItem('lang');if(l!=='ru'&&l!=='en')l='ru';d.dataset.lang=l;d.lang=l;if(localStorage.getItem('theme')==='light')d.dataset.theme='light';document.title=d.getAttribute('data-title-'+l)||document.title}catch(e){}`
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" data-theme="dark" className={`${display.variable} ${text.variable}`} suppressHydrationWarning>
<html
lang="ru"
data-lang="ru"
data-theme="dark"
data-title-ru="Эмиль Шанаев — Резюме"
data-title-en="Emil Shanayev — Resume"
suppressHydrationWarning
>
<head>
<script dangerouslySetInnerHTML={{ __html: THEME_SCRIPT }} />
<script dangerouslySetInnerHTML={{ __html: BOOT_SCRIPT }} />
</head>
<body>{children}</body>
</html>
+3 -2
View File
@@ -1,7 +1,8 @@
import Site from '@/components/Site'
import resume from '@/data/resume.json'
import resumeEn from '@/data/resume.json'
import resumeRu from '@/data/resume.ru.json'
import type { Resume } from '@/lib/types'
export default function Home() {
return <Site initial={resume as unknown as Resume} />
return <Site initialEn={resumeEn as unknown as Resume} initialRu={resumeRu as unknown as Resume} />
}