feat: setup i18n for static pages

This commit is contained in:
Sergey Yarkov 2022-08-15 17:17:20 +03:00
parent dac7b1c155
commit 8552126506
42 changed files with 483 additions and 143 deletions

View File

@ -17,6 +17,7 @@ const createAnchorLabel = (heading) => {
export default defineConfig({
site: "https://yarkov.tech",
integrations: [solid(), image(), mdx()],
trailingSlash: "always",
markdown: {
syntaxHighlight: "prism",
remarkPlugins: ["remark-gfm", "remark-smartypants"],

View File

@ -1,15 +1,18 @@
import type { Component } from "solid-js";
import { DateFormatter } from "@utils/date";
import { createDateFormatter } from "@utils/date";
import "./ArticleItem.scss";
export type ArticleItemProps = {
data: ArticleType;
lang: string;
};
export const ArticleItem: Component<ArticleItemProps> = (props) => {
// eslint-disable-next-line solid/reactivity
const DateFormatter = createDateFormatter(props.lang);
return (
<article class="article-item">
<a href={`/blog/${new Date(props.data.published_at).toLocaleDateString("en-CA")}/${props.data.slug}`}>
<a href={`/blog/${new Date(props.data.published_at).toLocaleDateString("en-CA")}/${props.data.slug}/`}>
<h3>{props.data.title}</h3>
<p>
<time datetime={props.data.published_at.toString()}>{DateFormatter.format(new Date(props.data.published_at))}</time>

View File

@ -1,5 +1,5 @@
---
import { DateFormatter } from "@root/utils";
import { createDateFormatter, getLanguageFromURL } from "@root/utils";
import { Image } from "@astrojs/image/components";
import "./ArticlePost.scss";
@ -9,6 +9,10 @@ export type Props = {
const { data } = Astro.props as Props;
const { title, tags, author, published_at } = data;
// const lang = getLanguageFromURL(Astro.url.pathname);
// console.log(lang);
const DateFormatter = createDateFormatter("ru");
---
<article class="article">
@ -18,7 +22,7 @@ const { title, tags, author, published_at } = data;
{
tags.map((tag) => (
<li>
<a href={`/articles?tags=${tag.toLocaleLowerCase()}`}>{`${tag}${tags.at(-1) === tag ? "" : ","}`}</a>
<a href={`/articles/?tags=${tag.toLocaleLowerCase()}`}>{`${tag}${tags.at(-1) === tag ? "" : ","}`}</a>
</li>
))
}

View File

@ -8,12 +8,18 @@ import SearchInput from "@components/SearchInput";
import TagsList from "@components/TagsList";
import "./ArticlesList.scss";
type UiStringsType = {
"input.search": string;
"articles.empty": string;
};
type ArticlesListProps = {
data: {
articles: ArticleBlockType;
tags: string[];
};
emptyPlaceholder?: string | undefined;
lang: string;
i18n: UiStringsType;
};
const ArticlesList: Component<ArticlesListProps> = (props) => {
@ -64,12 +70,12 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
return (
<>
<SearchInput count={getLength()} placeholder="поиск по названию или описанию..." />
<SearchInput count={getLength()} placeholder={props.i18n["input.search"]} />
<TagsList data={props.data.tags} />
<div class="articles-list">
{isEmpty() ? (
<div class="articles-list__empty">
<p>{props.emptyPlaceholder || "No articles found..."}</p>
<p>{props.i18n["articles.empty"]}</p>
</div>
) : (
<For
@ -78,7 +84,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
return (
<div class="articles-list__wrapper">
<h2>{year}</h2>
<For each={sortArtcilesByDate(articles()[year])} children={(article) => <ArticleItem data={article} />} />
<For each={sortArtcilesByDate(articles()[year])} children={(article) => <ArticleItem lang={props.lang} data={article} />} />
</div>
);
}}

View File

@ -1,11 +1,13 @@
---
import { SITE_METADATA } from "@root/config";
import { useTranslation } from "@i18n/utils";
export interface Props {
title?: string | undefined;
description?: string | undefined;
}
const t = useTranslation(Astro);
const { title = SITE_METADATA.title, description } = Astro.props as Props;
---
@ -18,7 +20,7 @@ const { title = SITE_METADATA.title, description } = Astro.props as Props;
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description || SITE_METADATA.description} />
<meta name="description" content={description || t("site.description")} />
<meta name="theme-color" content={SITE_METADATA.themeColor} />
<script is:inline src="/assets/js/detect-theme.js"></script>
<slot />

View File

@ -3,22 +3,35 @@ import type { Component, JSX } from "solid-js";
import { createSignal } from "solid-js";
import { createStore } from "solid-js/store";
import { useForm } from "@hooks/index";
import { encode } from "@root/utils/url";
import { encode } from "@utils/url";
import Button from "@components/Button";
import { validateEmail } from "./validations";
import validations from "./validations";
import Submitted from "./Submitted";
import Error from "./Error";
import "./ContactForm.scss";
type UiStringsType = {
"form.contactSuccess": string;
"form.contactError": string;
"input.nameField": string;
"input.subjectField": string;
"input.messageField": string;
"btn.send": string;
"btn.sending": string;
"error.invalidEmail": string;
};
const ErrorMessage: Component<{ message: string }> = (props) => <span class="error-message">{props.message}</span>;
const ContactForm: Component = () => {
const ContactForm: Component<{ i18n: UiStringsType }> = (props) => {
const { formSubmit, validate, errors } = useForm({ errorClass: "error-input" });
const [isSubmitted, setIsSubmitted] = createSignal<boolean>(false);
const [isLoading, setIsLoading] = createSignal<boolean>(false);
const [isError, setIsError] = createSignal<boolean>(false);
const [state, setState] = createStore<ContactFieldsType>({ name: "", email: "", subject: "", message: "", "bot-field": "" });
const { validateEmail } = validations;
const reset = (form: HTMLFormElement) => {
for (const key in state) {
form.elements[key].value = "";
@ -63,13 +76,22 @@ const ContactForm: Component = () => {
</div>
<div class="flex">
<div>
<input onInput={onChange} use:validate name="name" placeholder="Ваше имя" type="text" maxLength="20" autocomplete="off" required />
<input
onInput={onChange}
use:validate
name="name"
placeholder={props.i18n["input.nameField"]}
type="text"
maxLength="20"
autocomplete="off"
required
/>
{errors.name && <ErrorMessage message={errors.name} />}
</div>
<div>
<input
onInput={onChange}
use:validate={[validateEmail]}
use:validate={[validateEmail(props.i18n["error.invalidEmail"])]}
name="email"
placeholder="example@email.com"
autocomplete="off"
@ -84,7 +106,7 @@ const ContactForm: Component = () => {
onInput={onChange}
use:validate
name="subject"
placeholder="Тема сообщения"
placeholder={props.i18n["input.subjectField"]}
type="text"
autocomplete="off"
required
@ -94,7 +116,7 @@ const ContactForm: Component = () => {
onInput={onChange}
use:validate
name="message"
placeholder="Ваше сообщение..."
placeholder={props.i18n["input.messageField"]}
id="message"
autocomplete="off"
cols={30}
@ -104,12 +126,12 @@ const ContactForm: Component = () => {
{errors.message && <ErrorMessage message={errors.message} />}
<div class="form-footer">
{!isError() && !isSubmitted() && (
<Button isLoading={isLoading()} loadingText="Отправка..." type="submit">
Отправить
<Button isLoading={isLoading()} loadingText={props.i18n["btn.sending"]} type="submit">
{props.i18n["btn.send"]}
</Button>
)}
{!isError() && isSubmitted() && <Submitted />}
{isError() && isSubmitted() && <Error />}
{!isError() && isSubmitted() && <Submitted text={props.i18n["form.contactSuccess"]} />}
{isError() && isSubmitted() && <Error text={props.i18n["form.contactError"]} />}
</div>
</form>
);

View File

@ -1,7 +1,7 @@
import type { Component } from "solid-js";
const Submitted: Component = () => {
return <div class="form-result">&#10060; При отправке произошла ошибка. Повторите запрос позже.</div>;
const Submitted: Component<{ text: string }> = (props) => {
return <div class="form-result">&#10060; {props.text}</div>;
};
export default Submitted;

View File

@ -1,7 +1,7 @@
import type { Component } from "solid-js";
const Submitted: Component = () => {
return <div class="form-result">&#10004;&#65039; Сообщение было успешно отправлено!</div>;
const Submitted: Component<{ text: string }> = (props) => {
return <div class="form-result">&#10004;&#65039; {props.text}</div>;
};
export default Submitted;

View File

@ -1,3 +1,7 @@
import { isEmail } from "@utils/string";
export const validateEmail = (field: HTMLInputElement) => (isEmail(field.value) ? false : "Заполните поле в формате e-mail.");
export default {
validateEmail: (error: string) => {
return (field: HTMLInputElement) => (isEmail(field.value) ? false : error);
},
};

View File

@ -2,9 +2,10 @@
import { Icon } from "astro-icon";
import { SITE_METADATA } from "@root/config";
import ThemeSwitcher from "@components/ThemeSwticher";
import { removeSlashes } from "@utils/string";
import UIString from "@components/UIString";
import NavToggle from "./NavToggle";
import "./Header.scss";
import TranslatedLink from "@components/TranslatedLink";
const { name, nav, contacts } = SITE_METADATA;
const { pathname } = Astro.url;
@ -13,23 +14,25 @@ const { pathname } = Astro.url;
<header class="header">
<div class="container">
<div class="header__logo">
<a href="/">{name}</a>
<TranslatedLink to={"/"}>
{name}
</TranslatedLink>
</div>
<nav class="header__nav">
<ul class="header__nav-links">
{
nav.map((link) => (
<li>
<a class={removeSlashes(pathname) === removeSlashes(link.url) && "active"} href={link.url}>
{link.name}
</a>
<TranslatedLink class={pathname.includes(link.url) && "active"} to={link.url}>
<UIString key={link.key} />
</TranslatedLink>
</li>
))
}
</ul>
<div class="header__nav-contacts">
<a href={"mailto:" + contacts.email}>
Почта
Email
<Icon name="email" width="18" height="18" />
</a>
<a href={contacts.githubUrl}>
@ -37,11 +40,14 @@ const { pathname } = Astro.url;
<Icon name="github_filled" width="18" height="18" />
</a>
<a href={contacts.telegramUrl}>
Телеграм
Telegram
<Icon name="telegram" width="18" height="18" />
</a>
</div>
<div class="flex header__nav-settings">
<Icon name="translate" width="21" height="21" />
<ThemeSwitcher client:visible />
</div>
</nav>
<NavToggle client:visible />
</div>

View File

@ -3,8 +3,8 @@
top: 0;
border-bottom: 1px solid var(--border-color);
background-color: var(--background-header-color);
padding-top: 15px;
padding-bottom: 15px;
padding-top: 13.5px;
padding-bottom: 13.5px;
user-select: none;
z-index: 9999;
& .container {
@ -53,7 +53,7 @@
display: flex;
align-items: center;
column-gap: 10px;
margin-right: 40px;
margin-right: 30px;
& a {
text-decoration: none;
font-size: 0;
@ -62,6 +62,9 @@
}
}
}
&-settings {
column-gap: 13.5px;
}
}
}

View File

@ -1,12 +1,16 @@
---
import { Icon } from 'astro-icon';
import './ProjectItem.scss';
import { FALLBACK_LANGUAGE } from "@root/config";
import { getLanguageFromURL } from "@root/utils";
import { Icon } from "astro-icon";
import UIString from "../UIString";
import "./ProjectItem.scss";
export interface Props {
data: ProjectType;
}
const { data } = Astro.props as Props;
const lang = getLanguageFromURL(Astro.url.pathname);
const { icon, name, year, description, articleUrl, sourceUrl, demoUrl } = data;
---
@ -19,25 +23,31 @@ const { icon, name, year, description, articleUrl, sourceUrl, demoUrl } = data;
<span>{year}</span>
</div>
<div class="project-item__description">
<p>{description}</p>
<p>{description[lang] || description[FALLBACK_LANGUAGE]}</p>
</div>
<div class="project-item__links">
{articleUrl && (
{
articleUrl && (
<a class="btn-link" href={articleUrl} target="_blank">
Статья
<UIString key={"btn.article"} />
</a>
)}
{sourceUrl && (
)
}
{
sourceUrl && (
<a class="btn-link" href={sourceUrl} target="_blank">
Исходник
<UIString key={"btn.sourceCode"} />
<Icon name="link-out" width="15" height="15" />
</a>
)}
{demoUrl && (
)
}
{
demoUrl && (
<a class="btn-link" href={demoUrl} target="_blank">
Демо
<UIString key={"btn.demo"} />
<Icon name="link-out" width="15" height="15" />
</a>
)}
)
}
</div>
</div>

View File

@ -1,5 +1,9 @@
---
import UIString from "@components/UIString";
---
<div>
<p>Пока что здесь ничего нет...</p>
<p><UIString key={"recentArticles.empty"} /></p>
</div>
<style lang="scss">

View File

@ -1,7 +1,10 @@
---
import { ArticleItem } from "@components/ArticleItem";
import UIString from "@components/UIString";
import { getLanguageFromURL } from "@utils/url";
import { sortArtcilesByDate } from "@utils/articles";
import EmptyList from "./EmptyList.astro";
import TranslatedLink from "@components/TranslatedLink";
export interface Props {
articles: ArticleType[];
@ -10,18 +13,21 @@ export interface Props {
const { articles, length = 5 } = Astro.props as Props;
const lang = getLanguageFromURL(Astro.url.pathname);
const recent = sortArtcilesByDate(articles);
const isEmpty = recent.length === 0;
---
<section>
<div>
<h2>недавние публикации</h2>
<a href="/articles">смотреть все</a>
<h2><UIString key={"recentArticles.heading"} /></h2>
<TranslatedLink to="/articles/">
<UIString key={"recentArticles.all"} />
</TranslatedLink>
</div>
<div>
{!isEmpty ? recent.slice(0, length).map((article) => <ArticleItem data={article} />) : <EmptyList />}
{!isEmpty ? recent.slice(0, length).map((article) => <ArticleItem lang={lang} data={article} />) : <EmptyList />}
</div>
</section>

View File

@ -1,28 +1,33 @@
---
import { Icon } from 'astro-icon';
import './SkillsBlock.scss';
import { FALLBACK_LANGUAGE } from "@root/config";
import { getLanguageFromURL } from "@root/utils";
import { Icon } from "astro-icon";
import "./SkillsBlock.scss";
export interface Props {
data: SkillsType;
}
const lang = getLanguageFromURL(Astro.url.pathname);
const {
data: { title, list },
} = Astro.props as Props;
---
<div class='skills-block'>
<div class="skills-block">
<div>
<h3>{title}</h3>
<h3>{title[lang] || title[FALLBACK_LANGUAGE]}</h3>
</div>
<div>
<ul>
{list.map((entry) => (
{
list.map((entry) => (
<li>
{entry.icon && <Icon name={entry.icon} width="20" height="20" />}
{entry.name}
</li>
))}
))
}
</ul>
</div>
</div>

View File

@ -1,5 +1,6 @@
---
import { Icon } from "astro-icon";
import TranslatedLink from "@components/TranslatedLink";
export interface Props {
icon: string;
@ -11,9 +12,9 @@ const { icon, href } = Astro.props as Props;
<div>
<Icon name={icon} width="18" height="18" />
<a href={href}>
<TranslatedLink to={href}>
<slot />
</a>
</TranslatedLink>
</div>
<style lang="scss">

View File

@ -0,0 +1,14 @@
---
import { FALLBACK_LANGUAGE } from "@root/config";
import { getLanguageFromURL } from "@root/utils";
export interface Props extends astroHTML.JSX.AnchorHTMLAttributes {
to: string;
}
const lang = getLanguageFromURL(Astro.url.pathname);
const { to, ...attrs } = Astro.props as Props;
const href = lang === FALLBACK_LANGUAGE ? to : `/${lang}${to}`;
---
<a href={href} {...attrs}><slot /></a>

View File

@ -0,0 +1 @@
export { default } from "./TranslatedLink.astro";

View File

@ -0,0 +1,9 @@
---
import { useTranslation } from "@i18n/utils";
export interface Props {
key: UIDictionaryKeys;
}
---
{useTranslation(Astro)(Astro.props.key)}

View File

@ -0,0 +1 @@
export { default } from "./UIString.astro";

View File

@ -1,9 +1,9 @@
export const FALLBACK_LANGUAGE = "ru";
export const SITE_METADATA: SiteMetadataType = {
title: "Sergey Yarkov",
name: "yarkov.tech",
author: "Sergey Yarkov",
description:
"Здесь я публикую статьи и туториалы на различные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.",
themeColor: "#1e2327",
contacts: {
email: "serzh.yarkov@gmail.com",
@ -11,17 +11,20 @@ export const SITE_METADATA: SiteMetadataType = {
telegramUrl: "https://t.me/sergeyyarkov",
},
nav: [
{ name: "Статьи", url: "/articles" },
{ name: "Проекты", url: "/projects" },
{ name: "Обо мне", url: "/about" },
{ name: "Контакты", url: "/contacts" },
{ key: "navigation.articles", url: "/articles/" },
{ key: "navigation.projects", url: "/projects/" },
{ key: "navigation.about", url: "/about/" },
{ key: "navigation.contacts", url: "/contacts/" },
],
projects: [
{
icon: "book",
name: "yarkov.tech",
year: "2022",
description: "Мой персональный веб-сайт где находятся информация обо мне и список различных статей, туториалов...",
description: {
ru: "Мой персональный веб-сайт где находятся информация обо мне и список различных статей, туториалов...",
en: "My personal website where you can find information about me and a list of various articles, tutorials...",
},
sourceUrl: "https://github.com/sergeyyarkov/yarkov.tech",
demoUrl: "https://yarkov.tech",
articleUrl: undefined,
@ -30,7 +33,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "triangle_filled",
name: "devthread",
year: "2021",
description: "Статический веб-сайт для публикации различных статей, разработанный при помощи Gatsby.js и NetlifyCMS",
description: {
ru: "Статический веб-сайт для публикации различных статей, разработанный при помощи Gatsby.js и NetlifyCMS",
en: "Static website for publishing various articles, developed with Gatsby.js and NetlifyCMS",
},
sourceUrl: "https://github.com/sergeyyarkov/devthread.ru",
demoUrl: "https://devthread.netlify.app",
articleUrl: undefined,
@ -39,7 +45,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "round-school",
name: "educt",
year: "2022",
description: "Система управления обучением (LMS) для публикации ваших курсов, лекций, уроков",
description: {
ru: "Система управления обучением (LMS) для публикации ваших курсов, лекций, уроков",
en: "Learning Management System (LMS) to publish your courses, lectures, lessons",
},
sourceUrl: "https://github.com/sergeyyarkov/educt",
demoUrl: "https://client-educt-lms.netlify.app",
articleUrl: undefined,
@ -48,7 +57,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "round-shop-two",
name: "digital-store",
year: "2019",
description: "Настраиваемый интернет-магазин с панелью управления для продажи цифровых товаров",
description: {
ru: "Настраиваемый интернет-магазин с панелью управления для продажи цифровых товаров",
en: "Customizable online store with control panel for selling digital goods",
},
sourceUrl: "https://github.com/sergeyyarkov/digital-store/tree/digital-store-docker",
demoUrl: "https://digital-store.herokuapp.com",
articleUrl: undefined,
@ -57,7 +69,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "chat",
name: "react-chat-app",
year: "2021",
description: "Клиент-серверное веб приложение чата разработанное при помощи Node.js и Socket.io",
description: {
ru: "Клиент-серверное веб приложение чата разработанное при помощи Node.js и Socket.io",
en: "Client-server web chat application developed with Node.js and Socket.io",
},
sourceUrl: "https://github.com/sergeyyarkov/react-chat-app",
demoUrl: undefined,
articleUrl: undefined,
@ -66,7 +81,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "nodejs",
name: "node-docker-example",
year: "2022",
description: "Пример использования Docker для развертывания Node.js приложения",
description: {
ru: "Пример использования Docker для развертывания Node.js приложения",
en: "An example of using Docker to deploy a Node.js application",
},
sourceUrl: "https://github.com/sergeyyarkov/node-docker-example",
demoUrl: "https://website-simple-blog.herokuapp.com",
articleUrl: undefined,
@ -75,7 +93,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "chip",
name: "memory-led-game",
year: "2022",
description: "Аналог популярной игры “Саймон говорит” на AVR микроконтроллере",
description: {
ru: 'Аналог популярной игры "Саймон говорит" на AVR микроконтроллере',
en: 'Analogue of the popular game "Simon says" on the AVR microcontroller',
},
sourceUrl: "https://github.com/sergeyyarkov/attiny24a_memory-led-game",
demoUrl: undefined,
articleUrl: undefined,
@ -83,7 +104,10 @@ export const SITE_METADATA: SiteMetadataType = {
],
skills: [
{
title: "Программирование",
title: {
ru: "Программирование",
en: "Programming",
},
list: [
{ icon: "nodejs", name: "Javascript/Typescript" },
{ icon: "language-c", name: "C Language" },
@ -93,7 +117,10 @@ export const SITE_METADATA: SiteMetadataType = {
],
},
{
title: "Фреймворки",
title: {
ru: "Фреймворки",
en: "Frameworks",
},
list: [
{ icon: "adonisjs", name: "Adonis.js" },
{ icon: "expressjs", name: "Express.js" },
@ -104,7 +131,10 @@ export const SITE_METADATA: SiteMetadataType = {
],
},
{
title: "Инструменты",
title: {
ru: "Инструменты",
en: "Tools",
},
list: [
{ icon: "nodejs", name: "Node.js" },
{ icon: "docker", name: "Docker" },
@ -115,7 +145,10 @@ export const SITE_METADATA: SiteMetadataType = {
],
},
{
title: "Базы данных",
title: {
ru: "Базы данных",
en: "Databases",
},
list: [
{ icon: "postgresql", name: "PostgreSQL" },
{ icon: "mongodb", name: "MongoDB" },
@ -123,11 +156,11 @@ export const SITE_METADATA: SiteMetadataType = {
],
},
{
title: "Языки",
list: [
{ icon: undefined, name: "Русский (родной)" },
{ icon: undefined, name: "Английский (B1)" },
],
title: {
ru: "Языки",
en: "Languages",
},
list: [{ name: "Russian (native)" }, { name: "English (B1)" }],
},
],
};

View File

@ -1,5 +1,18 @@
export default {
"site.title": "yarkov.tech",
"site.description":
"Here I publish articles and tutorials on various IT topics, as well as talk about my projects that I have done over the past few years.",
"articles.empty": "No articles found...",
"recentArticles.empty": "There is nothing here yet...",
/* Page titles */
"pageTitle.about": "About me",
"pageTitle.articles": "Articles",
"pageTitle.projects": "Projects",
"pageTitle.contacts": "Contacts",
/* Page descriptions */
"pageDescription.articles": "Technical articles, tutorials and various reference materials related to IT topics can be found here.",
"pageDescription.projects": "A few of my main projects that have been developed and made available to the public.",
"pageDescription.contacts": "Use the feedback form or write to me on social networks.",
/* Navigation */
"navigation.articles": "Articles",
"navigation.projects": "Projects",
@ -10,7 +23,7 @@ export default {
"splash.subheadline": "Fullstack web-developer & freelancer",
"splash.description":
"I develop websites and web applications of varying complexity, as well as publish various articles and tutorials on various IT topics.",
"splash.about": "about",
"splash.about": "about me",
"splash.contact": "write me",
/* Recent articles */
"recentArticles.heading": "recent publications",
@ -18,4 +31,20 @@ export default {
/* 404 page */
"404.heading": "404",
"404.description": "The page you are trying to find does not exist.",
/* Buttons */
"btn.send": "Submit",
"btn.sending": "Submitting...",
"btn.sourceCode": "Source",
"btn.demo": "Demo",
"btn.article": "Article",
/* Inputs */
"input.nameField": "Your name",
"input.subjectField": "Message subject",
"input.messageField": "Your message...",
"input.search": "search by title or description...",
/* Forms */
"form.contactSuccess": "The message was submitted successfully!",
"form.contactError": "An error occurred while sending. Please try again later.",
/* Error messages */
"error.invalidEmail": "Fill in the field in the e-mail format.",
};

4
src/i18n/languages.ts Normal file
View File

@ -0,0 +1,4 @@
export default {
ru: "Русский",
en: "English",
};

View File

@ -1,5 +1,18 @@
export default {
"site.title": "yarkov.tech",
"site.description":
"Здесь я публикую статьи и туториалы на различные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.",
"articles.empty": "Не найдено ни одной статьи...",
"recentArticles.empty": "Пока что здесь ничего нет...",
/* Page titles */
"pageTitle.about": "Обо мне",
"pageTitle.articles": "Статьи",
"pageTitle.projects": "Проекты",
"pageTitle.contacts": "Контакты",
/* Page descriptions */
"pageDescription.articles": "Технические статьи, туториалы и различные справочные материалы которые связаны с IT тематикой находятся здесь.",
"pageDescription.projects": "Несколько моих основных проектов которые были разработаны и выложены в открытый доступ.",
"pageDescription.contacts": "Воспользуйтесь формой обратной связи или напишите мне в социальные сети.",
/* Navigation */
"navigation.articles": "Статьи",
"navigation.projects": "Проекты",
@ -18,4 +31,20 @@ export default {
/* 404 page */
"404.heading": "404",
"404.description": "Страницу которую вы пытаетесь найти не существует.",
/* Buttons */
"btn.send": "Отправить",
"btn.sending": "Отправка...",
"btn.sourceCode": "Исходник",
"btn.demo": "Демо",
"btn.article": "Статья",
/* Inputs */
"input.nameField": "Ваше имя",
"input.subjectField": "Тема сообщения",
"input.messageField": "Ваше сообщение...",
"input.search": "поиск по названию или описанию...",
/* Forms */
"form.contactSuccess": "Сообщение было успешно отправлено!",
"form.contactError": "При отправке произошла ошибка. Повторите запрос позже.",
/* Error messages */
"error.invalidEmail": "Заполните поле в формате e-mail.",
};

28
src/i18n/utils.ts Normal file
View File

@ -0,0 +1,28 @@
import type { AstroGlobal } from "astro";
import * as utils from "@utils/url";
import { FALLBACK_LANGUAGE } from "@root/config";
import languages from "./languages";
export function getLanguageKeys(): Array<string> {
return Object.keys(languages);
}
function transformExports<T>(modules: Record<string, { default: T }>) {
const translations: Record<string, T> = {};
for (const [path, module] of Object.entries(modules)) {
const [, lang] = path.split("/");
translations[lang] = module.default;
}
return translations;
}
const translations = transformExports<UIDictionaryKeys>(import.meta.glob("./*/ui.ts", { eager: true }));
export function useTranslation(Astro: Readonly<AstroGlobal>): (key: UIDictionaryKeys) => string | undefined {
const lang = utils.getLanguageFromURL(Astro.url.pathname);
return function translate(key: UIDictionaryKeys): string | undefined {
const string: string | undefined = translations[lang]?.[key] || translations[FALLBACK_LANGUAGE][key];
if (string === undefined) console.error(`Cannot find any string for translation key "${key}" on "${Astro.url.pathname}" page.`);
return string;
};
}

3
src/icons/translate.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.2612 13.1862L9.03875 10.99L9.065 10.9637C10.5474 9.31621 11.6549 7.36685 12.3112 5.25H14.875V3.5H8.75V1.75H7V3.5H0.875V5.24125H10.6488C10.0625 6.93 9.135 8.53125 7.875 9.93125C7.06125 9.03 6.3875 8.04125 5.85375 7H4.10375C4.7425 8.42625 5.6175 9.77375 6.71125 10.99L2.2575 15.3825L3.5 16.625L7.875 12.25L10.5962 14.9713L11.2612 13.1862ZM16.1875 8.75H14.4375L10.5 19.25H12.25L13.23 16.625H17.3863L18.375 19.25H20.125L16.1875 8.75ZM13.895 14.875L15.3125 11.0863L16.73 14.875H13.895V14.875Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@ -0,0 +1,13 @@
---
import { getLanguageKeys } from "@i18n/utils";
import { FALLBACK_LANGUAGE } from "@root/config";
import About from "@templates/About.astro";
export function getStaticPaths() {
return getLanguageKeys()
.filter((lang) => lang !== FALLBACK_LANGUAGE)
.map((lang) => ({ params: { lang } }));
}
---
<About />

View File

@ -0,0 +1,13 @@
---
import { getLanguageKeys } from "@i18n/utils";
import { FALLBACK_LANGUAGE } from "@root/config";
import Articles from "@templates/Articles.astro";
export function getStaticPaths() {
return getLanguageKeys()
.filter((lang) => lang !== FALLBACK_LANGUAGE)
.map((lang) => ({ params: { lang } }));
}
---
<Articles />

View File

@ -0,0 +1,13 @@
---
import { getLanguageKeys } from "@i18n/utils";
import { FALLBACK_LANGUAGE } from "@root/config";
import Contacts from "@templates/Contacts.astro";
export function getStaticPaths() {
return getLanguageKeys()
.filter((lang) => lang !== FALLBACK_LANGUAGE)
.map((lang) => ({ params: { lang } }));
}
---
<Contacts />

View File

@ -0,0 +1,13 @@
---
import { getLanguageKeys } from "@i18n/utils";
import { FALLBACK_LANGUAGE } from "@root/config";
import Home from "@templates/Home.astro";
export function getStaticPaths() {
return getLanguageKeys()
.filter((lang) => lang !== FALLBACK_LANGUAGE)
.map((lang) => ({ params: { lang } }));
}
---
<Home />

View File

@ -0,0 +1,13 @@
---
import { getLanguageKeys } from "@i18n/utils";
import { FALLBACK_LANGUAGE } from "@root/config";
import Projects from "@templates/Projects.astro";
export function getStaticPaths() {
return getLanguageKeys()
.filter((lang) => lang !== FALLBACK_LANGUAGE)
.map((lang) => ({ params: { lang } }));
}
---
<Projects />

View File

@ -1,17 +1,21 @@
---
import { SITE_METADATA } from '@root/config';
import PageLayout from '@layouts/default.astro';
import SkillsList from '@components/SkillsList';
import SkillsBlock from '@components/SkillsBlock';
import { SITE_METADATA } from "@root/config";
import PageLayout from "@layouts/default.astro";
import SkillsList from "@components/SkillsList";
import SkillsBlock from "@components/SkillsBlock";
// TODO: import content based on user ui language
import { default as Content } from '../../content/about/ru/index.mdx';
import ArticleContent from '@components/ArticleContent';
import ArticleContent from "@components/ArticleContent";
import { getLanguageFromURL } from "@root/utils";
import { useTranslation } from "@i18n/utils";
const lang = getLanguageFromURL(Astro.url.pathname);
const t = useTranslation(Astro);
const { Content } = await import(`../../content/about/${lang}/index.mdx`);
const { skills } = SITE_METADATA;
---
<PageLayout title="Обо мне">
<PageLayout lang={lang} title={t("pageTitle.about")}>
<section>
<ArticleContent>
<Content />

View File

@ -2,20 +2,25 @@
import PageLayout from "@layouts/default.astro";
import ArticlesList from "@components/ArticlesList";
import { transformArticles, formatToArticleBlocks } from "@utils/articles";
import { getLanguageFromURL } from "@root/utils";
import { useTranslation } from "@i18n/utils";
const t = useTranslation(Astro);
const articles = await Astro.glob<ArticleType>("../../content/articles/**/*.mdx");
const tags = new Set(articles.map((article) => article.frontmatter.tags).flat());
const lang = getLanguageFromURL(Astro.url.pathname);
---
<PageLayout
title="Статьи"
description="Технические статьи, туториалы и различные справочные материалы которые связаны с IT тематикой находятся здесь."
>
<PageLayout lang={lang} title={t("pageTitle.articles")} description={t("pageDescription.articles")}>
<section>
<ArticlesList
client:load
lang={lang}
i18n={{
"input.search": t("input.search"),
"articles.empty": t("articles.empty"),
}}
data={{ articles: formatToArticleBlocks(transformArticles(articles)), tags: [...tags] }}
emptyPlaceholder="Не найдено ни одной статьи..."
/>
</section>
</PageLayout>

View File

@ -2,11 +2,28 @@
import PageLayout from "@layouts/default.astro";
import ContactForm from "@components/ContactForm";
import ContactsList from "@components/ContactsList";
import { getLanguageFromURL } from "@root/utils";
import { useTranslation } from "@i18n/utils";
const t = useTranslation(Astro);
const lang = getLanguageFromURL(Astro.url.pathname);
---
<PageLayout title="Контакты" description="Воспользуйтесь формой обратной связи или напишите мне в социальные сети.">
<PageLayout lang={lang} title={t("pageTitle.contacts")} description={t("pageDescription.contacts")}>
<section>
<ContactForm client:idle />
<ContactForm
i18n={{
"input.nameField": t("input.nameField"),
"input.subjectField": t("input.subjectField"),
"input.messageField": t("input.messageField"),
"btn.send": t("btn.send"),
"btn.sending": t("btn.sending"),
"error.invalidEmail": t("error.invalidEmail"),
"form.contactSuccess": t("form.contactSuccess"),
"form.contactError": t("form.contactError"),
}}
client:load
/>
<ContactsList />
</section>
</PageLayout>

View File

@ -3,23 +3,28 @@ import * as utils from "@utils/index";
import PageLayout from "@layouts/default.astro";
import RecentArticles from "@components/RecentArticles";
import { Splash, SplashHeading, SplashLinks, Link } from "@components/Splash";
import { getLanguageFromURL } from "@root/utils";
import { useTranslation } from "@i18n/utils";
import UIString from "@components/UIString";
const data = await Astro.glob<ArticleType>("../../content/articles/**/*.mdx");
const articles = utils.transformArticles(data);
const t = useTranslation(Astro);
const lang = getLanguageFromURL(Astro.url.pathname);
---
<PageLayout showPageHeading={false}>
<PageLayout lang={lang} description={t("site.description")} showPageHeading={false}>
<Splash>
<SplashHeading>
<hgroup>
<h1>Hello, I'am Sergey</h1>
<h1><UIString key={"splash.headline"} /></h1>
<h2>Fullstack <b>web-developer</b> & <b>freelancer</b></h2>
</hgroup>
<p>Я занимаюсь разработкой веб-сайтов и веб-приложений разной сложности, а также публикую различные статьи и туториалы на разные IT темы.</p>
<p><UIString key={"splash.description"} /></p>
</SplashHeading>
<SplashLinks>
<Link icon="information" href="/about">обо мне</Link>
<Link icon="email_filled" href="/contacts">написать мне</Link>
<Link icon="information" href="/about/"><UIString key={"splash.about"} /></Link>
<Link icon="email_filled" href="/contacts/"><UIString key={"splash.contact"} /></Link>
</SplashLinks>
</Splash>
<RecentArticles articles={articles} length={4} />

View File

@ -1,10 +1,15 @@
---
import PageLayout from '@layouts/default.astro';
import PageLayout from "@layouts/default.astro";
import { useTranslation } from "@i18n/utils";
import { getLanguageFromURL } from "@root/utils";
const lang = getLanguageFromURL(Astro.url.pathname);
const t = useTranslation(Astro);
---
<PageLayout title="404" showPageHeading={false}>
<PageLayout lang={lang} title="404" showPageHeading={false}>
<div class="container">
<h1 style="font-size:var(--font-size-5xl);">404</h1>
<p>Страницу которую вы пытаетесь найти не существует.</p>
<p>{t("404.description")}</p>
</div>
</PageLayout>

View File

@ -1,13 +1,17 @@
---
import { SITE_METADATA } from '@root/config';
import PageLayout from '@layouts/default.astro';
import ProjectsList from '@components/ProjectsList';
import ProjectItem from '@components/ProjectItem';
import { SITE_METADATA } from "@root/config";
import PageLayout from "@layouts/default.astro";
import ProjectsList from "@components/ProjectsList";
import ProjectItem from "@components/ProjectItem";
import { getLanguageFromURL } from "@root/utils";
import { useTranslation } from "@i18n/utils";
const t = useTranslation(Astro);
const lang = getLanguageFromURL(Astro.url.pathname);
const { projects } = SITE_METADATA;
---
<PageLayout title="Проекты" description="Несколько моих основных проектов которые были разработаны и выложены в открытый доступ.">
<PageLayout lang={lang} title={t("pageTitle.projects")} description={t("pageDescription.projects")}>
<section>
<ProjectsList>
{projects.map((project) => <ProjectItem data={project} />)}

24
src/types/main.d.ts vendored
View File

@ -1,3 +1,5 @@
import uiKeys from "@i18n/ru/ui";
import languages from "@i18n/languages";
export {};
declare global {
@ -10,28 +12,22 @@ declare global {
onthemetoggled: CustomEvent<ThemeModeType>;
}
type LanguageKeys = keyof typeof languages;
type UIDictionaryKeys = keyof typeof uiKeys;
type SiteMetadataType = {
title: string;
name: string;
author: string;
description: string;
themeColor: string;
contacts: {
email: string;
githubUrl: string;
telegramUrl: string;
};
nav: Array<{ name: string; url: string }>;
projects: Array<{
icon: string;
name: string;
year: string;
description: string;
sourceUrl?: string | undefined;
demoUrl?: string | undefined;
articleUrl?: string | undefined;
}>;
skills: Array<{ title: string; list: Array<{ icon: string; name: string }> }>;
nav: Array<{ key: UIDictionaryKeys; url: string }>;
projects: Array<ProjectType>;
skills: Array<SkillsType>;
};
type ArticleType = {
@ -48,14 +44,14 @@ declare global {
icon: string;
name: string;
year: string;
description: string;
description: Record<LanguageKeys, string>;
sourceUrl?: string | undefined;
articleUrl?: string | undefined;
demoUrl?: string | undefined;
};
type SkillsType = {
title: string;
title: Record<LanguageKeys, string>;
list: Array<{ icon?: string | undefined; name: string }>;
};

View File

@ -1,6 +1,8 @@
export const DateFormatter = new Intl.DateTimeFormat(undefined, {
export function createDateFormatter(lang: string) {
return new Intl.DateTimeFormat(lang, {
timeZone: "UTC",
month: "short",
year: "numeric",
day: "numeric",
});
}

View File

@ -1,3 +1,4 @@
export * from "./articles";
export * from "./date";
export * from "./history";
export * from "./url";

View File

@ -1,5 +1,12 @@
import { FALLBACK_LANGUAGE } from "@root/config";
export const encode = (data: object) => {
return Object.keys(data)
.map((key) => encodeURIComponent(key) + "=" + encodeURIComponent(data[key]))
.join("&");
};
export const getLanguageFromURL = (pathname: string) => {
const langCodeMatch = pathname.match(/\/([a-z]{1}-?[a-z]{1})\//);
return langCodeMatch ? langCodeMatch[1] : FALLBACK_LANGUAGE;
};

View File

@ -17,7 +17,8 @@
"@layouts/*": ["./src/layouts/*"],
"@utils/*": ["./src/utils/*"],
"@stores/*": ["./src/stores/*"],
"@hooks/*": ["./src/hooks/*"]
"@hooks/*": ["./src/hooks/*"],
"@i18n/*": ["./src/i18n/*"]
},
"outDir": "./dist",
"typeRoots": ["./node_modules/@types", "./src/types"]