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({ export default defineConfig({
site: "https://yarkov.tech", site: "https://yarkov.tech",
integrations: [solid(), image(), mdx()], integrations: [solid(), image(), mdx()],
trailingSlash: "always",
markdown: { markdown: {
syntaxHighlight: "prism", syntaxHighlight: "prism",
remarkPlugins: ["remark-gfm", "remark-smartypants"], remarkPlugins: ["remark-gfm", "remark-smartypants"],

View File

@ -1,15 +1,18 @@
import type { Component } from "solid-js"; import type { Component } from "solid-js";
import { DateFormatter } from "@utils/date"; import { createDateFormatter } from "@utils/date";
import "./ArticleItem.scss"; import "./ArticleItem.scss";
export type ArticleItemProps = { export type ArticleItemProps = {
data: ArticleType; data: ArticleType;
lang: string;
}; };
export const ArticleItem: Component<ArticleItemProps> = (props) => { export const ArticleItem: Component<ArticleItemProps> = (props) => {
// eslint-disable-next-line solid/reactivity
const DateFormatter = createDateFormatter(props.lang);
return ( return (
<article class="article-item"> <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> <h3>{props.data.title}</h3>
<p> <p>
<time datetime={props.data.published_at.toString()}>{DateFormatter.format(new Date(props.data.published_at))}</time> <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 { Image } from "@astrojs/image/components";
import "./ArticlePost.scss"; import "./ArticlePost.scss";
@ -9,6 +9,10 @@ export type Props = {
const { data } = Astro.props as Props; const { data } = Astro.props as Props;
const { title, tags, author, published_at } = data; const { title, tags, author, published_at } = data;
// const lang = getLanguageFromURL(Astro.url.pathname);
// console.log(lang);
const DateFormatter = createDateFormatter("ru");
--- ---
<article class="article"> <article class="article">
@ -18,7 +22,7 @@ const { title, tags, author, published_at } = data;
{ {
tags.map((tag) => ( tags.map((tag) => (
<li> <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> </li>
)) ))
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
--- ---
import { Icon } from "astro-icon"; import { Icon } from "astro-icon";
import TranslatedLink from "@components/TranslatedLink";
export interface Props { export interface Props {
icon: string; icon: string;
@ -11,9 +12,9 @@ const { icon, href } = Astro.props as Props;
<div> <div>
<Icon name={icon} width="18" height="18" /> <Icon name={icon} width="18" height="18" />
<a href={href}> <TranslatedLink to={href}>
<slot /> <slot />
</a> </TranslatedLink>
</div> </div>
<style lang="scss"> <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 = { export const SITE_METADATA: SiteMetadataType = {
title: "Sergey Yarkov", title: "Sergey Yarkov",
name: "yarkov.tech", name: "yarkov.tech",
author: "Sergey Yarkov", author: "Sergey Yarkov",
description:
"Здесь я публикую статьи и туториалы на различные IT темы, а также рассказываю о своих проектах которые я сделал за последние несколько лет.",
themeColor: "#1e2327", themeColor: "#1e2327",
contacts: { contacts: {
email: "serzh.yarkov@gmail.com", email: "serzh.yarkov@gmail.com",
@ -11,17 +11,20 @@ export const SITE_METADATA: SiteMetadataType = {
telegramUrl: "https://t.me/sergeyyarkov", telegramUrl: "https://t.me/sergeyyarkov",
}, },
nav: [ nav: [
{ name: "Статьи", url: "/articles" }, { key: "navigation.articles", url: "/articles/" },
{ name: "Проекты", url: "/projects" }, { key: "navigation.projects", url: "/projects/" },
{ name: "Обо мне", url: "/about" }, { key: "navigation.about", url: "/about/" },
{ name: "Контакты", url: "/contacts" }, { key: "navigation.contacts", url: "/contacts/" },
], ],
projects: [ projects: [
{ {
icon: "book", icon: "book",
name: "yarkov.tech", name: "yarkov.tech",
year: "2022", 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", sourceUrl: "https://github.com/sergeyyarkov/yarkov.tech",
demoUrl: "https://yarkov.tech", demoUrl: "https://yarkov.tech",
articleUrl: undefined, articleUrl: undefined,
@ -30,7 +33,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "triangle_filled", icon: "triangle_filled",
name: "devthread", name: "devthread",
year: "2021", 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", sourceUrl: "https://github.com/sergeyyarkov/devthread.ru",
demoUrl: "https://devthread.netlify.app", demoUrl: "https://devthread.netlify.app",
articleUrl: undefined, articleUrl: undefined,
@ -39,7 +45,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "round-school", icon: "round-school",
name: "educt", name: "educt",
year: "2022", year: "2022",
description: "Система управления обучением (LMS) для публикации ваших курсов, лекций, уроков", description: {
ru: "Система управления обучением (LMS) для публикации ваших курсов, лекций, уроков",
en: "Learning Management System (LMS) to publish your courses, lectures, lessons",
},
sourceUrl: "https://github.com/sergeyyarkov/educt", sourceUrl: "https://github.com/sergeyyarkov/educt",
demoUrl: "https://client-educt-lms.netlify.app", demoUrl: "https://client-educt-lms.netlify.app",
articleUrl: undefined, articleUrl: undefined,
@ -48,7 +57,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "round-shop-two", icon: "round-shop-two",
name: "digital-store", name: "digital-store",
year: "2019", 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", sourceUrl: "https://github.com/sergeyyarkov/digital-store/tree/digital-store-docker",
demoUrl: "https://digital-store.herokuapp.com", demoUrl: "https://digital-store.herokuapp.com",
articleUrl: undefined, articleUrl: undefined,
@ -57,7 +69,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "chat", icon: "chat",
name: "react-chat-app", name: "react-chat-app",
year: "2021", 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", sourceUrl: "https://github.com/sergeyyarkov/react-chat-app",
demoUrl: undefined, demoUrl: undefined,
articleUrl: undefined, articleUrl: undefined,
@ -66,7 +81,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "nodejs", icon: "nodejs",
name: "node-docker-example", name: "node-docker-example",
year: "2022", 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", sourceUrl: "https://github.com/sergeyyarkov/node-docker-example",
demoUrl: "https://website-simple-blog.herokuapp.com", demoUrl: "https://website-simple-blog.herokuapp.com",
articleUrl: undefined, articleUrl: undefined,
@ -75,7 +93,10 @@ export const SITE_METADATA: SiteMetadataType = {
icon: "chip", icon: "chip",
name: "memory-led-game", name: "memory-led-game",
year: "2022", 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", sourceUrl: "https://github.com/sergeyyarkov/attiny24a_memory-led-game",
demoUrl: undefined, demoUrl: undefined,
articleUrl: undefined, articleUrl: undefined,
@ -83,7 +104,10 @@ export const SITE_METADATA: SiteMetadataType = {
], ],
skills: [ skills: [
{ {
title: "Программирование", title: {
ru: "Программирование",
en: "Programming",
},
list: [ list: [
{ icon: "nodejs", name: "Javascript/Typescript" }, { icon: "nodejs", name: "Javascript/Typescript" },
{ icon: "language-c", name: "C Language" }, { icon: "language-c", name: "C Language" },
@ -93,7 +117,10 @@ export const SITE_METADATA: SiteMetadataType = {
], ],
}, },
{ {
title: "Фреймворки", title: {
ru: "Фреймворки",
en: "Frameworks",
},
list: [ list: [
{ icon: "adonisjs", name: "Adonis.js" }, { icon: "adonisjs", name: "Adonis.js" },
{ icon: "expressjs", name: "Express.js" }, { icon: "expressjs", name: "Express.js" },
@ -104,7 +131,10 @@ export const SITE_METADATA: SiteMetadataType = {
], ],
}, },
{ {
title: "Инструменты", title: {
ru: "Инструменты",
en: "Tools",
},
list: [ list: [
{ icon: "nodejs", name: "Node.js" }, { icon: "nodejs", name: "Node.js" },
{ icon: "docker", name: "Docker" }, { icon: "docker", name: "Docker" },
@ -115,7 +145,10 @@ export const SITE_METADATA: SiteMetadataType = {
], ],
}, },
{ {
title: "Базы данных", title: {
ru: "Базы данных",
en: "Databases",
},
list: [ list: [
{ icon: "postgresql", name: "PostgreSQL" }, { icon: "postgresql", name: "PostgreSQL" },
{ icon: "mongodb", name: "MongoDB" }, { icon: "mongodb", name: "MongoDB" },
@ -123,11 +156,11 @@ export const SITE_METADATA: SiteMetadataType = {
], ],
}, },
{ {
title: "Языки", title: {
list: [ ru: "Языки",
{ icon: undefined, name: "Русский (родной)" }, en: "Languages",
{ icon: undefined, name: "Английский (B1)" }, },
], list: [{ name: "Russian (native)" }, { name: "English (B1)" }],
}, },
], ],
}; };

View File

@ -1,5 +1,18 @@
export default { export default {
"site.title": "yarkov.tech", "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 */
"navigation.articles": "Articles", "navigation.articles": "Articles",
"navigation.projects": "Projects", "navigation.projects": "Projects",
@ -10,7 +23,7 @@ export default {
"splash.subheadline": "Fullstack web-developer & freelancer", "splash.subheadline": "Fullstack web-developer & freelancer",
"splash.description": "splash.description":
"I develop websites and web applications of varying complexity, as well as publish various articles and tutorials on various IT topics.", "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", "splash.contact": "write me",
/* Recent articles */ /* Recent articles */
"recentArticles.heading": "recent publications", "recentArticles.heading": "recent publications",
@ -18,4 +31,20 @@ export default {
/* 404 page */ /* 404 page */
"404.heading": "404", "404.heading": "404",
"404.description": "The page you are trying to find does not exist.", "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 { export default {
"site.title": "yarkov.tech", "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 */
"navigation.articles": "Статьи", "navigation.articles": "Статьи",
"navigation.projects": "Проекты", "navigation.projects": "Проекты",
@ -18,4 +31,20 @@ export default {
/* 404 page */ /* 404 page */
"404.heading": "404", "404.heading": "404",
"404.description": "Страницу которую вы пытаетесь найти не существует.", "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 { SITE_METADATA } from "@root/config";
import PageLayout from '@layouts/default.astro'; import PageLayout from "@layouts/default.astro";
import SkillsList from '@components/SkillsList'; import SkillsList from "@components/SkillsList";
import SkillsBlock from '@components/SkillsBlock'; import SkillsBlock from "@components/SkillsBlock";
// TODO: import content based on user ui language // 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; const { skills } = SITE_METADATA;
--- ---
<PageLayout title="Обо мне"> <PageLayout lang={lang} title={t("pageTitle.about")}>
<section> <section>
<ArticleContent> <ArticleContent>
<Content /> <Content />

View File

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

View File

@ -2,11 +2,28 @@
import PageLayout from "@layouts/default.astro"; import PageLayout from "@layouts/default.astro";
import ContactForm from "@components/ContactForm"; import ContactForm from "@components/ContactForm";
import ContactsList from "@components/ContactsList"; 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> <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 /> <ContactsList />
</section> </section>
</PageLayout> </PageLayout>

View File

@ -3,23 +3,28 @@ import * as utils from "@utils/index";
import PageLayout from "@layouts/default.astro"; import PageLayout from "@layouts/default.astro";
import RecentArticles from "@components/RecentArticles"; import RecentArticles from "@components/RecentArticles";
import { Splash, SplashHeading, SplashLinks, Link } from "@components/Splash"; 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 data = await Astro.glob<ArticleType>("../../content/articles/**/*.mdx");
const articles = utils.transformArticles(data); 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> <Splash>
<SplashHeading> <SplashHeading>
<hgroup> <hgroup>
<h1>Hello, I'am Sergey</h1> <h1><UIString key={"splash.headline"} /></h1>
<h2>Fullstack <b>web-developer</b> & <b>freelancer</b></h2> <h2>Fullstack <b>web-developer</b> & <b>freelancer</b></h2>
</hgroup> </hgroup>
<p>Я занимаюсь разработкой веб-сайтов и веб-приложений разной сложности, а также публикую различные статьи и туториалы на разные IT темы.</p> <p><UIString key={"splash.description"} /></p>
</SplashHeading> </SplashHeading>
<SplashLinks> <SplashLinks>
<Link icon="information" href="/about">обо мне</Link> <Link icon="information" href="/about/"><UIString key={"splash.about"} /></Link>
<Link icon="email_filled" href="/contacts">написать мне</Link> <Link icon="email_filled" href="/contacts/"><UIString key={"splash.contact"} /></Link>
</SplashLinks> </SplashLinks>
</Splash> </Splash>
<RecentArticles articles={articles} length={4} /> <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"> <div class="container">
<h1 style="font-size:var(--font-size-5xl);">404</h1> <h1 style="font-size:var(--font-size-5xl);">404</h1>
<p>Страницу которую вы пытаетесь найти не существует.</p> <p>{t("404.description")}</p>
</div> </div>
</PageLayout> </PageLayout>

View File

@ -1,13 +1,17 @@
--- ---
import { SITE_METADATA } from '@root/config'; import { SITE_METADATA } from "@root/config";
import PageLayout from '@layouts/default.astro'; import PageLayout from "@layouts/default.astro";
import ProjectsList from '@components/ProjectsList'; import ProjectsList from "@components/ProjectsList";
import ProjectItem from '@components/ProjectItem'; 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; const { projects } = SITE_METADATA;
--- ---
<PageLayout title="Проекты" description="Несколько моих основных проектов которые были разработаны и выложены в открытый доступ."> <PageLayout lang={lang} title={t("pageTitle.projects")} description={t("pageDescription.projects")}>
<section> <section>
<ProjectsList> <ProjectsList>
{projects.map((project) => <ProjectItem data={project} />)} {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 {}; export {};
declare global { declare global {
@ -10,28 +12,22 @@ declare global {
onthemetoggled: CustomEvent<ThemeModeType>; onthemetoggled: CustomEvent<ThemeModeType>;
} }
type LanguageKeys = keyof typeof languages;
type UIDictionaryKeys = keyof typeof uiKeys;
type SiteMetadataType = { type SiteMetadataType = {
title: string; title: string;
name: string; name: string;
author: string; author: string;
description: string;
themeColor: string; themeColor: string;
contacts: { contacts: {
email: string; email: string;
githubUrl: string; githubUrl: string;
telegramUrl: string; telegramUrl: string;
}; };
nav: Array<{ name: string; url: string }>; nav: Array<{ key: UIDictionaryKeys; url: string }>;
projects: Array<{ projects: Array<ProjectType>;
icon: string; skills: Array<SkillsType>;
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 }> }>;
}; };
type ArticleType = { type ArticleType = {
@ -48,14 +44,14 @@ declare global {
icon: string; icon: string;
name: string; name: string;
year: string; year: string;
description: string; description: Record<LanguageKeys, string>;
sourceUrl?: string | undefined; sourceUrl?: string | undefined;
articleUrl?: string | undefined; articleUrl?: string | undefined;
demoUrl?: string | undefined; demoUrl?: string | undefined;
}; };
type SkillsType = { type SkillsType = {
title: string; title: Record<LanguageKeys, string>;
list: Array<{ icon?: string | undefined; name: 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) {
timeZone: "UTC", return new Intl.DateTimeFormat(lang, {
month: "short", timeZone: "UTC",
year: "numeric", month: "short",
day: "numeric", year: "numeric",
}); day: "numeric",
});
}

View File

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

View File

@ -1,5 +1,12 @@
import { FALLBACK_LANGUAGE } from "@root/config";
export const encode = (data: object) => { export const encode = (data: object) => {
return Object.keys(data) return Object.keys(data)
.map((key) => encodeURIComponent(key) + "=" + encodeURIComponent(data[key])) .map((key) => encodeURIComponent(key) + "=" + encodeURIComponent(data[key]))
.join("&"); .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/*"], "@layouts/*": ["./src/layouts/*"],
"@utils/*": ["./src/utils/*"], "@utils/*": ["./src/utils/*"],
"@stores/*": ["./src/stores/*"], "@stores/*": ["./src/stores/*"],
"@hooks/*": ["./src/hooks/*"] "@hooks/*": ["./src/hooks/*"],
"@i18n/*": ["./src/i18n/*"]
}, },
"outDir": "./dist", "outDir": "./dist",
"typeRoots": ["./node_modules/@types", "./src/types"] "typeRoots": ["./node_modules/@types", "./src/types"]