Merge branch 'i18n' into 'master'

i18n

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!58
This commit is contained in:
Aarni Halinen
2021-04-04 20:13:34 +00:00
16 changed files with 402 additions and 117 deletions
+14 -28
View File
@@ -7,7 +7,7 @@ import breakpoints from "@theme/breakpoints";
interface WrappedCardProps {
title: string;
start_time: string;
startTime: string;
text: string;
link: string;
image?: {
@@ -15,6 +15,7 @@ interface WrappedCardProps {
alt: string;
};
buttonOnClick?: () => void;
buttonText?: string;
}
const StyledCard = styled.article`
@@ -69,36 +70,21 @@ const StyledCard = styled.article`
`;
const WrappedCard: React.FC<WrappedCardProps> = ({
title, text, link, image, start_time, buttonOnClick, ...props
}) => {
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
const datetime = new Date(start_time).toLocaleString("fi-FI", options);
const button = (
title, text, link, image, startTime, buttonOnClick, buttonText, ...props
}) => (
<StyledCard {...props}>
{image && (
<Image src={image.src} alt={image.alt} layout="responsive" width={0} height={0} objectFit="scale-down" />
)}
<p>{startTime}</p>
<h3>{title}</h3>
<p>{text}</p>
<Link to={link}>
<button type="button" onClick={buttonOnClick}>
Lue lisää&nbsp;
{buttonText}
</button>
</Link>
);
return (
<StyledCard {...props}>
{image && (
<Image src={image.src} alt={image.alt} layout="responsive" width={0} height={0} objectFit="scale-down" />
)}
<p>{datetime}</p>
<h3>{title}</h3>
<p>{text}</p>
{button}
</StyledCard>
);
};
</StyledCard>
);
export default WrappedCard;
+27
View File
@@ -0,0 +1,27 @@
import React from "react";
import styled from "styled-components";
import { useTranslation } from "../i18n";
import Icon, { IconType } from "./Icon";
const ChangeLanguageButton: React.FC = (props) => {
const { i18n } = useTranslation();
const { language, changeLanguage } = i18n;
return (
<button
{...props}
type="button"
onClick={() => {
changeLanguage(language === "fi" ? "en" : "fi");
}}
>
<Icon name={language === "fi" ? IconType.GBFlag : IconType.FinlandFlag} />
</button>
);
};
export default styled(ChangeLanguageButton)`
font-size: 4rem;
background: none;
border: none;
width: fit-content;
`;
+10 -2
View File
@@ -69,10 +69,18 @@ const nameToIcon = (name: IconType): JSX.Element | string => {
);
}
if (name === IconType.FinlandFlag) {
return "🇫🇮";
return (
<span role="img">
🇫🇮
</span>
);
}
if (name === IconType.GBFlag) {
return "🇬🇧";
return (
<span role="img">
🇬🇧
</span>
);
}
return null;
};
+1
View File
@@ -10,3 +10,4 @@ export { default as InfoBox } from "./InfoBox";
export { default as Accordion } from "./Accordion/Accordion";
export { default as Link } from "./Link";
export { default as CrossFadeImages } from "./CrossFadeImages";
export { default as ChangeLanguageButton } from "./ChangeLanguageButton";
+98
View File
@@ -0,0 +1,98 @@
import React, {
createContext, useContext, useReducer,
} from "react";
import fi from "./locales/fi/common.json";
import en from "./locales/en/common.json";
type Lang = "fi" | "en";
const LOCAL_STORAGE_KEY = "locale";
type TranslateFunc = (key: string) => string;
const translateEn: TranslateFunc = (key) => {
const res = en[key];
if (!res) {
console.warn(`Locale 'en' has no key: ${key}!`);
}
return res || key;
};
const translateFi: TranslateFunc = (key) => {
const res = fi[key];
if (!res) {
// Silence warnings for Finnish
// console.warn(`Locale 'en' has no key: ${key}!`);
}
return res || key;
};
interface Store {
language: Lang;
changeLanguage: React.Dispatch<Lang>,
}
let initialLanguage: Lang = "fi";
try {
const storedLang = localStorage.getItem(LOCAL_STORAGE_KEY) as Lang;
initialLanguage = storedLang;
} catch (err) {
// Just ignore if fails to get value from browser (server etc.)
}
const initialState: Store = {
language: initialLanguage,
changeLanguage: null,
};
const Reducer = (state: Store, action: Lang) => {
switch (action) {
case "fi":
return {
...state,
language: action,
};
case "en":
return {
...state,
language: action,
};
default:
return state;
}
};
const LocaleContext = createContext(initialState);
const LocaleStore: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
const changeLanguage = (action: Lang) => {
dispatch(action);
try {
localStorage.setItem(LOCAL_STORAGE_KEY, action);
} catch (err) {
// Just ignore if fails to store value in user's browser
}
};
return (
<LocaleContext.Provider value={{ ...state, changeLanguage }}>
{children}
</LocaleContext.Provider>
);
};
export default LocaleStore;
const useTranslation = () => {
const { language, changeLanguage } = useContext(LocaleContext);
const t = language === "en" ? translateEn : translateFi;
return {
t,
i18n: {
language,
changeLanguage,
},
};
};
export { useTranslation };
+51
View File
@@ -0,0 +1,51 @@
{
"Lue lisää": "Read more",
"lngButton": "Suomeksi",
"Paikka": "Location",
"Alkaa": "Starts at",
"Päättyy": "Ends at",
"Lataa lisää": "Load more",
"Tapahtumat": "Events",
"Uutiset": "News",
"Hakemaasi sivua":
"Page",
"ei löydy":
"does not exist",
"Hups, tapahtui virhe": "Oops, an error occured",
"Lue lisää täältä": "More information here",
"Tärkeä tiedote!": "Important notice!",
"Katso kaikki tapahtumat":
"All events",
"Ilmoittaudu":
"Sign-up",
"Ilmoittautuminen":
"Sign-up",
"Peruuta":
"Cancel",
"Kokonaishinta":
"Total price",
"Ilmoittautuminen ei ole vielä auki!":
"Signup is not open yet!",
"Se aukeaa":
"Signup opens at",
"Ilmoittautumalla hyväksyn":
"By signing up I accept the",
"tietosuojaselosteen":
"privacy policy",
"ja tietojeni tallentamisen.":
"and storing of my data."
}
+3
View File
@@ -0,0 +1,3 @@
{
"lngButton": "In English"
}
+6 -3
View File
@@ -5,6 +5,7 @@ import Head from "next/head";
import styled, { createGlobalStyle } from "styled-components";
import { ToastContainer } from "react-toastify";
import { colors } from "@theme/colors";
import LocaleStore from "../i18n";
import "react-mde/lib/styles/css/react-mde-all.css";
import "react-toastify/dist/ReactToastify.css";
@@ -140,9 +141,11 @@ const Web20App = ({ Component, pageProps }: AppProps) => (
<meta name="keywords" content="SIK AYY" />
</Head>
<GlobalCommonStyles />
<AppContainer>
<Component {...pageProps} />
</AppContainer>
<LocaleStore>
<AppContainer>
<Component {...pageProps} />
</AppContainer>
</LocaleStore>
<ToastContainer position="bottom-right" />
</>
);
+7 -3
View File
@@ -5,7 +5,7 @@ import Event from "@models/Event";
import Post from "@models/Feed";
import {
Divider, CTASection, TextSection, Link, CrossFadeImages,
Divider, CTASection, TextSection, Link, CrossFadeImages, ChangeLanguageButton,
} from "@components/index";
import ActualPageHero from "./ActualPageHero";
import EventCalendar from "./EventCalendar";
@@ -32,14 +32,18 @@ const Gallery = styled.div`
}
`;
const LngButton = styled(ChangeLanguageButton)`
align-self: flex-end;
margin-right: 1rem;
`;
const ActualPageView: React.FC<ActualPageViewProps> = ({ events, feed }) => (
<>
<ActualPageHero />
<LngButton />
<EventCalendar events={events} />
<Divider />
<News feed={feed} />
<CTASection
+33 -9
View File
@@ -1,9 +1,11 @@
import React, { useState } from "react";
import Event from "@models/Event";
import Button from "@components/Button";
import { CardSection, Card, FullWidthSection } from "@components/index";
import Event from "@models/Event";
import {
Button, CardSection, Card, FullWidthSection,
} from "@components/index";
import noop from "@utils/noop";
import { useTranslation } from "../../i18n";
import FilterContainer from "./FilterContainer";
interface EventCalendarProps {
@@ -13,11 +15,32 @@ interface EventCalendarProps {
const EventCalendar: React.FC<EventCalendarProps> = ({ events }) => {
// const [filterSelected, setFilter] = useState(0);
const [numberShown, setNumberShown] = useState(8);
const filteredEvents = events.slice(0, numberShown);
const { t, i18n } = useTranslation();
const isFi = i18n.language === "fi";
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
const filteredEvents = events.slice(0, numberShown).map((e) => ({
...e,
title: isFi ? e.title_fi : e.title_en,
description: isFi ? e.description_fi : e.description_en,
content: isFi ? e.content_fi : e.content_en,
location: isFi ? e.location_fi : e.location_en,
startDate: new Date(e.start_time).toLocaleString(isFi ? "fi-FI" : "en-GB", options),
endDate: new Date(e.end_time).toLocaleString(isFi ? "fi-FI" : "en-GB", options),
}));
return (
<FullWidthSection>
<h1 id="tapahtumat">
Tapahtumat
{t("Tapahtumat")}
{/* <FilterContainer>
<Button buttonStyle="filter" onClick={() => { setFilter(0); }} selected={filterSelected === 0}>
Näytä kaikki
@@ -35,18 +58,19 @@ const EventCalendar: React.FC<EventCalendarProps> = ({ events }) => {
{filteredEvents.map((e) => (
<Card
key={e.id}
title={e.title_fi}
start_time={e.start_time}
text={e.description_fi}
title={e.title}
startTime={e.startDate}
text={e.description}
link={`/events/${e.id}`}
buttonOnClick={noop}
buttonText={`${t("Lue lisää")} `}
/>
))}
</CardSection>
{ numberShown < events.length && (
<FilterContainer>
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
Lataa lisää
{t("Lataa lisää")}
</Button>
</FilterContainer>
)}
+29 -8
View File
@@ -1,9 +1,11 @@
import React, { useState } from "react";
import Post from "@models/Feed";
import Button from "@components/Button";
import { CardSection, Card, FullWidthSection } from "@components/index";
import {
Button, CardSection, Card, FullWidthSection,
} from "@components/index";
import noop from "@utils/noop";
import { useTranslation } from "../../i18n";
import FilterContainer from "./FilterContainer";
interface NewsProps {
@@ -13,11 +15,29 @@ interface NewsProps {
const News: React.FC<NewsProps> = ({ feed }) => {
// const [filterSelected, setFilter] = useState(0);
const [numberShown, setNumberShown] = useState(8);
const filteredFeed = feed.slice(0, numberShown);
const { i18n, t } = useTranslation();
const isFi = i18n.language === "fi";
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
const filteredFeed = feed.slice(0, numberShown).map((p) => ({
...p,
title: isFi ? p.title_fi : p.title_en,
description: isFi ? p.description_fi : p.description_en,
content: isFi ? p.content_fi : p.content_en,
publishTime: new Date(p.publish_time).toLocaleString(isFi ? "fi-FI" : "en-GB", options),
}));
return (
<FullWidthSection>
<h1 id="uutiset">
Uutiset
{t("Uutiset")}
{/* <FilterContainer>
<Button buttonStyle="filter" onClick={() => { setFilter(0); }} selected={filterSelected === 0}>
Näytä kaikki
@@ -34,18 +54,19 @@ const News: React.FC<NewsProps> = ({ feed }) => {
{filteredFeed.map((post) => (
<Card
key={post.id}
title={post.title_fi}
start_time={post.publish_time}
text={post.description_fi}
title={post.title}
startTime={post.publishTime}
text={post.description}
link={`/feed/${post.id}`}
buttonOnClick={noop}
buttonText={`${t("Lue lisää")} `}
/>
))}
</CardSection>
{ numberShown < feed.length && (
<FilterContainer>
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
Lataa lisää
{t("Lataa lisää")}
</Button>
</FilterContainer>
)}
+63 -44
View File
@@ -3,12 +3,13 @@ import Image from "next/image";
import styled from "styled-components";
import colors from "@theme/colors";
import Event from "@models/Event";
import Button from "@components/Button";
import { Link, TextSection } from "@components/index";
import {
Button, Link, TextSection, ChangeLanguageButton,
} from "@components/index";
import noop from "@utils/noop";
import MarkdownStyles from "@views/common/MarkdownStyles";
import LoadingView from "@views/common/LoadingView";
import { useTranslation } from "../../i18n";
interface EventPageViewProps {
event?: Event;
@@ -33,7 +34,6 @@ const StyledTextSection = styled(TextSection)`
line-height: 0.4rem;
}
}
`;
const SignupButtons = styled.div`
@@ -46,49 +46,68 @@ const Content = styled(MarkdownStyles)`
margin-top: 1.5rem;
`;
const LngButton = styled(ChangeLanguageButton)`
align-self: flex-end;
margin-right: 1rem;
`;
const EventPageView: React.FC<EventPageViewProps> = ({ event }) => {
const { i18n, t } = useTranslation();
if (!event) return <LoadingView />;
const date_start = new Date(event.start_time).toLocaleString("fi-FI");
const date_end = new Date(event.end_time).toLocaleString("fi-FI");
const isFi = i18n.language === "fi";
const {
title, description, content, location, startDate, endDate,
} = {
title: isFi ? event.title_fi : event.title_en,
description: isFi ? event.description_fi : event.description_en,
content: isFi ? event.content_fi : event.content_en,
location: isFi ? event.location_fi : event.location_en,
startDate: new Date(event.start_time).toLocaleString(isFi ? "fi-FI" : "en-GB"),
endDate: new Date(event.end_time).toLocaleString(isFi ? "fi-FI" : "en-GB"),
};
return (
<StyledTextSection>
<h1>
{event.title_fi}
<p>
{event.description_fi}
</p>
<Image
src={event.image || event.tags[0].icon}
alt={event.title_fi}
objectFit="scale-down"
layout="responsive"
width={16}
height={9}
/>
</h1>
<div>
<Content source={event.content_fi} escapeHtml={false} />
<p>
Paikka: {event.location_fi}
</p>
<p>
<time>Alkaa: {date_start}</time>
</p>
<p>
<time>Päättyy: {date_end}</time>
</p>
{/* We may have multiple signup forms. Generate own Button for each one */}
<SignupButtons>
{event.signupForm.map((sf) => (
<Link key={sf.id} to={`/signup/${sf.id}`}>
<Button buttonStyle="filled" onClick={noop}>
{sf.title_fi}
</Button>
</Link>
))}
</SignupButtons>
</div>
</StyledTextSection>
<>
<LngButton />
<StyledTextSection>
<h1>
{title}
<p>
{description}
</p>
<Image
src={event.image || event.tags[0].icon}
alt={title}
objectFit="scale-down"
layout="responsive"
width={16}
height={9}
/>
</h1>
<div>
<Content source={content} escapeHtml={false} />
<p>
{`${t("Paikka")}: ${location}`}
</p>
<p>
<time>{`${t("Alkaa")}: ${startDate}`}</time>
</p>
<p>
<time>{`${t("Päättyy")}: ${endDate}`}</time>
</p>
{/* We may have multiple signup forms. Generate own Button for each one */}
<SignupButtons>
{event.signupForm.map((sf) => (
<Link key={sf.id} to={`/signup/${sf.id}`}>
<Button data-e2e="signup-button" buttonStyle="filled" onClick={noop}>
{isFi ? sf.title_fi : sf.title_en}
</Button>
</Link>
))}
</SignupButtons>
</div>
</StyledTextSection>
</>
);
};
export default EventPageView;
+35 -15
View File
@@ -3,9 +3,10 @@ import Image from "next/image";
import styled from "styled-components";
import colors from "@theme/colors";
import Post from "@models/Feed";
import { TextSection } from "@components/index";
import { TextSection, ChangeLanguageButton } from "@components/index";
import MarkdownStyles from "@views/common/MarkdownStyles";
import LoadingView from "@views/common/LoadingView";
import { useTranslation } from "../../i18n";
interface FeedPageViewProps {
post?: Post;
@@ -30,30 +31,49 @@ const Content = styled(MarkdownStyles)`
margin-top: 1.5rem;
`;
const LngButton = styled(ChangeLanguageButton)`
align-self: flex-end;
margin-right: 1rem;
`;
const FeedPageView: React.FC<FeedPageViewProps> = ({ post }) => {
const { i18n } = useTranslation();
if (!post) return <LoadingView />;
const { language } = i18n;
const isFi = language === "fi";
const {
title, description, content,
} = {
title: isFi ? post.title_fi : post.title_en,
description: isFi ? post.description_fi : post.description_en,
content: isFi ? post.content_fi : post.content_en,
};
return (
<StyledTextSection>
<h1>
{post.title_fi}
<p>
{post.description_fi}
</p>
{post.image && (
<>
<LngButton />
<StyledTextSection>
<h1>
{title}
<p>
{description}
</p>
{post.image && (
<Image
src={post.image}
alt={post.title_fi}
alt={title}
objectFit="scale-down"
layout="responsive"
width={16}
height={9}
/>
)}
</h1>
<div>
<Content source={post.content_fi} escapeHtml={false} />
</div>
</StyledTextSection>
)}
</h1>
<div>
<Content source={content} escapeHtml={false} />
</div>
</StyledTextSection>
</>
);
};
export default FeedPageView;
+12 -2
View File
@@ -32,6 +32,14 @@ interface FrontPageViewProps {
feed: Post[];
}
const cardTimeOpts: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
const SponsorReel = styled.div`
text-align: center;
& > div {
@@ -72,7 +80,7 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
<Card
key={event.id}
title={event.title_fi}
start_time={event.start_time}
startTime={new Date(event.start_time).toLocaleString("fi-FI", cardTimeOpts)}
text={event.description_fi}
link={`/events/${event.id}`}
image={{
@@ -80,6 +88,7 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
alt: event.title_fi,
}}
buttonOnClick={noop}
buttonText="Lue lisää&nbsp;"
data-e2e="event-card"
/>
))}
@@ -104,10 +113,11 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
<Card
key={inst.id}
title={inst.title_fi}
start_time={inst.publish_time}
startTime={new Date(inst.publish_time).toLocaleString("fi-FI", cardTimeOpts)}
text={inst.description_fi}
link={`/feed/${inst.id}`}
buttonOnClick={noop}
buttonText="Lue lisää&nbsp;"
/>
))}
<aside>
+12 -2
View File
@@ -37,6 +37,14 @@ interface InEnglishPageViewProps {
feed: Post[];
}
const cardTimeOpts: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) => (
<>
<InEnglishPageHero />
@@ -198,7 +206,7 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
<Card
key={event.id}
title={event.title_en}
start_time={event.start_time}
startTime={new Date(event.start_time).toLocaleString("en-GB", cardTimeOpts)}
text={event.description_en}
link={`/events/${event.id}`}
image={{
@@ -206,6 +214,7 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
alt: event.title_en,
}}
buttonOnClick={noop}
buttonText="Read more&nbsp;"
data-e2e="event-card"
/>
))}
@@ -227,10 +236,11 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
<Card
key={inst.id}
title={inst.title_en}
start_time={inst.publish_time}
startTime={new Date(inst.publish_time).toLocaleString("en-GB", cardTimeOpts)}
text={inst.description_en}
link={`/feed/${inst.id}`}
buttonOnClick={noop}
buttonText="Read more&nbsp;"
/>
))}
<aside>
+1 -1
View File
@@ -32,7 +32,7 @@ test("User signups to event from front page", async (t) => {
await t.wait(3000);
await t.expect(await getPageUrl()).match(/\/events\/\d{1,4}/, "URL isn't /events/<id>");
const SignupButton = Selector("button");
const SignupButton = Selector("[data-e2e=\"signup-button\"]");
await t
.click(SignupButton);