Translations (#225)

This commit is contained in:
Luke Vella 2022-07-17 17:11:56 +01:00 committed by GitHub
parent 9c61d34f24
commit bfb14b0e02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 342 additions and 275 deletions

View file

@ -216,7 +216,7 @@ const Page: NextPage<CreatePollPageProps> = ({
type="primary"
>
{currentStepIndex < steps.length - 1
? t("next")
? t("continue")
: t("createPoll")}
</Button>
</div>

View file

@ -1,6 +1,7 @@
import clsx from "clsx";
import dayjs from "dayjs";
import { AnimatePresence, motion } from "framer-motion";
import { useTranslation } from "next-i18next";
import { usePlausible } from "next-plausible";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
@ -25,6 +26,7 @@ interface CommentForm {
const Discussion: React.VoidFunctionComponent = () => {
const queryClient = trpc.useContext();
const { t } = useTranslation("app");
const { poll } = usePoll();
const pollId = poll.id;
@ -82,7 +84,7 @@ const Discussion: React.VoidFunctionComponent = () => {
return (
<div className="overflow-hidden border-t border-b shadow-sm md:rounded-lg md:border">
<div className="border-b bg-white px-4 py-2">
<div className="font-medium">Comments</div>
<div className="font-medium">{t("comments")}</div>
</div>
<div
className={clsx({
@ -129,7 +131,7 @@ const Discussion: React.VoidFunctionComponent = () => {
>
<DropdownItem
icon={Trash}
label="Delete comment"
label={t("deleteComment")}
disabled={!canDelete}
onClick={() => {
deleteComment.mutate({
@ -158,7 +160,7 @@ const Discussion: React.VoidFunctionComponent = () => {
>
<textarea
id="comment"
placeholder="Thanks for the invite!"
placeholder={t("commentPlaceholder")}
className="input w-full py-2 pl-3 pr-4"
{...register("content", { validate: requiredString })}
/>
@ -175,7 +177,7 @@ const Discussion: React.VoidFunctionComponent = () => {
/>
</div>
<Button htmlType="submit" loading={formState.isSubmitting}>
Comment
{t("comment")}
</Button>
</div>
</form>

View file

@ -1,5 +1,6 @@
import clsx from "clsx";
import dayjs from "dayjs";
import { useTranslation } from "next-i18next";
import { usePlausible } from "next-plausible";
import * as React from "react";
@ -37,6 +38,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
duration,
onChangeDuration,
}) => {
const { t } = useTranslation("app");
const isTimedEvent = options.some((option) => option.type === "timeSlot");
const plausible = usePlausible();
@ -91,14 +93,14 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
<div className="mb-3 flex items-center justify-center space-x-4">
<Button
icon={<ChevronLeft />}
title="Previous month"
title={t("previousMonth")}
onClick={datepicker.prev}
/>
<div className="grow text-center text-lg font-medium">
{datepicker.label}
</div>
<Button
title="Next month"
title={t("nextMonth")}
icon={<ChevronRight />}
onClick={datepicker.next}
/>
@ -190,9 +192,9 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
>
<div className="flex items-center space-x-3 p-4">
<div className="grow">
<div className="font-medium">Specify times</div>
<div className="font-medium">{t("specifyTimes")}</div>
<div className="text-sm text-slate-400">
Include start and end times for each option
{t("specifyTimesDescription")}
</div>
</div>
<div>
@ -338,7 +340,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
]);
}}
>
Add time option
{t("addTimeOption")}
</Button>
<Dropdown
trigger={<CompactButton icon={DotsHorizontal} />}
@ -347,7 +349,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
<DropdownItem
icon={Magic}
disabled={datepicker.selection.length < 2}
label="Apply to all dates"
label={t("applyToAllDates")}
onClick={() => {
plausible("Applied options to all dates");
const times = optionsForDay.map(
@ -384,7 +386,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
}}
/>
<DropdownItem
label="Delete date"
label={t("deleteDate")}
icon={Trash}
onClick={() => {
onChange(
@ -431,7 +433,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
<div className="flex h-full items-center justify-center py-12">
<div className="text-center font-medium text-gray-400">
<Calendar className="mb-2 inline-block h-12 w-12" />
<div>No dates selected</div>
<div>{t("noDatesSelected")}</div>
</div>
</div>
)}

View file

@ -52,18 +52,18 @@ const PollOptionsForm: React.VoidFunctionComponent<
const views = React.useMemo(() => {
const res = [
{
label: "Month view",
label: t("monthView"),
value: "month",
Component: MonthCalendar,
},
{
label: "Week view",
label: t("weekView"),
value: "week",
Component: WeekCalendar,
},
];
return res;
}, []);
}, [t]);
const watchView = watch("view");
@ -79,10 +79,9 @@ const PollOptionsForm: React.VoidFunctionComponent<
const datesOnly = watchOptions.every((option) => option.type === "date");
const [dateOrTimeRangeModal, openDateOrTimeRangeModal] = useModal({
title: "Wait a minute… 🤔",
description:
"You can't have both time and date options in the same poll. Which would you like to keep?",
okText: "Keep time options",
title: t("mixedOptionsTitle"),
description: t("mixedOptionsDescription"),
okText: t("mixedOptionsKeepTimes"),
onOk: () => {
setValue(
"options",
@ -92,7 +91,7 @@ const PollOptionsForm: React.VoidFunctionComponent<
setValue("timeZone", getBrowserTimeZone());
}
},
cancelText: "Keep date options",
cancelText: t("mixedOptionsKeepDates"),
onCancel: () => {
setValue(
"options",
@ -130,7 +129,7 @@ const PollOptionsForm: React.VoidFunctionComponent<
const [calendarHelpModal, openHelpModal] = useModal({
overlayClosable: true,
title: "Forget something?",
title: t("calendarHelpTitle"),
description: t("calendarHelp"),
okText: t("ok"),
});
@ -172,7 +171,7 @@ const PollOptionsForm: React.VoidFunctionComponent<
}}
type="button"
>
<Calendar className="mr-2 h-5 w-5" /> Month view
<Calendar className="mr-2 h-5 w-5" /> {t("monthView")}
</button>
<button
className={clsx({
@ -183,7 +182,7 @@ const PollOptionsForm: React.VoidFunctionComponent<
setValue("view", "week");
}}
>
<Table className="mr-2 h-5 w-5" /> Week view
<Table className="mr-2 h-5 w-5" /> {t("weekView")}
</button>
</div>
</div>
@ -191,7 +190,9 @@ const PollOptionsForm: React.VoidFunctionComponent<
<div className="relative w-full">
<React.Suspense
fallback={
<FullPageLoader className="h-[400px]">Loading</FullPageLoader>
<FullPageLoader className="h-[400px]">
{t("loading")}
</FullPageLoader>
}
>
<selectedView.Component

View file

@ -38,7 +38,7 @@ export const UserDetailsForm: React.VoidFunctionComponent<
style={{ width: 400 }}
onSubmit={handleSubmit(onSubmit)}
>
<h2>Your details</h2>
<h2>{t("yourDetails")}</h2>
<div className="formField">
<label className="text-slate-500" htmlFor="name">
{t("name")}

View file

@ -1,4 +1,5 @@
import Head from "next/head";
import { useTranslation } from "next-i18next";
import React from "react";
import Bonus from "./home/bonus";
@ -7,14 +8,12 @@ import Hero from "./home/hero";
import PageLayout from "./page-layout";
const Home: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
return (
<PageLayout>
<Head>
<meta
name="description"
content="Create polls and vote to find the best day or time. A free alternative to Doodle."
/>
<title>Rallly - Schedule group meetings</title>
<meta name="description" content={t("metaDescription")} />
<title>{t("metaTitle")}</title>
</Head>
<Hero />
<Features />

View file

@ -1,3 +1,4 @@
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import Code from "@/components/icons/code.svg";
@ -7,50 +8,52 @@ import Server from "@/components/icons/server.svg";
import Ban from "./ban-ads.svg";
const Bonus: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
return (
<div className="mx-auto max-w-7xl px-8 pt-8 pb-24">
<h2 className="heading">Principles</h2>
<p className="subheading">We&apos;re not like the others</p>
<h2 className="heading">{t("principles")}</h2>
<p className="subheading">{t("principlesSubheading")}</p>
<div className="grid grid-cols-4 gap-16">
<div className="col-span-4 md:col-span-2 lg:col-span-1">
<div className="mb-4 text-gray-400">
<CursorClick className="w-16" />
</div>
<h3 className="heading-sm">No login required</h3>
<h3 className="heading-sm">{t("noLoginRequired")}</h3>
<div className="text text-base leading-relaxed">
We keep things simple and don&apos;t ask for more than what we need.
{t("noLoginRequiredDescription")}
</div>
</div>
<div className="col-span-4 md:col-span-2 lg:col-span-1">
<div className="mb-4 text-gray-400">
<Code className="w-16" />
</div>
<h3 className="heading-sm">Open-source</h3>
<h3 className="heading-sm">{t("openSource")}</h3>
<div className="text text-base leading-relaxed">
The codebase is fully open-source and{" "}
<a href="https://github.com/lukevella/rallly">
available on github
</a>
.
<Trans
t={t}
i18nKey={"openSourceDescription"}
components={{
a: <a href="https://github.com/lukevella/rallly" />,
}}
/>
</div>
</div>
<div className="col-span-4 md:col-span-2 lg:col-span-1">
<div className="mb-4 text-gray-400">
<Server className="w-16" />
</div>
<h3 className="heading-sm">Self-hostable</h3>
<h3 className="heading-sm">{t("selfHostable")}</h3>
<div className="text text-base leading-relaxed">
Run it on your own server to get full control of your data.
{t("selfHostableDescription")}
</div>
</div>
<div className="col-span-4 md:col-span-2 lg:col-span-1">
<div className="mb-4 text-gray-400">
<Ban className="w-16" />
</div>
<h3 className="heading-sm">Ad-free</h3>
<h3 className="heading-sm">{t("adFree")}</h3>
<div className="text text-base leading-relaxed">
You can give your ad-blocker a rest &ndash; You won&apos;t need it
here.
{t("adFreeDescription")}
</div>
</div>
</div>

View file

@ -1,3 +1,4 @@
import { useTranslation } from "next-i18next";
import * as React from "react";
import Bell from "@/components/icons/bell.svg";
@ -6,56 +7,44 @@ import Clock from "@/components/icons/clock.svg";
import DeviceMobile from "@/components/icons/device-mobile.svg";
const Features: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
return (
<div className="mx-auto max-w-7xl py-16 px-8">
<h2 className="heading">Features</h2>
<p className="subheading">Everything you need to get the job done</p>
<h2 className="heading">{t("features")}</h2>
<p className="subheading">{t("featuresSubheading")}</p>
<div className="grid grid-cols-2 gap-12">
<div className="col-span-2 md:col-span-1">
<div className="mb-4 inline-block rounded-2xl bg-green-100/50 p-3 text-green-400">
<Clock className="h-8 w-8" />
</div>
<h3 className="heading-sm flex items-center">
Time slots
{t("timeSlots")}
<span className="ml-2 rounded-full bg-green-500 px-2 py-1 text-sm font-normal text-white">
New
{t("new")}
</span>
</h3>
<p className="text">
If you need more granular options, Rallly lets you choose time slots
as options. If your participants are international, they can see
times in their own time zone.
</p>
<p className="text">{t("timeSlotsDescription")}</p>
</div>
<div className="col-span-2 md:col-span-1">
<div className="mb-4 inline-block rounded-2xl bg-cyan-100/50 p-3 text-cyan-400">
<DeviceMobile className="h-8 w-8" />
</div>
<h3 className="heading-sm">Mobile friendly design</h3>
<p className="text">
Rallly is optimized to look and work great on mobile devices so you
and your participants can use it on the go.
</p>
<h3 className="heading-sm">{t("mobileFriendly")}</h3>
<p className="text">{t("mobileFriendlyDescription")}</p>
</div>
<div className="col-span-2 md:col-span-1">
<div className="mb-4 inline-block rounded-2xl bg-rose-100/50 p-3 text-rose-400">
<Bell className="h-8 w-8" />
</div>
<h3 className="heading-sm">Notifications</h3>
<p className="text">
Need help staying on top of things? Rallly can send you an email
whenever participants vote or comment on your poll.
</p>
<h3 className="heading-sm">{t("notifications")}</h3>
<p className="text">{t("notificationsDescription")}</p>
</div>
<div className="col-span-2 md:col-span-1">
<div className="mb-4 inline-block rounded-2xl bg-yellow-100/50 p-3 text-yellow-400">
<Chat className="h-8 w-8" />
</div>
<h3 className="heading-sm">Comments</h3>
<p className="text">
Got a question or just have something to say? You and your
participants can comment on polls to start a discussion.
</p>
<h3 className="heading-sm">{t("comments")}</h3>
<p className="text">{t("commentsDescription")}</p>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
import { motion } from "framer-motion";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import { UserAvatarProvider } from "../poll/user-avatar";
@ -15,15 +15,16 @@ const Hero: React.VoidFunctionComponent = () => {
<div className="mx-auto max-w-7xl items-end p-8 lg:flex lg:justify-between">
<div className="my-8 text-center lg:text-left">
<h1 className="text-4xl font-bold sm:text-5xl">
Schedule
<br />
<span className="text-primary-500">group&nbsp;meetings</span>
<br />
with ease
<Trans
t={t}
i18nKey="heroText"
components={{
br: <br />,
s: <span className="whitespace-nowrap text-primary-500" />,
}}
/>
</h1>
<div className="mb-12 text-xl text-gray-400">
Find the right date without the back and&nbsp;forth.
</div>
<div className="mb-12 text-xl text-gray-400">{t("heroSubText")}</div>
<div className="space-x-3">
<Link href="/new">
<a className="rounded-lg bg-primary-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-primary-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 focus:ring-primary-200 active:bg-primary-600/90">
@ -35,7 +36,7 @@ const Hero: React.VoidFunctionComponent = () => {
className="rounded-lg bg-slate-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-slate-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 focus:ring-primary-200 active:bg-slate-600/90"
rel="nofollow"
>
{t("viewDemo")}
{t("liveDemo")}
</a>
</Link>
</div>
@ -60,7 +61,7 @@ const Hero: React.VoidFunctionComponent = () => {
animate={{ opacity: 1, translateY: 0 }}
transition={{ type: "spring", delay: 2 }}
>
Perfect! 🤩
{t("perfect")} 🤩
<ScribbleArrow className="absolute -right-8 top-3 text-slate-400" />
</motion.div>
<motion.div

View file

@ -33,7 +33,7 @@ const participants = [
const options = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"];
const PollDemo: React.VoidFunctionComponent = () => {
const { t } = useTranslation("app");
const { t } = useTranslation("homepage");
return (
<div

View file

@ -1,30 +0,0 @@
import Link from "next/link";
import { useTranslation } from "next-i18next";
import * as React from "react";
const Stats: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");
return (
<div className="py-16">
<h2 className="heading text-center">Stats</h2>
<p className="subheading text-center">100,000+ polls created</p>
<div className="flex justify-center space-x-3">
<Link href="/new">
<a className="bg-primary-500 hover:bg-primary-500/90 focus:ring-primary-200 active:bg-primary-600/90 rounded-lg px-5 py-3 font-semibold text-white shadow-sm transition-all hover:text-white hover:no-underline hover:shadow-md focus:ring-2">
{t("getStarted")}
</a>
</Link>
<Link href="/demo">
<a
className="focus:ring-primary-200 rounded-lg bg-slate-500 px-5 py-3 font-semibold text-white shadow-sm transition-all hover:bg-slate-500/90 hover:text-white hover:no-underline hover:shadow-md focus:ring-2 active:bg-slate-600/90"
rel="nofollow"
>
{t("viewDemo")}
</a>
</Link>
</div>
</div>
);
};
export default Stats;

View file

@ -3,6 +3,7 @@ import dynamic from "next/dynamic";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import { createBreakpoint } from "react-use";
@ -28,7 +29,7 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
<Link href="/">
<a
className={clsx(
"hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2",
"text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2",
{
"pointer-events-none font-bold text-gray-600":
pathname === "/home",
@ -41,7 +42,7 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
<Link href="https://blog.rallly.co">
<a
className={clsx(
"hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2",
"text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2",
)}
>
Blog
@ -49,12 +50,12 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({
</Link>
<a
href="https://support.rallly.co"
className="hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2"
className="text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2"
>
Support
</a>
<Link href="https://github.com/lukevella/rallly">
<a className="hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2">
<a className="text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2">
<Github className="w-6" />
</a>
</Link>
@ -66,6 +67,7 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
children,
}) => {
const breakpoint = useBreakpoint();
const { t } = useTranslation("homepage");
return (
<div className="bg-pattern min-h-full overflow-x-hidden">
<Head>
@ -76,11 +78,11 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
<div className="relative inline-block">
<Link href="/">
<a>
<Logo className="text-primary-500 w-40" alt="Rallly" />
<Logo className="w-40 text-primary-500" alt="Rallly" />
</a>
</Link>
<span className="absolute -bottom-6 right-0 text-sm text-slate-400 transition-colors">
Yes&mdash;with 3 <em>L</em>s
<Trans t={t} i18nKey="3Ls" components={{ e: <em /> }} />
</span>
</div>
</div>
@ -89,7 +91,7 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
<Popover
placement="left-start"
trigger={
<button className="hover:text-primary-500 text-gray-400 transition-colors hover:no-underline hover:underline-offset-2">
<button className="text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2">
<DotsVertical className="w-5" />
</button>
}

View file

@ -34,32 +34,32 @@ const Footer: React.VoidFunctionComponent = () => {
aria-label="Star lukevella/rallly on GitHub"
data-show-count={true}
>
Star
{t("star")}
</GitHubButton>
<GitHubButton
href="https://github.com/sponsors/lukevella"
data-icon="octicon-heart"
aria-label="Sponsor @lukevella on GitHub"
>
Sponsor this project
{t("sponsorThisProject")}
</GitHubButton>
</div>
</div>
<div className="col-span-6 md:col-span-2">
<div className="mb-4 font-medium">Links</div>
<div className="mb-4 font-medium">{t("links")}</div>
<ul>
<li>
<a
className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"
href="https://github.com/lukevella/rallly/discussions"
>
Forum
{t("discussions")}
</a>
</li>
<li>
<Link href="https://blog.rallly.co">
<a className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline">
Blog
{t("blog")}
</a>
</Link>
</li>
@ -68,20 +68,20 @@ const Footer: React.VoidFunctionComponent = () => {
href="https://support.rallly.co"
className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline"
>
Support
{t("support")}
</a>
</li>
<li>
<Link href="/privacy-policy">
<a className="font-normal leading-loose text-gray-400 hover:text-gray-800 hover:no-underline">
Privacy Policy
{t("privacyPolicy")}
</a>
</Link>
</li>
</ul>
</div>
<div className="col-span-6 md:col-span-2">
<div className="mb-4 font-medium">Follow</div>
<div className="mb-4 font-medium">{t("follow")}</div>
<ul>
<li>
<a
@ -107,7 +107,7 @@ const Footer: React.VoidFunctionComponent = () => {
className="inline-block text-white"
>
<span className="mb-1 inline-block w-full text-right text-xs italic text-gray-400">
Powered by
{t("poweredBy")}
</span>
<Vercel className="w-24" />
</a>

View file

@ -1,5 +1,6 @@
import { Participant, Vote, VoteType } from "@prisma/client";
import { keyBy } from "lodash";
import { useTranslation } from "next-i18next";
import React from "react";
import Trash from "@/components/icons/trash.svg";
@ -57,6 +58,7 @@ export const PollContextProvider: React.VoidFunctionComponent<{
admin: boolean;
children?: React.ReactNode;
}> = ({ poll, urlId, admin, children }) => {
const { t } = useTranslation("app");
const { participants } = useParticipants();
const [isDeleted, setDeleted] = React.useState(false);
const { user } = useSession();
@ -176,8 +178,8 @@ export const PollContextProvider: React.VoidFunctionComponent<{
return (
<ErrorPage
icon={Trash}
title="Deleted poll"
description="This poll doesn't exist anymore."
title={t("deletedPoll")}
description={t("deletedPollInfo")}
/>
);
}

View file

@ -52,7 +52,7 @@ const PollPage: NextPage = () => {
const verifyEmail = trpc.useMutation(["polls.verification.verify"], {
onSuccess: () => {
toast.success("Your poll has been verified");
toast.success(t("pollHasBeenVerified"));
queryClient.setQueryData(["polls.get", { urlId, admin }], {
...poll,
verified: true,
@ -61,7 +61,7 @@ const PollPage: NextPage = () => {
plausible("Verified email");
},
onError: () => {
toast.error("Your link has expired or is no longer valid");
toast.error(t("linkHasExpired"));
},
onSettled: () => {
router.replace(`/admin/${router.query.urlId}`, undefined, {
@ -83,7 +83,7 @@ const PollPage: NextPage = () => {
{ urlId: urlId, notifications: false },
{
onSuccess: () => {
toast.success("Notifications have been disabled");
toast.success(t("notificationsDisabled"));
plausible("Unsubscribed from notifications");
},
},
@ -92,7 +92,7 @@ const PollPage: NextPage = () => {
shallow: true,
});
}
}, [plausible, urlId, router, updatePollMutation]);
}, [plausible, urlId, router, updatePollMutation, t]);
const checkIfWideScreen = () => window.innerWidth > 640;
@ -146,7 +146,7 @@ const PollPage: NextPage = () => {
setSharingVisible((value) => !value);
}}
>
Share
{t("share")}
</Button>
</div>
<AnimatePresence initial={false}>
@ -196,11 +196,10 @@ const PollPage: NextPage = () => {
{!poll.admin && poll.adminUrlId ? (
<div className="mb-4 items-center justify-between rounded-lg px-4 md:flex md:space-x-4 md:border md:p-2 md:pl-4">
<div className="mb-4 font-medium md:mb-0">
Hey {poll.user.name}, looks like you are the owner of this
poll.
{t("pollOwnerNotice", { name: poll.user.name })}
</div>
<a href={`/admin/${poll.adminUrlId}`} className="btn-default">
Go to admin &rarr;
{t("goToAdmin")} &rarr;
</a>
</div>
) : null}
@ -210,7 +209,7 @@ const PollPage: NextPage = () => {
<LockClosed className="w-6" />
</div>
<div>
<div className="font-medium">This poll has been locked</div>
<div className="font-medium">{t("pollHasBeenLocked")}</div>
</div>
</div>
) : null}
@ -243,22 +242,26 @@ const PollPage: NextPage = () => {
) : null}
<div>
<div className="mb-2 text-sm text-slate-500">
Possible answers
{t("possibleAnswers")}
</div>
<div className="flex items-center space-x-3">
<span className="inline-flex items-center space-x-1">
<VoteIcon type="yes" />
<span className="text-xs text-slate-500">Yes</span>
<span className="text-xs text-slate-500">
{t("yes")}
</span>
</span>
<span className="inline-flex items-center space-x-1">
<VoteIcon type="ifNeedBe" />
<span className="text-xs text-slate-500">
If need be
{t("ifNeedBe")}
</span>
</span>
<span className="inline-flex items-center space-x-1">
<VoteIcon type="no" />
<span className="text-xs text-slate-500">No</span>
<span className="text-xs text-slate-500">
{t("no")}
</span>
</span>
</div>
</div>
@ -269,7 +272,9 @@ const PollPage: NextPage = () => {
</React.Suspense>
</div>
<React.Suspense fallback={<div className="p-4">Loading</div>}>
<React.Suspense
fallback={<div className="p-4">{t("loading")}</div>}
>
<Discussion />
</React.Suspense>
</div>

View file

@ -1,4 +1,5 @@
import clsx from "clsx";
import { useTranslation } from "next-i18next";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
@ -27,6 +28,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
HTMLFormElement,
ParticipantRowFormProps
> = ({ defaultValues, onSubmit, className, onCancel }, ref) => {
const { t } = useTranslation("app");
const {
columnWidth,
scrollPosition,
@ -168,7 +170,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
loading={isSubmitting}
data-testid="submitNewParticipant"
>
Save
{t("save")}
</Button>
) : null}
{scrollPosition < maxScrollPosition ? (
@ -178,7 +180,7 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
goToNextPage();
}}
>
Next &rarr;
{t("next")} &rarr;
</Button>
) : null}
{onCancel ? <CompactButton onClick={onCancel} icon={X} /> : null}

View file

@ -55,13 +55,13 @@ const ManagePoll: React.VoidFunctionComponent<{
openChangeOptionsModal,
closeChangeOptionsModal,
] = useModal({
okText: "Save",
okText: t("save"),
okButtonProps: {
form: "pollOptions",
htmlType: "submit",
loading: isUpdating,
},
cancelText: "Cancel",
cancelText: t("cancel"),
content: (
<React.Suspense fallback={null}>
<PollOptionsForm
@ -116,7 +116,7 @@ const ManagePoll: React.VoidFunctionComponent<{
if (optionsToDeleteThatHaveVotes.length > 0) {
modalContext.render({
title: "Are you sure?",
title: t("areYouSure"),
description: (
<Trans
t={t}
@ -128,8 +128,8 @@ const ManagePoll: React.VoidFunctionComponent<{
okButtonProps: {
type: "danger",
},
okText: "Delete",
cancelText: "Cancel",
okText: t("delete"),
cancelText: t("cancel"),
});
} else {
onOk();
@ -145,13 +145,13 @@ const ManagePoll: React.VoidFunctionComponent<{
openChangePollDetailsModa,
closePollDetailsModal,
] = useModal({
okText: "Save changes",
okText: t("save"),
okButtonProps: {
form: "updateDetails",
loading: isUpdating,
htmlType: "submit",
},
cancelText: "Cancel",
cancelText: t("cancel"),
content: (
<PollDetailsForm
name="updateDetails"
@ -181,31 +181,35 @@ const ManagePoll: React.VoidFunctionComponent<{
>
<DropdownItem
icon={Pencil}
label="Edit details"
label={t("editDetails")}
onClick={openChangePollDetailsModa}
/>
<DropdownItem
icon={Table}
label="Edit options"
label={t("editOptions")}
onClick={handleChangeOptions}
/>
<DropdownItem icon={Save} label="Export to CSV" onClick={exportToCsv} />
<DropdownItem
icon={Save}
label={t("exportToCsv")}
onClick={exportToCsv}
/>
{poll.closed ? (
<DropdownItem
icon={LockOpen}
label="Unlock poll"
label={t("unlockPoll")}
onClick={() => updatePollMutation({ urlId, closed: false })}
/>
) : (
<DropdownItem
icon={LockClosed}
label="Lock poll"
label={t("lockPoll")}
onClick={() => updatePollMutation({ urlId, closed: true })}
/>
)}
<DropdownItem
icon={Trash}
label="Delete poll"
label={t("deletePoll")}
onClick={() => {
modalContext.render({
overlayClosable: true,

View file

@ -26,7 +26,7 @@ const NotificationsToggle: React.VoidFunctionComponent = () => {
poll.notifications ? (
<div>
<div className="font-medium text-primary-300">
Notifications are on
{t("notificationsOn")}
</div>
<div className="max-w-sm">
<Trans
@ -44,10 +44,10 @@ const NotificationsToggle: React.VoidFunctionComponent = () => {
</div>
</div>
) : (
"Notifications are off"
t("notificationsOff")
)
) : (
"You need to verify your email to turn on notifications"
t("notificationsVerifyEmail")
)
}
>

View file

@ -22,7 +22,9 @@ const Preferences: React.VoidFunctionComponent = () => {
</div>
<div className="grow">
<div className="mb-2">
<div className="mb-2 grow text-sm text-slate-500">Week starts on</div>
<div className="mb-2 grow text-sm text-slate-500">
{t("weekStartsOn")}
</div>
<div>
<div className="segment-button inline-flex">
<button
@ -61,7 +63,9 @@ const Preferences: React.VoidFunctionComponent = () => {
</div>
</div>
<div className="mb-2">
<div className="mb-2 grow text-sm text-slate-500">Time format</div>
<div className="mb-2 grow text-sm text-slate-500">
{t("timeFormat")}
</div>
<div className="segment-button inline-flex">
<button
className={clsx({

View file

@ -26,7 +26,7 @@ export const Profile: React.VoidFunctionComponent = () => {
return (
<div className="card my-4 p-0">
<Head>
<title>Profile - Login</title>
<title>{t("profileLogin")}</title>
</Head>
<LoginForm />
</div>
@ -36,7 +36,11 @@ export const Profile: React.VoidFunctionComponent = () => {
return (
<div className="mx-auto max-w-3xl py-4 lg:mx-0">
<Head>
<title>Profile - {user.name}</title>
<title>
{t("profileUser", {
username: user.name,
})}
</title>
</Head>
<div className="mb-4 flex items-center px-4">
<div className="mr-4 inline-flex h-14 w-14 items-center justify-center rounded-lg bg-primary-50">
@ -50,7 +54,7 @@ export const Profile: React.VoidFunctionComponent = () => {
{user.shortName}
</div>
<div className="text-slate-500">
{user.isGuest ? "Guest" : "User"}
{user.isGuest ? t("guest") : t("user")}
</div>
</div>
</div>
@ -92,7 +96,7 @@ export const Profile: React.VoidFunctionComponent = () => {
</div>
</div>
) : (
<EmptyState icon={Pencil} text="No polls created" />
<EmptyState icon={Pencil} text={t("pollsEmpty")} />
)}
</div>
) : null}

View file

@ -32,13 +32,13 @@ const Sharing: React.VoidFunctionComponent<SharingProps> = ({
<div className={clsx("card p-4", className)}>
<div className="mb-1 flex items-center justify-between">
<div className="text-lg font-semibold text-slate-700">
Share via link
{t("shareLink")}
</div>
<button
onClick={onHide}
className="h-8 items-center justify-center rounded-md px-3 text-slate-400 transition-colors hover:bg-slate-500/10 hover:text-slate-500 active:bg-slate-500/20"
>
Hide
{t("hide")}
</button>
</div>
<div className="mb-4 text-slate-600">
@ -71,7 +71,7 @@ const Sharing: React.VoidFunctionComponent<SharingProps> = ({
}}
className="md:absolute md:top-1/2 md:right-3 md:-translate-y-1/2"
>
{didCopy ? "Copied" : "Copy Link"}
{didCopy ? t("copied") : t("copyLink")}
</Button>
</div>
</div>

View file

@ -1,6 +1,7 @@
import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import React from "react";
import Menu from "@/components/icons/menu.svg";
@ -40,6 +41,7 @@ const MobileNavigation: React.VoidFunctionComponent<{
openLoginModal: () => void;
}> = ({ openLoginModal }) => {
const { user } = useSession();
const { t } = useTranslation("app");
return (
<div
className="fixed top-0 z-40 flex h-12 w-full shrink-0 items-center justify-between border-b bg-gray-50
@ -55,7 +57,7 @@ const MobileNavigation: React.VoidFunctionComponent<{
className="flex w-full cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
>
<Login className="h-5 opacity-75" />
<span className="inline-block">Login</span>
<span className="inline-block">{t("login")}</span>
</button>
)}
<AnimatePresence initial={false}>
@ -93,7 +95,7 @@ const MobileNavigation: React.VoidFunctionComponent<{
className="group flex items-center whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
>
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500" />
<span className="ml-2 hidden sm:block">Preferences</span>
<span className="ml-2 hidden sm:block">{t("preferences")}</span>
</button>
}
>
@ -107,7 +109,7 @@ const MobileNavigation: React.VoidFunctionComponent<{
className="group flex items-center rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
>
<Menu className="w-5 group-hover:text-primary-500" />
<span className="ml-2 hidden sm:block">Menu</span>
<span className="ml-2 hidden sm:block">{t("menu")}</span>
</button>
}
>
@ -121,12 +123,13 @@ const MobileNavigation: React.VoidFunctionComponent<{
const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
className,
}) => {
const { t } = useTranslation("app");
return (
<div className={clsx("space-y-1", className)}>
<Link href="/new">
<a className="flex cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 pr-4 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300">
<Pencil className="h-5 opacity-75 " />
<span className="inline-block">New Poll</span>
<span className="inline-block">{t("newPoll")}</span>
</a>
</Link>
<a
@ -136,7 +139,7 @@ const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
rel="noreferrer"
>
<Support className="h-5 opacity-75" />
<span className="inline-block">Support</span>
<span className="inline-block">{t("support")}</span>
</a>
</div>
);
@ -146,6 +149,7 @@ const UserDropdown: React.VoidFunctionComponent<
DropdownProps & { openLoginModal: () => void }
> = ({ children, openLoginModal, ...forwardProps }) => {
const { logout, user } = useSession();
const { t } = useTranslation("app");
const modalContext = useModalContext();
if (!user) {
return null;
@ -175,17 +179,14 @@ const UserDropdown: React.VoidFunctionComponent<
</div>
</div>
</div>
<p>
You are using a guest session. This allows us to recognize
you if you come back later so you can edit your votes.
</p>
<p>{t("guestSessionNotice")}</p>
<div>
<a
href="https://support.rallly.co/guest-sessions"
target="_blank"
rel="noreferrer"
>
Read more about guest sessions.
{t("guestSessionReadMore")}
</a>
</div>
</div>
@ -204,20 +205,19 @@ const UserDropdown: React.VoidFunctionComponent<
) : null}
<DropdownItem
icon={Logout}
label={user.isGuest ? "Forget me" : "Logout"}
label={user.isGuest ? t("forgetMe") : t("logout")}
onClick={() => {
if (user?.isGuest) {
modalContext.render({
title: "Are you sure?",
description:
"Once a guest session ends it cannot be resumed. You will not be able to edit any votes or comments you've made with this session.",
title: t("areYouSure"),
description: t("endingGuestSessionNotice"),
onOk: logout,
okButtonProps: {
type: "danger",
},
okText: "End session",
cancelText: "Cancel",
okText: t("endSession"),
cancelText: t("cancel"),
});
} else {
logout();
@ -232,6 +232,7 @@ const StandardLayout: React.VoidFunctionComponent<{
children?: React.ReactNode;
}> = ({ children, ...rest }) => {
const { user } = useSession();
const { t } = useTranslation("app");
const [loginModal, openLoginModal] = useModal({
footer: null,
overlayClosable: true,
@ -255,7 +256,7 @@ const StandardLayout: React.VoidFunctionComponent<{
<Link href="/new">
<a className="group mb-1 flex items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
<Pencil className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">New Poll</span>
<span className="grow text-left">{t("newPoll")}</span>
</a>
</Link>
<a
@ -265,14 +266,14 @@ const StandardLayout: React.VoidFunctionComponent<{
rel="noreferrer"
>
<Support className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">Support</span>
<span className="grow text-left">{t("support")}</span>
</a>
<Popover
placement="right-start"
trigger={
<button className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">Preferences</span>
<span className="grow text-left">{t("preferences")}</span>
<DotsVertical className="h-4 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100" />
</button>
}
@ -285,7 +286,7 @@ const StandardLayout: React.VoidFunctionComponent<{
className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20"
>
<Login className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
<span className="grow text-left">Login</span>
<span className="grow text-left">{t("login")}</span>
</button>
)}
</div>
@ -311,7 +312,7 @@ const StandardLayout: React.VoidFunctionComponent<{
{user.shortName}
</div>
<div className="truncate text-xs text-slate-500">
{user.isGuest ? "Guest" : "User"}
{user.isGuest ? t("guest") : t("user")}
</div>
</div>
<DotsVertical className="h-4 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100" />
@ -343,16 +344,16 @@ const StandardLayout: React.VoidFunctionComponent<{
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
rel="noreferrer"
>
Support
{t("support")}
</a>
<Link href="https://github.com/lukevella/rallly/discussions">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
Discussions
{t("discussions")}
</a>
</Link>
<Link href="https://blog.rallly.co">
<a className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline">
Blog
{t("blog")}
</a>
</Link>
<div className="hidden text-slate-300 lg:block">&bull;</div>
@ -373,7 +374,7 @@ const StandardLayout: React.VoidFunctionComponent<{
<Link href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E">
<a className="inline-flex h-8 items-center rounded-full bg-slate-100 pl-2 pr-3 text-sm text-slate-400 transition-colors hover:bg-primary-500 hover:text-white hover:no-underline focus:ring-2 focus:ring-primary-500 focus:ring-offset-1 active:bg-primary-600">
<Cash className="mr-1 inline-block w-5" />
<span>Donate</span>
<span>{t("donate")}</span>
</a>
</Link>
</div>