add: multilanguage support, Dockerfile for production
This commit is contained in:
parent
8e48e40a31
commit
3c019ba1cf
17
Dockerfile
17
Dockerfile
@ -1,11 +1,16 @@
|
|||||||
FROM node:lts AS node
|
FROM node:lts AS builder
|
||||||
|
ENV NODE_ENV production
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json .
|
|
||||||
RUN npm install --production
|
COPY ./package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM nginx
|
||||||
COPY --from=node /app/build /usr/share/nginx/html
|
COPY --from=builder /app/build /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
EXPOSE 80 443
|
EXPOSE 80 443
|
||||||
|
CMD [ "nginx", "-g", "daemon off;" ]
|
10
compose.yaml
10
compose.yaml
@ -1,11 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
www:
|
personal-website:
|
||||||
build: .
|
build: .
|
||||||
container_name: personal-website
|
container_name: personal-website
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
|
||||||
- cloudflare
|
|
||||||
|
|
||||||
networks:
|
|
||||||
cloudflare: # adapt networks to your configuration
|
|
||||||
external: true
|
|
9
nginx.conf
Normal file
9
nginx.conf
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html/;
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
2
public/icons/countries/pl.svg
Normal file
2
public/icons/countries/pl.svg
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--twemoji" preserveAspectRatio="xMidYMid meet"><path fill="#EEE" d="M32 5H4a4 4 0 0 0-4 4v9h36V9a4 4 0 0 0-4-4z"></path><path fill="#DC143C" d="M0 27a4 4 0 0 0 4 4h28a4 4 0 0 0 4-4v-9H0v9z"></path></svg>
|
After Width: | Height: | Size: 506 B |
23
public/icons/countries/uk.svg
Normal file
23
public/icons/countries/uk.svg
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<path style="fill:#41479B;" d="M473.655,88.276H38.345C17.167,88.276,0,105.443,0,126.621V385.38
|
||||||
|
c0,21.177,17.167,38.345,38.345,38.345h435.31c21.177,0,38.345-17.167,38.345-38.345V126.621
|
||||||
|
C512,105.443,494.833,88.276,473.655,88.276z"/>
|
||||||
|
<path style="fill:#F5F5F5;" d="M511.469,120.282c-3.022-18.159-18.797-32.007-37.814-32.007h-9.977l-163.54,107.147V88.276h-88.276
|
||||||
|
v107.147L48.322,88.276h-9.977c-19.017,0-34.792,13.847-37.814,32.007l139.778,91.58H0v88.276h140.309L0.531,391.717
|
||||||
|
c3.022,18.159,18.797,32.007,37.814,32.007h9.977l163.54-107.147v107.147h88.276V316.577l163.54,107.147h9.977
|
||||||
|
c19.017,0,34.792-13.847,37.814-32.007l-139.778-91.58H512v-88.276H371.691L511.469,120.282z"/>
|
||||||
|
<g>
|
||||||
|
<polygon style="fill:#FF4B55;" points="282.483,88.276 229.517,88.276 229.517,229.517 0,229.517 0,282.483 229.517,282.483
|
||||||
|
229.517,423.724 282.483,423.724 282.483,282.483 512,282.483 512,229.517 282.483,229.517 "/>
|
||||||
|
<path style="fill:#FF4B55;" d="M24.793,421.252l186.583-121.114h-32.428L9.224,410.31
|
||||||
|
C13.377,415.157,18.714,418.955,24.793,421.252z"/>
|
||||||
|
<path style="fill:#FF4B55;" d="M346.388,300.138H313.96l180.716,117.305c5.057-3.321,9.277-7.807,12.287-13.075L346.388,300.138z"
|
||||||
|
/>
|
||||||
|
<path style="fill:#FF4B55;" d="M4.049,109.475l157.73,102.387h32.428L15.475,95.842C10.676,99.414,6.749,104.084,4.049,109.475z"/>
|
||||||
|
<path style="fill:#FF4B55;" d="M332.566,211.862l170.035-110.375c-4.199-4.831-9.578-8.607-15.699-10.86L300.138,211.862H332.566z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
34
public/icons/countries/us.svg
Normal file
34
public/icons/countries/us.svg
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 -4 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_503_3486)">
|
||||||
|
<rect width="28" height="20" rx="2" fill="white"/>
|
||||||
|
<mask id="mask0_503_3486" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="20">
|
||||||
|
<rect width="28" height="20" rx="2" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_503_3486)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M28 0H0V1.33333H28V0ZM28 2.66667H0V4H28V2.66667ZM0 5.33333H28V6.66667H0V5.33333ZM28 8H0V9.33333H28V8ZM0 10.6667H28V12H0V10.6667ZM28 13.3333H0V14.6667H28V13.3333ZM0 16H28V17.3333H0V16ZM28 18.6667H0V20H28V18.6667Z" fill="#D02F44"/>
|
||||||
|
<rect width="12" height="9.33333" fill="#46467F"/>
|
||||||
|
<g filter="url(#filter0_d_503_3486)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.66665 1.99999C2.66665 2.36818 2.36817 2.66666 1.99998 2.66666C1.63179 2.66666 1.33331 2.36818 1.33331 1.99999C1.33331 1.63181 1.63179 1.33333 1.99998 1.33333C2.36817 1.33333 2.66665 1.63181 2.66665 1.99999ZM5.33331 1.99999C5.33331 2.36818 5.03484 2.66666 4.66665 2.66666C4.29846 2.66666 3.99998 2.36818 3.99998 1.99999C3.99998 1.63181 4.29846 1.33333 4.66665 1.33333C5.03484 1.33333 5.33331 1.63181 5.33331 1.99999ZM7.33331 2.66666C7.7015 2.66666 7.99998 2.36818 7.99998 1.99999C7.99998 1.63181 7.7015 1.33333 7.33331 1.33333C6.96512 1.33333 6.66665 1.63181 6.66665 1.99999C6.66665 2.36818 6.96512 2.66666 7.33331 2.66666ZM10.6666 1.99999C10.6666 2.36818 10.3682 2.66666 9.99998 2.66666C9.63179 2.66666 9.33331 2.36818 9.33331 1.99999C9.33331 1.63181 9.63179 1.33333 9.99998 1.33333C10.3682 1.33333 10.6666 1.63181 10.6666 1.99999ZM3.33331 3.99999C3.7015 3.99999 3.99998 3.70152 3.99998 3.33333C3.99998 2.96514 3.7015 2.66666 3.33331 2.66666C2.96512 2.66666 2.66665 2.96514 2.66665 3.33333C2.66665 3.70152 2.96512 3.99999 3.33331 3.99999ZM6.66665 3.33333C6.66665 3.70152 6.36817 3.99999 5.99998 3.99999C5.63179 3.99999 5.33331 3.70152 5.33331 3.33333C5.33331 2.96514 5.63179 2.66666 5.99998 2.66666C6.36817 2.66666 6.66665 2.96514 6.66665 3.33333ZM8.66665 3.99999C9.03484 3.99999 9.33331 3.70152 9.33331 3.33333C9.33331 2.96514 9.03484 2.66666 8.66665 2.66666C8.29846 2.66666 7.99998 2.96514 7.99998 3.33333C7.99998 3.70152 8.29846 3.99999 8.66665 3.99999ZM10.6666 4.66666C10.6666 5.03485 10.3682 5.33333 9.99998 5.33333C9.63179 5.33333 9.33331 5.03485 9.33331 4.66666C9.33331 4.29847 9.63179 3.99999 9.99998 3.99999C10.3682 3.99999 10.6666 4.29847 10.6666 4.66666ZM7.33331 5.33333C7.7015 5.33333 7.99998 5.03485 7.99998 4.66666C7.99998 4.29847 7.7015 3.99999 7.33331 3.99999C6.96512 3.99999 6.66665 4.29847 6.66665 4.66666C6.66665 5.03485 6.96512 5.33333 7.33331 5.33333ZM5.33331 4.66666C5.33331 5.03485 5.03484 5.33333 4.66665 5.33333C4.29846 5.33333 3.99998 5.03485 3.99998 4.66666C3.99998 4.29847 4.29846 3.99999 4.66665 3.99999C5.03484 3.99999 5.33331 4.29847 5.33331 4.66666ZM1.99998 5.33333C2.36817 5.33333 2.66665 5.03485 2.66665 4.66666C2.66665 4.29847 2.36817 3.99999 1.99998 3.99999C1.63179 3.99999 1.33331 4.29847 1.33331 4.66666C1.33331 5.03485 1.63179 5.33333 1.99998 5.33333ZM3.99998 5.99999C3.99998 6.36819 3.7015 6.66666 3.33331 6.66666C2.96512 6.66666 2.66665 6.36819 2.66665 5.99999C2.66665 5.6318 2.96512 5.33333 3.33331 5.33333C3.7015 5.33333 3.99998 5.6318 3.99998 5.99999ZM5.99998 6.66666C6.36817 6.66666 6.66665 6.36819 6.66665 5.99999C6.66665 5.6318 6.36817 5.33333 5.99998 5.33333C5.63179 5.33333 5.33331 5.6318 5.33331 5.99999C5.33331 6.36819 5.63179 6.66666 5.99998 6.66666ZM9.33331 5.99999C9.33331 6.36819 9.03484 6.66666 8.66665 6.66666C8.29846 6.66666 7.99998 6.36819 7.99998 5.99999C7.99998 5.6318 8.29846 5.33333 8.66665 5.33333C9.03484 5.33333 9.33331 5.6318 9.33331 5.99999ZM9.99998 8C10.3682 8 10.6666 7.70152 10.6666 7.33333C10.6666 6.96514 10.3682 6.66666 9.99998 6.66666C9.63179 6.66666 9.33331 6.96514 9.33331 7.33333C9.33331 7.70152 9.63179 8 9.99998 8ZM7.99998 7.33333C7.99998 7.70152 7.7015 8 7.33331 8C6.96512 8 6.66665 7.70152 6.66665 7.33333C6.66665 6.96514 6.96512 6.66666 7.33331 6.66666C7.7015 6.66666 7.99998 6.96514 7.99998 7.33333ZM4.66665 8C5.03484 8 5.33331 7.70152 5.33331 7.33333C5.33331 6.96514 5.03484 6.66666 4.66665 6.66666C4.29846 6.66666 3.99998 6.96514 3.99998 7.33333C3.99998 7.70152 4.29846 8 4.66665 8ZM2.66665 7.33333C2.66665 7.70152 2.36817 8 1.99998 8C1.63179 8 1.33331 7.70152 1.33331 7.33333C1.33331 6.96514 1.63179 6.66666 1.99998 6.66666C2.36817 6.66666 2.66665 6.96514 2.66665 7.33333Z" fill="url(#paint0_linear_503_3486)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_503_3486" x="1.33331" y="1.33333" width="9.33331" height="7.66667" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_503_3486"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_503_3486" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="paint0_linear_503_3486" x1="1.33331" y1="1.33333" x2="1.33331" y2="7.99999" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="#F0F0F0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_503_3486">
|
||||||
|
<rect width="28" height="20" rx="2" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.5 KiB |
@ -18,6 +18,7 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
min-width: 75%;
|
||||||
transition: 0.2s background-color ease-in-out;
|
transition: 0.2s background-color ease-in-out;
|
||||||
background-color: ${({ theme }) => theme.primary};
|
background-color: ${({ theme }) => theme.primary};
|
||||||
color: ${({ theme }) => theme.text};
|
color: ${({ theme }) => theme.text};
|
||||||
|
@ -7,6 +7,7 @@ export type Props = {
|
|||||||
link?: string,
|
link?: string,
|
||||||
primary?: boolean,
|
primary?: boolean,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
|
noeffects?: boolean
|
||||||
} & React.ComponentProps<'div'>;
|
} & React.ComponentProps<'div'>;
|
||||||
|
|
||||||
const Button: React.FC<Props> = ({
|
const Button: React.FC<Props> = ({
|
||||||
@ -15,10 +16,16 @@ const Button: React.FC<Props> = ({
|
|||||||
link,
|
link,
|
||||||
primary,
|
primary,
|
||||||
children,
|
children,
|
||||||
|
noeffects = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const item = <ButtonStyled {...props} className={classes({ link, primary, disabled })}>
|
const item = <ButtonStyled {...props} className={classes({
|
||||||
|
link: link || typeof props.onClick === 'function',
|
||||||
|
primary,
|
||||||
|
disabled,
|
||||||
|
noeffects
|
||||||
|
})}>
|
||||||
{icon && <img src={icon} alt="Button icon" />}
|
{icon && <img src={icon} alt="Button icon" />}
|
||||||
{content || children}
|
{content || children}
|
||||||
</ButtonStyled>;
|
</ButtonStyled>;
|
||||||
@ -46,7 +53,7 @@ const ButtonStyled = styled.div`
|
|||||||
${({ theme }) => theme.type === 'dark' && 'filter: invert();'}
|
${({ theme }) => theme.type === 'dark' && 'filter: invert();'}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover, &.primary {
|
&:not(.noeffects):hover, &.primary {
|
||||||
background: ${({ theme }) => theme.text};
|
background: ${({ theme }) => theme.text};
|
||||||
color: ${({ theme }) => theme.secondary};
|
color: ${({ theme }) => theme.secondary};
|
||||||
box-shadow: 0 0 5px ${({ theme }) => theme.text};
|
box-shadow: 0 0 5px ${({ theme }) => theme.text};
|
||||||
@ -56,7 +63,7 @@ const ButtonStyled = styled.div`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.primary:hover {
|
&:not(.noeffects).primary:hover {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: ${({ theme }) => theme.text};
|
color: ${({ theme }) => theme.text};
|
||||||
|
|
||||||
|
@ -3,13 +3,17 @@ import React from "react";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
import { ThemeProvider } from "./style/themes";
|
import { ThemeProvider } from "./style/themes";
|
||||||
|
import { LanguageProvider } from "./language/context";
|
||||||
|
|
||||||
import "./style/index.css";
|
import "./style/index.css";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root")!!);
|
const root = ReactDOM.createRoot(document.getElementById("root")!!);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider>
|
<LanguageProvider>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
</ThemeProvider>
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
</LanguageProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
50
src/language/context.tsx
Normal file
50
src/language/context.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import LanguageManager, { Language } from './manager';
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
type LanguageContextProps = {
|
||||||
|
language: Language;
|
||||||
|
$: LanguageManager;
|
||||||
|
setLanguage: (lang: Language) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LanguageContext = createContext<LanguageContextProps>({
|
||||||
|
language: 'en-US',
|
||||||
|
setLanguage: () => {},
|
||||||
|
$: new LanguageManager('en-US'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type LanguageProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LanguageProvider: React.FC<LanguageProviderProps> = ({ children }) => {
|
||||||
|
const [language, setLanguageState] = useState<Language>(() => {
|
||||||
|
const saved = localStorage.getItem('language');
|
||||||
|
return (saved as Language) || 'en-US';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('language', language);
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
const setLanguage = (lang: Language) => setLanguageState(lang);
|
||||||
|
|
||||||
|
const $ = useMemo(() =>
|
||||||
|
new LanguageManager(language),
|
||||||
|
[language]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <LanguageContext.Provider value={{ language, setLanguage, $ }}>
|
||||||
|
{children}
|
||||||
|
</LanguageContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useLanguage = () => useContext(LanguageContext);
|
||||||
|
|
||||||
|
export { LanguageProvider, useLanguage };
|
50
src/language/en-GB.json
Normal file
50
src/language/en-GB.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"hello": "Hello",
|
||||||
|
"about": "My name is Franek. I'm {%years%} years, {%months%} months and {%days%} days old.",
|
||||||
|
"switch_theme": "Switch theme to {%theme%}",
|
||||||
|
"broad_music_spectrum": "Yeah, I know, I listen to a broad spectrum of music",
|
||||||
|
"headers": {
|
||||||
|
"facts": "Some facts about me",
|
||||||
|
"skills": "I can...",
|
||||||
|
"music": "My favourite music genres are:",
|
||||||
|
"bands": "I like these bands/singers:"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"play the keyboard/organ",
|
||||||
|
"tinker with PCs from software to hardware",
|
||||||
|
"code various apps and scripts"
|
||||||
|
],
|
||||||
|
"about_me": [
|
||||||
|
"introvert",
|
||||||
|
"IT enthusiast",
|
||||||
|
"automotive enthusiast"
|
||||||
|
],
|
||||||
|
"section": {
|
||||||
|
"fediverse": "Fediverse...",
|
||||||
|
"contact": "Contact methods...",
|
||||||
|
"links": "Some links..."
|
||||||
|
},
|
||||||
|
"fediverse": {
|
||||||
|
"pleroma": "Pleroma",
|
||||||
|
"pixelfed": "Pixelfed",
|
||||||
|
"peertube": "PeerTube"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"matrix": "Matrix",
|
||||||
|
"signal": "@sadorowo.66"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"pgp": "PGP",
|
||||||
|
"uptime": "Uptime of my services",
|
||||||
|
"email": "E-mail",
|
||||||
|
"gitea": "Gitea",
|
||||||
|
"instagram": "Instagram (deactivated)"
|
||||||
|
},
|
||||||
|
"most_used_languages": "Languages that I use the most:",
|
||||||
|
"favourite_tools": "My favourite tools...",
|
||||||
|
"languages": {
|
||||||
|
"pl": "Polish",
|
||||||
|
"en-US": "United States",
|
||||||
|
"en-GB": "United Kingdom"
|
||||||
|
}
|
||||||
|
}
|
50
src/language/en-US.json
Normal file
50
src/language/en-US.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"hello": "Hello",
|
||||||
|
"about": "My name is Franek. I'm {%years%} years, {%months%} months and {%days%} days old.",
|
||||||
|
"switch_theme": "Switch theme to {%theme%}",
|
||||||
|
"broad_music_spectrum": "Yeah I know, I listen to a broad spectrum of music",
|
||||||
|
"headers": {
|
||||||
|
"facts": "Some facts about me",
|
||||||
|
"skills": "I can...",
|
||||||
|
"music": "My favourite music genres are:",
|
||||||
|
"bands": "I like these bands/singers:"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"play keyboard/organ",
|
||||||
|
"mess with PCs from software to hardware",
|
||||||
|
"code various apps and scripts"
|
||||||
|
],
|
||||||
|
"about_me": [
|
||||||
|
"introvert",
|
||||||
|
"IT enthusiast",
|
||||||
|
"automotive enthusiast"
|
||||||
|
],
|
||||||
|
"section": {
|
||||||
|
"fediverse": "Fediverse...",
|
||||||
|
"contact": "Contact ways...",
|
||||||
|
"links": "Some links..."
|
||||||
|
},
|
||||||
|
"fediverse": {
|
||||||
|
"pleroma": "Pleroma",
|
||||||
|
"pixelfed": "Pixelfed",
|
||||||
|
"peertube": "PeerTube"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"matrix": "Matrix",
|
||||||
|
"signal": "@sadorowo.66"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"pgp": "PGP",
|
||||||
|
"uptime": "Uptime of my services",
|
||||||
|
"email": "E-mail",
|
||||||
|
"gitea": "Gitea",
|
||||||
|
"instagram": "Instagram (deactivated)"
|
||||||
|
},
|
||||||
|
"most_used_languages": "Languages that I use the most:",
|
||||||
|
"favourite_tools": "My favourite tools...",
|
||||||
|
"languages": {
|
||||||
|
"pl": "Polski",
|
||||||
|
"en-US": "United States",
|
||||||
|
"en-GB": "United Kingdom"
|
||||||
|
}
|
||||||
|
}
|
56
src/language/manager.ts
Normal file
56
src/language/manager.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import enUS from './en-US.json';
|
||||||
|
import plPL from './pl-PL.json';
|
||||||
|
import enGB from './en-GB.json';
|
||||||
|
|
||||||
|
export type Language = 'pl-PL' | 'en-GB' | 'en-US';
|
||||||
|
type ToStringable = { toString(): string };
|
||||||
|
|
||||||
|
interface Translations {
|
||||||
|
[key: string]: ToStringable | Translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_TRANSLATIONS: Record<Language, Translations> = {
|
||||||
|
'en-US': enUS,
|
||||||
|
'pl-PL': plPL,
|
||||||
|
'en-GB': enGB,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class LanguageManager {
|
||||||
|
private translations: Translations = {};
|
||||||
|
public language: Language;
|
||||||
|
|
||||||
|
private readonly supportedLanguages: Language[] = ['pl-PL', 'en-GB', 'en-US'];
|
||||||
|
private readonly fallbackLanguage: Language = 'en-US';
|
||||||
|
|
||||||
|
public constructor(language: string) {
|
||||||
|
this.language = this.isSupported(language) ? (language as Language) : this.fallbackLanguage;
|
||||||
|
this.translations = ALL_TRANSLATIONS[this.language];
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSupported(lang: string): lang is Language {
|
||||||
|
return this.supportedLanguages.includes(lang as Language);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNestedValue(obj: any, path: string): unknown {
|
||||||
|
return path
|
||||||
|
.split('.')
|
||||||
|
.reduce((acc, key) => (acc && key in acc ? acc[key] : undefined), obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public raw(key: string): unknown {
|
||||||
|
return this.getNestedValue(this.translations, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public tr(key: string, map?: Record<string, ToStringable>): string {
|
||||||
|
const raw = this.raw(key);
|
||||||
|
let value = (raw as ToStringable)?.toString?.() ?? `N/A`;
|
||||||
|
|
||||||
|
if (map) {
|
||||||
|
for (const [placeholder, replacement] of Object.entries(map)) {
|
||||||
|
value = value.replace(new RegExp(`{%${placeholder}%}`, 'g'), replacement.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
50
src/language/pl-PL.json
Normal file
50
src/language/pl-PL.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"hello": "Cześć",
|
||||||
|
"about": "Nazywam się Franek. Mam {%years%} lat, {%months%} miesięcy i {%days%} dni.",
|
||||||
|
"switch_theme": "Zmień motyw na {%theme%}",
|
||||||
|
"broad_music_spectrum": "Tak, zdaję sobie sprawę z tego, że słucham szerokiego spektrum muzyki",
|
||||||
|
"headers": {
|
||||||
|
"facts": "Kilka faktów o mnie",
|
||||||
|
"skills": "Potrafię...",
|
||||||
|
"music": "Moje ulubione gatunki muzyczne to:",
|
||||||
|
"bands": "Lubię tych wykonawców:"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"grać na pianinie/organach",
|
||||||
|
"majstrować przy komputerach — od oprogramowania po sprzęt",
|
||||||
|
"tworzyć różne aplikacje i skrypty"
|
||||||
|
],
|
||||||
|
"about_me": [
|
||||||
|
"introwertyk",
|
||||||
|
"entuzjasta IT",
|
||||||
|
"pasjonat motoryzacji"
|
||||||
|
],
|
||||||
|
"section": {
|
||||||
|
"fediverse": "Fediverse...",
|
||||||
|
"contact": "Skontaktuj się ze mną...",
|
||||||
|
"links": "Kilka linków..."
|
||||||
|
},
|
||||||
|
"fediverse": {
|
||||||
|
"pleroma": "Pleroma",
|
||||||
|
"pixelfed": "Pixelfed",
|
||||||
|
"peertube": "PeerTube"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"matrix": "Matrix",
|
||||||
|
"signal": "@sadorowo.66"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"pgp": "PGP",
|
||||||
|
"uptime": "Uptime moich usług",
|
||||||
|
"email": "E-mail",
|
||||||
|
"gitea": "Gitea",
|
||||||
|
"instagram": "Instagram (dezaktywowany)"
|
||||||
|
},
|
||||||
|
"most_used_languages": "Języki, których najczęściej używam:",
|
||||||
|
"favourite_tools": "Świetne narzędzia...",
|
||||||
|
"languages": {
|
||||||
|
"pl": "Polski",
|
||||||
|
"en-US": "Stany Zjednoczone",
|
||||||
|
"en-GB": "Wielka Brytania"
|
||||||
|
}
|
||||||
|
}
|
@ -1,40 +1,53 @@
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { useTheme } from "../style/themes";
|
|
||||||
import { calculateAge } from "../utils/math";
|
|
||||||
|
|
||||||
|
import { useTheme } from "../style/themes";
|
||||||
|
import { useLanguage } from "../language/context";
|
||||||
|
|
||||||
|
import { calculateAge } from "../utils/math";
|
||||||
import { asCardStack } from "../components/card-stack";
|
import { asCardStack } from "../components/card-stack";
|
||||||
|
|
||||||
const About: React.FC = () => {
|
const About: React.FC = () => {
|
||||||
const { years, months, days } = calculateAge(new Date(2007, 6, 25));
|
const age = calculateAge(new Date(2007, 6, 25));
|
||||||
const { theme, toggleTheme } = useTheme();
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
const { $, setLanguage } = useLanguage();
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Heading>Hello.</Heading>
|
<Heading>{$.tr("hello")}.</Heading>
|
||||||
<p>My name is Franek. I'm {years} years, {months} months and {days} days old.</p>
|
<p>{$.tr("about", age)}</p>
|
||||||
{asCardStack([
|
{asCardStack([
|
||||||
{
|
{
|
||||||
content: `switch theme to ${theme.type === "light" ? "dark" : "light"}`,
|
content: $.tr("switch_theme", { theme: theme.type === "light" ? "dark" : "light" }),
|
||||||
icon: "/icons/" + (theme.type === "light" ? "moon" : "sun") + ".svg",
|
icon: "/icons/" + (theme.type === "light" ? "moon" : "sun") + ".svg",
|
||||||
primary: theme.type === "light",
|
primary: theme.type === "light",
|
||||||
onClick: toggleTheme
|
onClick: toggleTheme
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: $.tr("languages.pl"),
|
||||||
|
icon: "/icons/countries/pl.svg",
|
||||||
|
noeffects: true,
|
||||||
|
onClick: () => setLanguage('pl-PL')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: $.tr("languages.en-US"),
|
||||||
|
icon: "/icons/countries/us.svg",
|
||||||
|
noeffects: true,
|
||||||
|
onClick: () => setLanguage('en-US')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: $.tr("languages.en-GB"),
|
||||||
|
icon: "/icons/countries/uk.svg",
|
||||||
|
noeffects: true,
|
||||||
|
onClick: () => setLanguage('en-GB')
|
||||||
}
|
}
|
||||||
])}
|
])}
|
||||||
|
|
||||||
<h2>Some facts about me</h2>
|
<h2>{$.tr("headers.facts")}</h2>
|
||||||
{asCardStack([
|
{asCardStack($.raw("about_me") as string[])}
|
||||||
"introvert",
|
|
||||||
"IT enthusiast",
|
|
||||||
"automotive enthusiast"
|
|
||||||
])}
|
|
||||||
|
|
||||||
<h2>I can...</h2>
|
<h2>{$.tr("headers.skills")}</h2>
|
||||||
{asCardStack([
|
{asCardStack($.raw("skills") as string[])}
|
||||||
"play keyboard/organ",
|
|
||||||
"mess with PCs from software to hardware",
|
|
||||||
"code various apps and scripts"
|
|
||||||
])}
|
|
||||||
|
|
||||||
<h2>My favourite music genres are:</h2>
|
<h2>{$.tr("headers.music")}</h2>
|
||||||
{asCardStack([
|
{asCardStack([
|
||||||
"black metal",
|
"black metal",
|
||||||
"doom metal",
|
"doom metal",
|
||||||
@ -43,7 +56,8 @@ const About: React.FC = () => {
|
|||||||
"hard rock"
|
"hard rock"
|
||||||
])}
|
])}
|
||||||
|
|
||||||
<h2>I like these bands/singers:</h2>
|
<h2>{$.tr("headers.bands")}</h2>
|
||||||
|
<p><i>{$.tr("broad_music_spectrum")}</i></p>
|
||||||
{asCardStack([
|
{asCardStack([
|
||||||
"Mgła",
|
"Mgła",
|
||||||
"Behemoth",
|
"Behemoth",
|
||||||
@ -60,7 +74,6 @@ const About: React.FC = () => {
|
|||||||
"ABBA",
|
"ABBA",
|
||||||
"Evanescence"
|
"Evanescence"
|
||||||
])}
|
])}
|
||||||
<p>yeah I know, I listen to a broad spectrum of music</p>
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,81 +1,80 @@
|
|||||||
import styled from "styled-components";
|
|
||||||
import { asCardStack } from "../components/card-stack";
|
import { asCardStack } from "../components/card-stack";
|
||||||
|
import { useLanguage } from "../language/context";
|
||||||
|
import LanguageManager from "../language/manager";
|
||||||
|
|
||||||
const Links: React.FC = () => {
|
const Links: React.FC = () => {
|
||||||
|
const { $ } = useLanguage();
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<h2>Fediverse...</h2>
|
<h2>{$.tr("section.fediverse")}</h2>
|
||||||
{asCardStack(FEDIVERSE_LINKS)}
|
{asCardStack(FEDIVERSE_LINKS($))}
|
||||||
|
|
||||||
<h2>Contact ways...</h2>
|
<h2>{$.tr("section.contact")}</h2>
|
||||||
{asCardStack(CONTACT_WAYS)}
|
{asCardStack(CONTACT_WAYS($))}
|
||||||
|
|
||||||
<h2>Some links...</h2>
|
<h2>{$.tr("section.links")}</h2>
|
||||||
{asCardStack(LINKS)}
|
{asCardStack(LINKS($))}
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Heading = styled.h1`
|
const FEDIVERSE_LINKS = ($: LanguageManager) => [
|
||||||
font-family: "Pacifico", serif;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FEDIVERSE_LINKS = [
|
|
||||||
{
|
{
|
||||||
content: "Pleroma",
|
content: $.tr("fediverse.pleroma"),
|
||||||
icon: "/icons/fediverse/pleroma.svg",
|
icon: "/icons/fediverse/pleroma.svg",
|
||||||
link: "https://social.sador.me/@boss"
|
link: "https://social.sador.me/@boss"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "Pixelfed",
|
content: $.tr("fediverse.pixelfed"),
|
||||||
icon: "/icons/fediverse/pixelfed.svg",
|
icon: "/icons/fediverse/pixelfed.svg",
|
||||||
link: "https://pix.sador.me/boss"
|
link: "https://pix.sador.me/boss"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "PeerTube",
|
content: $.tr("fediverse.peertube"),
|
||||||
icon: "/icons/fediverse/peertube.svg",
|
icon: "/icons/fediverse/peertube.svg",
|
||||||
link: "https://tube.sador.me/c/sador"
|
link: "https://tube.sador.me/c/sador"
|
||||||
}
|
}
|
||||||
].map(link => Object.assign(link, { primary: true }));
|
].map(link => Object.assign(link, { primary: true }));
|
||||||
|
|
||||||
const CONTACT_WAYS = [
|
const CONTACT_WAYS = ($: LanguageManager) => [
|
||||||
{
|
{
|
||||||
content: "Matrix",
|
content: $.tr("contact.matrix"),
|
||||||
icon: "/icons/matrix.svg",
|
icon: "/icons/matrix.svg",
|
||||||
link: "https://matrix.to/#/@boss:sador.me"
|
link: "https://matrix.to/#/@boss:sador.me"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "@sadorowo.66",
|
content: $.tr("contact.signal"),
|
||||||
icon: "/icons/signal.svg"
|
icon: "/icons/signal.svg"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const LINKS = [
|
const LINKS = ($: LanguageManager) => [
|
||||||
{
|
{
|
||||||
content: "PGP",
|
content: $.tr("link.pgp"),
|
||||||
icon: "/icons/pgp.svg",
|
icon: "/icons/pgp.svg",
|
||||||
link: "/pgp.asc",
|
link: "/pgp.asc",
|
||||||
primary: true
|
primary: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "Uptime of my services",
|
content: $.tr("link.uptime"),
|
||||||
icon: "/icons/uptime.svg",
|
icon: "/icons/uptime.svg",
|
||||||
link: "https://health.sador.me/status/aio"
|
link: "https://health.sador.me/status/aio"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "E-mail",
|
content: $.tr("link.email"),
|
||||||
icon: "/icons/email.svg",
|
icon: "/icons/email.svg",
|
||||||
link: "mailto:contact@sador.me?subject=[sador.me] ..."
|
link: "mailto:contact@sador.me?subject=[sador.me] ..."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "Gitea",
|
content: $.tr("link.gitea"),
|
||||||
icon: "/icons/gitea.svg",
|
icon: "/icons/gitea.svg",
|
||||||
link: "https://git.sador.me"
|
link: "https://git.sador.me"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "Instagram (deactivated)",
|
content: $.tr("link.instagram"),
|
||||||
icon: "/icons/instagram.svg",
|
icon: "/icons/instagram.svg",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
link: "https://instagram.com/sadorowo"
|
link: "https://instagram.com/sadorowo"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default Links;
|
export default Links;
|
@ -1,11 +1,14 @@
|
|||||||
import { asCardStack } from "../components/card-stack";
|
import { asCardStack } from "../components/card-stack";
|
||||||
|
import { useLanguage } from "../language/context";
|
||||||
|
|
||||||
const Tools: React.FC = () => {
|
const Tools: React.FC = () => {
|
||||||
|
const { $ } = useLanguage();
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<h2>Languages that I use the most:</h2>
|
<h2>{$.tr("most_used_languages")}</h2>
|
||||||
{asCardStack(LANGUAGES)}
|
{asCardStack(LANGUAGES)}
|
||||||
|
|
||||||
<h2>My favourite tools...</h2>
|
<h2>{$.tr("favourite_tools")}</h2>
|
||||||
{asCardStack(TOOLS)}
|
{asCardStack(TOOLS)}
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
@ -12,4 +12,8 @@ body {
|
|||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
@ -5,6 +5,7 @@
|
|||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user