Add translations

This commit is contained in:
Kalle
2022-08-13 12:39:08 +03:00
parent 1b652f52f0
commit ec38b80147
10 changed files with 144 additions and 71 deletions

View File

@@ -19,7 +19,7 @@ Calendar
- [x] Winners on the event info page
- [x] There should be a banner on the list page if you have past tournaments to report
- [x] On the user page tab showing past results
- [ ] Translations
- [x] Translations
## Other

View File

@@ -1,6 +1,8 @@
import { useActionData } from "@remix-run/react";
import { useTranslation } from "react-i18next";
export function FormErrors() {
export function FormErrors({ namespace }: { namespace: "calendar" }) {
const { t } = useTranslation(["common", namespace]);
const actionData = useActionData<{ errors?: string[] }>();
if (!actionData?.errors || actionData.errors.length === 0) {
@@ -9,10 +11,10 @@ export function FormErrors() {
return (
<div className="form-errors">
<h4>Following errors need to be fixed:</h4>
<h4>{t("common:forms.errors.title")}:</h4>
<ol>
{actionData.errors.map((error) => (
<li key={error}>{error}</li>
<li key={error}>{t(`${namespace}:${error}` as any)}</li>
))}
</ol>
</div>

View File

@@ -73,7 +73,7 @@ export const loader = async ({ params, request }: LoaderArgs) => {
export default function CalendarEventPage() {
const user = useUser();
const data = useLoaderData<typeof loader>();
const { i18n } = useTranslation();
const { i18n, t } = useTranslation(["common", "calendar"]);
const isMounted = useIsMounted();
return (
@@ -87,7 +87,9 @@ export default function CalendarEventPage() {
hidden: data.event.startTimes.length === 1,
})}
>
Day {i + 1}
{t("calendar:day", {
number: i + 1,
})}
</span>
<time dateTime={databaseTimestampToDate(startTime).toISOString()}>
{isMounted
@@ -136,7 +138,7 @@ export default function CalendarEventPage() {
to={calendarEditPage(data.event.eventId)}
data-cy="edit-button"
>
Edit
{t("common:actions.edit")}
</LinkButton>
)}
{canReportCalendarEventWinners({
@@ -149,7 +151,7 @@ export default function CalendarEventPage() {
to={calendarReportWinnersPage(data.event.eventId)}
data-cy="edit-button"
>
Report winners
{t("calendar:actions.reportWinners")}
</LinkButton>
)}
</div>
@@ -162,29 +164,32 @@ export default function CalendarEventPage() {
}
function Results() {
const { t } = useTranslation(["common", "calendar"]);
const data = useLoaderData<typeof loader>();
if (!data.results.length) return null;
return (
<Section title="Results" className="event__results-section">
<div className="event__results-participant-count">
{data.event.participantCount} teams participated
</div>
<Section title={t("calendar:results")} className="event__results-section">
{data.event.participantCount && (
<div className="event__results-participant-count">
{t("calendar:participatedCount", {
count: data.event.participantCount,
})}
</div>
)}
<table>
<thead>
<tr>
<th>Placing</th>
<th>Name</th>
<th>Members</th>
<th>{t("calendar:forms.team.placing")}</th>
<th>{t("common:forms.name")}</th>
<th>{t("calendar:members")}</th>
</tr>
</thead>
<tbody>
{data.results.map((result, i) => (
<tr key={i}>
<td className="text-center">
{placementString(result.placement)}
</td>
<td className="pl-4">{placementString(result.placement)}</td>
<td>{result.teamName}</td>
<td>
<ul className="event__results-players">
@@ -221,10 +226,11 @@ function Results() {
}
function Description() {
const { t } = useTranslation();
const data = useLoaderData<typeof loader>();
return (
<Section title="Description">
<Section title={t("forms.description")}>
<div className="stack sm">
<div className="event__author">
<Avatar

View File

@@ -28,6 +28,7 @@ import { FormErrors } from "~/components/FormErrors";
import type { UseDataFunctionReturn } from "@remix-run/react/dist/components";
import type { Unpacked } from "~/utils/types";
import { calendarEventPage } from "~/utils/urls";
import { useTranslation } from "react-i18next";
const playersSchema = z
.array(
@@ -36,7 +37,7 @@ const playersSchema = z
z.object({ id }),
])
)
.nonempty({ message: "Each team must have at least one player." })
.nonempty({ message: "forms.errors.emptyTeam" })
.max(CALENDAR_EVENT_RESULT.MAX_PLAYERS_LENGTH)
.refine(
(val) => {
@@ -47,7 +48,7 @@ const playersSchema = z
return userIds.length === new Set(userIds).size;
},
{
message: "Can't have the same player twice in the same team.",
message: "forms.errors.duplicatePlayer",
}
);
@@ -85,7 +86,7 @@ const reportWinnersActionSchema = z.object({
)
.refine(
(val) => val.length === new Set(val.map((team) => team.teamName)).size,
{ message: "Each team needs a unique name." }
{ message: "forms.errors.uniqueTeamName" }
)
),
});
@@ -149,34 +150,37 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export default function ReportWinnersPage() {
const { t } = useTranslation(["common", "calendar"]);
const data = useLoaderData<typeof loader>();
return (
<Main halfWidth>
<Form method="post" className="stack smedium items-start">
<h1 className="text-lg">Reporting results of {data.name}</h1>
<h1 className="text-lg">
{t("calendar:forms.reportResultsHeader", { eventName: data.name })}
</h1>
<ParticipantsCountInput />
<FormMessage type="info">
You choose how many results to report. It can be just the winning
team, top 3 or whatever you decide.
{t("calendar:forms.reportResultsInfo")}
</FormMessage>
<TeamInputs />
<Button type="submit" className="mt-4">
Submit
{t("common:actions.submit")}
</Button>
<FormErrors />
<FormErrors namespace="calendar" />
</Form>
</Main>
);
}
function ParticipantsCountInput() {
const { t } = useTranslation("calendar");
const data = useLoaderData<typeof loader>();
return (
<div>
<Label htmlFor="name" required>
Participant count
{t("forms.participantCount")}
</Label>
<input
name="participantCount"
@@ -193,6 +197,7 @@ function ParticipantsCountInput() {
}
function TeamInputs() {
const { t } = useTranslation("calendar");
const data = useLoaderData<typeof loader>();
const [amountOfTeams, setAmountOfTeams] = React.useState(
Math.max(data.winners.length, 1)
@@ -229,7 +234,7 @@ function TeamInputs() {
onClick={() => setAmountOfTeams((amountOfTeams) => amountOfTeams + 1)}
tiny
>
Add team
{t("forms.team.add")}
</Button>
</>
);
@@ -259,6 +264,7 @@ function Team({
initialPlacement: string;
initialValues?: Unpacked<UseDataFunctionReturn<typeof loader>["winners"]>;
}) {
const { t } = useTranslation("calendar");
const teamNameId = React.useId();
const placementId = React.useId();
@@ -296,7 +302,7 @@ function Team({
/>
<div className="stack horizontal md flex-wrap">
<div>
<Label htmlFor={teamNameId}>Team name</Label>
<Label htmlFor={teamNameId}>{t("forms.team.name")}</Label>
<input
id={teamNameId}
value={results.teamName}
@@ -306,7 +312,7 @@ function Team({
/>
</div>
<div>
<Label htmlFor={placementId}>Placement</Label>
<Label htmlFor={placementId}>{t("forms.team.placing")}</Label>
<input
id={placementId}
value={results.placement}
@@ -329,7 +335,7 @@ function Team({
variant="minimal-destructive"
className="mt-4"
>
Remove team
{t("forms.team.remove")}
</Button>
)}
</div>
@@ -343,6 +349,7 @@ function Players({
players: TeamResults["players"];
setPlayers: (newPlayers: TeamResults["players"]) => void;
}) {
const { t } = useTranslation("calendar");
const handleAddPlayer = () => {
setPlayers([...players, NEW_PLAYER]);
};
@@ -374,14 +381,16 @@ function Players({
<div key={i}>
<div className="stack horizontal md items-center mb-1">
<label htmlFor={formId} className="mb-0">
Player {i + 1}
{t("forms.team.player.header", { number: i + 1 })}
</label>
<Button
tiny
variant="minimal"
onClick={() => handlePlayerInputTypeChange(i)}
>
{asPlainInput ? "Add as user (recommended)" : "Add as text"}
{asPlainInput
? t("forms.team.player.addAsUser")
: t("forms.team.player.addAsText")}
</Button>
</div>
{asPlainInput ? (
@@ -414,7 +423,7 @@ function Players({
disabled={players.length === CALENDAR_EVENT_RESULT.MAX_PLAYERS_LENGTH}
variant="outlined"
>
Add player
{t("forms.team.player.add")}
</Button>{" "}
<Button
tiny
@@ -422,7 +431,7 @@ function Players({
onClick={handleRemovePlayer}
disabled={players.length === 1}
>
Remove player
{t("forms.team.player.remove")}
</Button>
</div>
</div>

View File

@@ -129,6 +129,7 @@ function fetchEventsOfWeek(args: { week: number; year: number }) {
}
export default function CalendarPage() {
const { t } = useTranslation("calendar");
const data = useLoaderData<typeof loader>();
const user = useUser();
const isMounted = useIsMounted();
@@ -150,7 +151,7 @@ export default function CalendarPage() {
<div className="stack md">
{user && (
<LinkButton to="new" className="calendar__add-new-button" tiny>
Add new
{t("addNew")}
</LinkButton>
)}
{isMounted ? (
@@ -159,13 +160,13 @@ export default function CalendarPage() {
<>
<EventsList events={thisWeeksEvents} />
<div className="calendar__time-zone-info">
All times in your local time zone:{" "}
{t("inYourTimeZone")}{" "}
{Intl.DateTimeFormat().resolvedOptions().timeZone}
</div>
</>
) : (
<h2 className="calendar__no-events" data-cy="no-events">
No events this week
{t("noEvents")}
</h2>
)}
</>
@@ -230,23 +231,24 @@ function WeekLinkTitle({
}: {
week: Unpacked<UseDataFunctionReturn<typeof loader>["weeks"]>;
}) {
const { t } = useTranslation("calendar");
const data = useLoaderData<typeof loader>();
const { i18n } = useTranslation();
const relativeWeekIdentifier =
week.number === data.currentWeek
? "This"
? t("week.this")
: week.number - data.currentWeek === 1
? "Next"
? t("week.next")
: week.number - data.currentWeek === -1
? "Last"
? t("week.last")
: null;
if (relativeWeekIdentifier) {
return (
<div className="stack xxs">
<div>{relativeWeekIdentifier}</div>
<div>Week</div>
<div>{t("week.week")}</div>
</div>
);
}
@@ -292,21 +294,18 @@ function getEventsCountPerWeek(
}
function EventsToReport() {
const { t } = useTranslation("calendar");
const data = useLoaderData<typeof loader>();
if (data.eventsToReport.length === 0) return null;
return (
<Alert textClassName="calendar__events-to-report">
You can report results for{" "}
{t("reportResults")}{" "}
{data.eventsToReport.map((event, i) => (
<React.Fragment key={event.id}>
<Link to={calendarReportWinnersPage(event.id)}>{event.name}</Link>
{i === data.eventsToReport.length - 1
? ""
: i === data.eventsToReport.length - 2
? " and "
: ", "}
{i === data.eventsToReport.length - 1 ? "" : ", "}
</React.Fragment>
))}
</Alert>
@@ -318,7 +317,7 @@ function EventsList({
}: {
events: UseDataFunctionReturn<typeof loader>["events"];
}) {
const { i18n } = useTranslation();
const { t, i18n } = useTranslation("calendar");
return (
<div className="calendar__events-container">
@@ -374,7 +373,9 @@ function EventsList({
{calendarEvent.name}{" "}
{calendarEvent.nthAppearance > 1 ? (
<span className="calendar__event__day">
Day {calendarEvent.nthAppearance}
{t("day", {
number: calendarEvent.nthAppearance,
})}
</span>
) : null}
</h2>

View File

@@ -215,12 +215,13 @@ export default function CalendarNewEventPage() {
}
function NameInput() {
const { t } = useTranslation();
const { eventToEdit } = useLoaderData<typeof loader>();
return (
<div>
<Label htmlFor="name" required>
Name
{t("forms.name")}
</Label>
<input
name="name"
@@ -235,6 +236,7 @@ function NameInput() {
}
function DescriptionTextarea() {
const { t } = useTranslation();
const { eventToEdit } = useLoaderData<typeof loader>();
const [value, setValue] = React.useState(eventToEdit?.description ?? "");
@@ -247,7 +249,7 @@ function DescriptionTextarea() {
max: CALENDAR_EVENT.DESCRIPTION_MAX_LENGTH,
}}
>
Description
{t("forms.description")}
</Label>
<textarea
id="description"
@@ -262,6 +264,7 @@ function DescriptionTextarea() {
}
function DatesInput() {
const { t } = useTranslation(["common", "calendar"]);
const { eventToEdit } = useLoaderData<typeof loader>();
const [datesCount, setDatesCount] = React.useState(
eventToEdit?.startTimes.length ?? 1
@@ -276,7 +279,7 @@ function DatesInput() {
<div className="stack md items-start">
<div>
<Label htmlFor="date" required>
Dates
{t("calendar:forms.dates")}
</Label>
<div className="stack sm">
{new Array(datesCount).fill(null).map((_, i) => {
@@ -307,7 +310,7 @@ function DatesInput() {
onClick={() => setDatesCount((count) => count + 1)}
data-cy="add-date-button"
>
Add
{t("common:actions.add")}
</Button>
{datesCount > 1 && (
<Button
@@ -316,7 +319,7 @@ function DatesInput() {
data-cy="remove-date-button"
variant="destructive"
>
Remove
{t("common:actions.remove")}
</Button>
)}
</>
@@ -326,7 +329,7 @@ function DatesInput() {
})}
</div>
<FormMessage type="info" className={clsx({ invisible: !isMounted })}>
Times in your local time zone: {usersTimeZone}
{t("calendar:inYourTimeZone")} {usersTimeZone}
</FormMessage>
</div>
</div>
@@ -334,12 +337,13 @@ function DatesInput() {
}
function BracketUrlInput() {
const { t } = useTranslation("calendar");
const { eventToEdit } = useLoaderData<typeof loader>();
return (
<div>
<Label htmlFor="bracketUrl" required>
Bracket URL
{t("forms.bracketUrl")}
</Label>
<input
name="bracketUrl"
@@ -354,11 +358,12 @@ function BracketUrlInput() {
}
function DiscordLinkInput() {
const { t } = useTranslation("calendar");
const { eventToEdit } = useLoaderData<typeof loader>();
return (
<div className="stack items-start">
<Label htmlFor="discordInviteCode">Discord server invite URL</Label>
<Label htmlFor="discordInviteCode">{t("forms.discordInvite")}</Label>
<Input
name="discordInviteCode"
leftAddon="https://discord.gg/"
@@ -371,8 +376,8 @@ function DiscordLinkInput() {
}
function TagsAdder() {
const { eventToEdit } = useLoaderData<typeof loader>();
const { t } = useTranslation("calendar");
const { eventToEdit } = useLoaderData<typeof loader>();
const [tags, setTags] = React.useState(
(eventToEdit?.tags ?? []) as Array<CalendarEventTag>
);
@@ -390,7 +395,7 @@ function TagsAdder() {
value={JSON.stringify(tags.length > 0 ? tags : null)}
/>
<div>
<label htmlFor={id}>Tags</label>
<label htmlFor={id}>{t("forms.tags")}</label>
<select
id={id}
className="calendar-new__select"
@@ -399,16 +404,14 @@ function TagsAdder() {
}
data-cy="tags-select"
>
<option value="">Choose a tag</option>
<option value="">{t("forms.tags.placeholder")}</option>
{tagsForSelect.map((tag) => (
<option key={tag} value={tag}>
{t(`tag.name.${tag}`)}
</option>
))}
</select>
<FormMessage type="info">
&quot;Badge prizes&quot; tag is added automatically if applicable
</FormMessage>
<FormMessage type="info">{t("forms.tags.info")}</FormMessage>
</div>
<Tags
tags={tags}
@@ -421,6 +424,7 @@ function TagsAdder() {
}
function BadgesAdder() {
const { t } = useTranslation("calendar");
const { eventToEdit } = useLoaderData<typeof loader>();
const { managedBadges } = useLoaderData<typeof loader>();
const [badges, setBadges] = React.useState(eventToEdit?.badges ?? []);
@@ -448,7 +452,7 @@ function BadgesAdder() {
<div className="stack md">
{input}
<div>
<label htmlFor={id}>Badge prizes</label>
<label htmlFor={id}>{t("forms.badges")}</label>
<select
id={id}
className="calendar-new__select"
@@ -462,7 +466,7 @@ function BadgesAdder() {
}}
data-cy="badges-select"
>
<option value="">Choose a badge prize</option>
<option value="">{t("forms.badges.placeholder")}</option>
{badgesForSelect.map((badge) => (
<option key={badge.id} value={badge.id}>
{badge.displayName}

View File

@@ -29,9 +29,7 @@ export default function UserResultsPage() {
<tbody>
{data.results.map((result) => (
<tr key={result.eventId}>
<td className="text-center">
{placementString(result.placement)}
</td>
<td className="pl-4">{placementString(result.placement)}</td>
<td>{result.teamName}</td>
<td>
<Link to={calendarEventPage(result.eventId)}>

View File

@@ -38,6 +38,10 @@
width: var(--s-24);
}
.pl-4 {
padding-inline-start: var(--s-4);
}
.mt-2 {
margin-block-start: var(--s-2);
}

View File

@@ -1,4 +1,46 @@
{
"inYourTimeZone": "Times in your local time zone:",
"addNew": "Add new",
"noEvents": "No events this week",
"reportResults": "You can report results:",
"day": "Day {{number}}",
"actions.reportWinners": "Report winners",
"participatedCount": "{{count}} teams participated",
"members": "Members",
"results": "Results",
"forms.dates": "Dates",
"forms.bracketUrl": "Bracket URL",
"forms.discordInvite": "Discord server invite URL",
"forms.tags": "Tags",
"forms.tags.placeholder": "Choose a tag",
"forms.tags.info": "\"Badge prizes\" tag is added automatically if applicable",
"forms.badges": "Badge prizes",
"forms.badges.placeholder": "Choose a badge prize",
"forms.participantCount": "Participant count",
"forms.reportResultsHeader": "Reporting results for {{eventName}}",
"forms.reportResultsInfo": "You choose how many results to report. It can be just the winning team, top 3 or whatever you decide.",
"forms.team.add": "Add team",
"forms.team.remove": "Remove team",
"forms.team.name": "Team name",
"forms.team.placing": "Placing",
"forms.team.player.header": "Player {{number}}",
"forms.team.player.add": "Add player",
"forms.team.player.remove": "Remove player",
"forms.team.player.addAsUser": "Add as user (recommended)",
"forms.team.player.addAsText": "Add as text",
"forms.errors.uniqueTeamName": "Each team needs a unique name.",
"forms.errors.duplicatePlayer": "Can't have the same player twice in the same team.",
"forms.errors.emptyTeam": "Each team must have at least one player.",
"week.this": "This",
"week.next": "Next",
"week.last": "Last",
"week.week": "Week",
"tag.name.BADGE": "Badge prizes",
"tag.name.SPECIAL": "Special rules",
"tag.name.ART": "Art prizes",

View File

@@ -18,7 +18,14 @@
"actions.save": "Save",
"actions.saving": "Saving...",
"actions.submit": "Submit",
"actions.edit": "Edit",
"actions.add": "Add",
"actions.remove": "Remove",
"results": "Results"
"results": "Results",
"forms.name": "Name",
"forms.description": "Description",
"forms.errors.title": "Following errors need to be fixed"
}