diff --git a/declarations/i18next.d.ts b/declarations/i18next.d.ts index 8fdf4fd0e..a25acbd1b 100644 --- a/declarations/i18next.d.ts +++ b/declarations/i18next.d.ts @@ -2,6 +2,7 @@ import "react-i18next"; import app from "~/public/locales/en/app.json"; import common from "~/public/locales/en/common.json"; +import errors from "~/public/locales/en/errors.json"; import homepage from "~/public/locales/en/homepage.json"; declare module "next-i18next" { @@ -9,5 +10,6 @@ declare module "next-i18next" { homepage: typeof homepage; app: typeof app; common: typeof common; + errors: typeof errors; } } diff --git a/next-i18next.config.js b/next-i18next.config.js index 09f3cfbd8..34fb8cdc8 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -3,7 +3,7 @@ const path = require("path"); module.exports = { i18n: { defaultLocale: "en", - locales: ["en", "de", "fr", "sv"], + locales: ["en", "es", "de", "fr", "sv"], localePath: path.resolve("./public/locales"), reloadOnPrerender: process.env.NODE_ENV === "development", }, diff --git a/public/locales/en/common.json b/public/locales/en/common.json index fbb0bfc38..cf129b88d 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -17,5 +17,9 @@ "starOnGithub": "Star us on Github", "support": "Support", "swedish": "Swedish", - "volunteerTranslator": "Help translate this site" + "volunteerTranslator": "Help translate this site", + "notFoundTitle": "404 not found", + "notFoundDescription": "We couldn't find the page you're looking for.", + "goToHome": "Go to home", + "startChat": "Start chat" } diff --git a/public/locales/en/errors.json b/public/locales/en/errors.json new file mode 100644 index 000000000..b267ef1db --- /dev/null +++ b/public/locales/en/errors.json @@ -0,0 +1,6 @@ +{ + "notFoundTitle": "404 not found", + "notFoundDescription": "We couldn't find the page you're looking for.", + "goToHome": "Go to home", + "startChat": "Start chat" +} diff --git a/public/locales/es/app.json b/public/locales/es/app.json new file mode 100644 index 000000000..9e472a68b --- /dev/null +++ b/public/locales/es/app.json @@ -0,0 +1,128 @@ +{ + "12h": "12h", + "24h": "24h", + "addParticipant": "Añadir participante", + "addTimeOption": "Añadir hora", + "alreadyVoted": "Ya has votado", + "applyToAllDates": "Aplicar a todas las fechas", + "areYouSure": "¿Estás seguro?", + "back": "Volver", + "calendarHelp": "No puedes crear una encuesta sin ninguna opción. Añada al menos una opción para continuar.", + "calendarHelpTitle": "¿Olvidaste algo?", + "cancel": "Cancelar", + "comment": "Comentario", + "commentPlaceholder": "Dejar un comentario en esta encuesta (visible para todos)", + "comments": "Comentarios", + "continue": "Continuar", + "copied": "Copiado", + "copyLink": "Copiar enlace", + "createdBy": "de {{name}}", + "createPoll": "Crear encuesta", + "creatingDemo": "Creando una encuesta de demostración…", + "delete": "Borrar", + "deleteComment": "Borrar comentario", + "deleteDate": "Borrar fecha", + "deletedPoll": "Encuesta borrada", + "deletedPollInfo": "Esta encuesta ya no existe.", + "deletePoll": "Borrar encuesta", + "deletePollDescription": "Todos los datos relacionados con esta encuesta se eliminarán. Para confirmar, por favor escribe “{{confirmText}}” en el campo siguiente:", + "deletingOptionsWarning": "Estás eliminando opciones por las que algunos participantes han votado. También se eliminarán sus votos.", + "demoPollNotice": "Las encuestas de demostración se eliminan automáticamente después de 1 día", + "description": "Descripción", + "descriptionPlaceholder": "¡Hola a todos, por favor elijan las fechas que les convengan!", + "donate": "Donar", + "editDetails": "Editar detalles", + "editOptions": "Editar opciones", + "email": "Correo electrónico", + "emailPlaceholder": "jessie.smith@email.com", + "endingGuestSessionNotice": "Una vez que finalices la sesión de visitante, no se puede reanudar. No podrás editar ningún voto o comentario que hayas hecho con esta sesión.", + "endSession": "Cerrar Sesión", + "errorCreate": "¡Oh! Hubo un problema al crear tu encuesta. El error ha sido registrado y vamos a intentar arreglarlo.", + "exportToCsv": "Exportar a CSV", + "finish": "Finalizar", + "forgetMe": "Olvídame", + "goToAdmin": "Ir al panel de administración", + "guest": "Visitante", + "guestSessionNotice": "Estás usando una sesión de visitante. Esto nos permite reconocerte si vuelves más tarde para que puedas editar tus votos.", + "guestSessionReadMore": "Lee más sobre sesiones de visitantes.", + "hide": "Ocultar", + "ifNeedBe": "Si es necesario", + "linkHasExpired": "Tu enlace ha expirado o ya no es válido", + "loading": "Cargando…", + "loadingParticipants": "Cargando participantes…", + "location": "Ubicación", + "locationPlaceholder": "Café de Carlos", + "lockPoll": "Bloquear encuesta", + "login": "Iniciar sesión", + "loginCheckInbox": "Por favor, revisa tus correos electrónicos.", + "loginMagicLinkSent": "Se ha enviado un enlace mágico a:", + "loginSendMagicLink": "Enviarme un enlace mágico", + "loginViaMagicLink": "Iniciar sesión a través de un enlace mágico", + "loginViaMagicLinkDescription": "Te enviaremos un correo electrónico con un enlace mágico que puedes usar para iniciar sesión.", + "loginWithValidEmail": "Por favor ingresa un correo electrónico válido", + "logout": "Cerrar sesión", + "manage": "Gestionar", + "menu": "Menú", + "mixedOptionsDescription": "No puedes tener opciones de fecha y opciones de hora en la misma encuesta. ¿Cuáles quieres mantener?", + "mixedOptionsKeepDates": "Mantener las opciones de fecha", + "mixedOptionsKeepTimes": "Mantener las opciones de hora", + "mixedOptionsTitle": "Aguanta un minuto…🤔", + "monday": "Lunes", + "monthView": "Vista mensual", + "name": "Nombre", + "namePlaceholder": "Jessie Smith", + "newPoll": "Nueva encuesta", + "next": "Siguiente", + "nextMonth": "Siguiente mes", + "no": "No", + "noDatesSelected": "Ninguna fecha seleccionada", + "notificationsDisabled": "Las notificaciones han sido desactivadas", + "notificationsOff": "Las notificaciones están desactivadas", + "notificationsOn": "Las notificaciones están activadas", + "notificationsOnDescription": "Vamos a mandar un correo electrónico a {{email}} cuando haya actividad en esta encuesta.", + "notificationsVerifyEmail": "Tienes que verificar tu correo electrónico para activar las notificaciones", + "ok": "Aceptar", + "options": "Opciones", + "participant": "Participante", + "participantCount_other": "{{count}} participantes", + "participantCount": "{{count}} participante", + "pollHasBeenLocked": "Esta encuesta ha sido bloqueada", + "pollHasBeenVerified": "Esta encuesta ha sido verificada", + "pollOwnerNotice": "Hola {{name}}, parece que eres el dueño de esta encuesta.", + "pollsEmpty": "Ninguna encuesta creada", + "possibleAnswers": "Respuestas posibles", + "preferences": "Ajustes", + "previousMonth": "Mes anterior", + "profileLogin": "Perfil - Iniciar sesión", + "profileUser": "Perfil - {{username}}", + "requiredNameError": "El nombre es obligatorio", + "save": "Guardar", + "saveInstruction": "Selecciona tu disponibilidad y haz clic en {{save}}", + "share": "Compartir", + "shareDescription": "Dale este enlace a tus participantes para permitirles votar en tu encuesta.", + "shareLink": "Compartir con un enlace", + "specifyTimes": "Especificar tiempos", + "specifyTimesDescription": "Incluir horas de inicio y fin para cada opción", + "stepSummary": "Paso {{current}} de {{total}}", + "sunday": "Domingo", + "timeAndDate": "Fecha y hora", + "timeFormat": "Formato de hora:", + "timeZone": "Zona horaria:", + "title": "Título", + "titlePlaceholder": "Reunión mensual", + "today": "Hoy", + "unlockPoll": "Desbloquear encuesta", + "unverifiedMessage": "Se ha enviado un correo electrónico a {{email}} con un enlace para verificar la dirección de correo electrónico.", + "user": "Usuario", + "vote": "Votar", + "voteCount_other": "{{count}} votos", + "voteCount": "{{count}} voto", + "weekStartsOn": "La semana comienza el", + "weekView": "Vista semanal", + "whatsThis": "¿Qué es esto?", + "yes": "Sí", + "you": "Tú", + "yourDetails": "Tus datos", + "yourName": "Tu nombre…", + "yourPolls": "Tus encuestas" +} diff --git a/public/locales/es/common.json b/public/locales/es/common.json new file mode 100644 index 000000000..1eda1e7df --- /dev/null +++ b/public/locales/es/common.json @@ -0,0 +1,21 @@ +{ + "blog": "Blog", + "discussions": "Discusiones", + "donate": "Donar", + "english": "Inglés", + "footerCredit": "Creado por @imlukevella", + "footerSponsor": "Este proyecto está financiado por los usuarios. Por favor, considera apoyarlo donando.", + "french": "Francés", + "german": "Alemán", + "home": "Inicio", + "italian": "Italiano", + "language": "Idioma", + "links": "Enlaces", + "poweredBy": "Con tecnología de", + "privacyPolicy": "Política de privacidad", + "spanish": "Español", + "starOnGithub": "Seguir el proyecto en GitHub", + "support": "Soporte", + "swedish": "Sueco", + "volunteerTranslator": "Ayuda a traducir esta página" +} diff --git a/public/locales/es/homepage.json b/public/locales/es/homepage.json new file mode 100644 index 000000000..a95b29fe9 --- /dev/null +++ b/public/locales/es/homepage.json @@ -0,0 +1,36 @@ +{ + "3Ls": "Sí—con 3 Ls", + "adFree": "Sin anuncios", + "adFreeDescription": "Puedes darle un descanso a tu bloqueador de anuncios — No lo necesitarás aquí.", + "comments": "Comentarios", + "commentsDescription": "Los participantes pueden comentar en tu encuesta y los comentarios serán visibles para todos.", + "features": "Características", + "featuresSubheading": "Programar reuniones, de una manera inteligente", + "follow": "Seguir", + "getStarted": "Empezar", + "heroSubText": "Encuentra la fecha correcta sin dar vueltas", + "heroText": "Programar
reuniones
con facilidad", + "links": "Enlaces", + "liveDemo": "Demostración en vivo", + "metaDescription": "Crea encuestas y vota para encontrar el mejor día o la mejor hora. Una alternativa gratuita a Doodle.", + "metaTitle": "Rallly - Programar reuniones", + "mobileFriendly": "Optimizado para dispositivos móviles", + "mobileFriendlyDescription": "Funciona muy bien en dispositivos móviles para que los participantes puedan responder a las encuestas dondequiera que estén.", + "new": "Nuevo", + "noLoginRequired": "No es necesario iniciar sesión", + "noLoginRequiredDescription": "No es necesario iniciar sesión para crear o participar en una encuesta", + "notifications": "Notificaciones", + "notificationsDescription": "Sabe quién ha respondido. Recibe notificaciones cuando los participantes voten o comenten en tu encuesta.", + "openSource": "Código abierto", + "openSourceDescription": "Ese proyecto es completamente de código abierto y disponible en GitHub.", + "participant": "Participante", + "participantCount_other": "{{count}} participantes", + "participantCount": "{{count}} participante", + "perfect": "¡Perfecto!", + "principles": "Principios", + "principlesSubheading": "No somos como los otros", + "selfHostable": "Autoalojable", + "selfHostableDescription": "Ejecútalo en tu propio servidor para tener el control total de tus datos", + "timeSlots": "Intervalos de tiempo", + "timeSlotsDescription": "Establece la hora de inicio y fin individual para cada opción de tu encuesta. Los tiempos se pueden ajustar automáticamente a la zona horaria de cada participante o pueden ser ajustados para ignorar completamente las zonas horarias." +} diff --git a/src/components/discussion/discussion.tsx b/src/components/discussion/discussion.tsx index 2710524ac..20213bb69 100644 --- a/src/components/discussion/discussion.tsx +++ b/src/components/discussion/discussion.tsx @@ -1,11 +1,11 @@ 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"; +import { useDayjs } from "../../utils/dayjs"; import { requiredString } from "../../utils/form-validation"; import { trpc } from "../../utils/trpc"; import { Button } from "../button"; @@ -25,6 +25,7 @@ interface CommentForm { } const Discussion: React.VoidFunctionComponent = () => { + const { dayjs } = useDayjs(); const queryClient = trpc.useContext(); const { t } = useTranslation("app"); const { poll } = usePoll(); diff --git a/src/components/error-page.tsx b/src/components/error-page.tsx index 402c3424b..c43f07ba7 100644 --- a/src/components/error-page.tsx +++ b/src/components/error-page.tsx @@ -1,5 +1,6 @@ import Head from "next/head"; import Link from "next/link"; +import { useTranslation } from "next-i18next"; import * as React from "react"; import { Button } from "@/components/button"; @@ -19,6 +20,7 @@ const ErrorPage: React.VoidFunctionComponent = ({ title, description, }) => { + const { t } = useTranslation("errors"); return (
@@ -28,16 +30,16 @@ const ErrorPage: React.VoidFunctionComponent = ({
-
+
{title}

{description}

- Go to home + {t("goToHome")}
diff --git a/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx b/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx index 483130259..ed9a213e7 100644 --- a/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx +++ b/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx @@ -1,16 +1,14 @@ import clsx from "clsx"; -import dayjs from "dayjs"; import { useTranslation } from "next-i18next"; import { usePlausible } from "next-plausible"; import * as React from "react"; -import { usePreferences } from "@/components/preferences/use-preferences"; - import { expectTimeOption, getDateProps, removeAllOptionsForDay, } from "../../../../utils/date-time-utils"; +import { useDayjs } from "../../../../utils/dayjs"; import { Button } from "../../../button"; import CompactButton from "../../../compact-button"; import DateCard from "../../../date-card"; @@ -38,6 +36,7 @@ const MonthCalendar: React.VoidFunctionComponent = ({ duration, onChangeDuration, }) => { + const { dayjs, weekStartsOn } = useDayjs(); const { t } = useTranslation("app"); const isTimedEvent = options.some((option) => option.type === "timeSlot"); @@ -76,8 +75,6 @@ const MonthCalendar: React.VoidFunctionComponent = ({ ); }, [optionsByDay]); - const { weekStartsOn } = usePreferences(); - const datepicker = useHeadlessDatePicker({ selection: datepickerSelection, onNavigationChange: onNavigate, diff --git a/src/components/forms/poll-options-form/month-calendar/time-picker.tsx b/src/components/forms/poll-options-form/month-calendar/time-picker.tsx index 803cd024b..3f24cfdee 100644 --- a/src/components/forms/poll-options-form/month-calendar/time-picker.tsx +++ b/src/components/forms/poll-options-form/month-calendar/time-picker.tsx @@ -7,11 +7,11 @@ import { } from "@floating-ui/react-dom-interactions"; import { Listbox } from "@headlessui/react"; import clsx from "clsx"; -import dayjs from "dayjs"; import * as React from "react"; import { stopPropagation } from "@/utils/stop-propagation"; +import { useDayjs } from "../../../../utils/dayjs"; import ChevronDown from "../../../icons/chevron-down.svg"; import { styleMenuItem } from "../../../menu-styles"; @@ -28,6 +28,7 @@ const TimePicker: React.VoidFunctionComponent = ({ className, startFrom, }) => { + const { dayjs } = useDayjs(); const { reference, floating, x, y, strategy, refs } = useFloating({ strategy: "fixed", middleware: [ diff --git a/src/components/forms/poll-options-form/week-calendar.tsx b/src/components/forms/poll-options-form/week-calendar.tsx index 2c3c4d855..35ba4f84e 100644 --- a/src/components/forms/poll-options-form/week-calendar.tsx +++ b/src/components/forms/poll-options-form/week-calendar.tsx @@ -1,18 +1,15 @@ import clsx from "clsx"; -import dayjs from "dayjs"; import React from "react"; import { Calendar } from "react-big-calendar"; import { useMount } from "react-use"; import { getDuration } from "../../../utils/date-time-utils"; -import { usePreferences } from "../../preferences/use-preferences"; +import { useDayjs } from "../../../utils/dayjs"; import DateNavigationToolbar from "./date-navigation-toolbar"; import dayjsLocalizer from "./dayjs-localizer"; import { DateTimeOption, DateTimePickerProps } from "./types"; import { formatDateWithoutTime, formatDateWithoutTz } from "./utils"; -const localizer = dayjsLocalizer(dayjs); - const WeekCalendar: React.VoidFunctionComponent = ({ title, options, @@ -23,8 +20,9 @@ const WeekCalendar: React.VoidFunctionComponent = ({ onChangeDuration, }) => { const [scrollToTime, setScrollToTime] = React.useState(); + const { dayjs, timeFormat } = useDayjs(); + const localizer = React.useMemo(() => dayjsLocalizer(dayjs), [dayjs]); - const { timeFormat } = usePreferences(); useMount(() => { // Bit of a hack to force rbc to scroll to the right time when we close/open a modal setScrollToTime(dayjs(date).add(-60, "minutes").toDate()); diff --git a/src/components/headless-date-picker.tsx b/src/components/headless-date-picker.tsx index 958c4bd19..6b76b2fdc 100644 --- a/src/components/headless-date-picker.tsx +++ b/src/components/headless-date-picker.tsx @@ -1,6 +1,7 @@ -import dayjs from "dayjs"; import React from "react"; +import { useDayjs } from "../utils/dayjs"; + interface DayProps { date: Date; day: string; @@ -33,6 +34,7 @@ export const useHeadlessDatePicker = ( selection: Date[]; toggle: (date: Date) => void; } => { + const { dayjs } = useDayjs(); const [localSelection, setSelection] = React.useState([]); const selection = options?.selection ?? localSelection; const [localNavigationDate, setNavigationDate] = React.useState(today); diff --git a/src/components/home/hero.tsx b/src/components/home/hero.tsx index 51311a8fd..ba733e9ad 100644 --- a/src/components/home/hero.tsx +++ b/src/components/home/hero.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { Trans, useTranslation } from "next-i18next"; import * as React from "react"; +import { DayjsProvider } from "../../utils/dayjs"; import { UserAvatarProvider } from "../poll/user-avatar"; import PollDemo from "./poll-demo"; import ScribbleArrow from "./scribble-arrow.svg"; @@ -43,36 +44,38 @@ const Hero: React.VoidFunctionComponent = () => {
-
- - - {t("perfect")} 🤩 - - - - - -
+ +
+ + + {t("perfect")} 🤩 + + + + + +
+
diff --git a/src/components/home/poll-demo.tsx b/src/components/home/poll-demo.tsx index 1bbdc95dd..33aca5e18 100644 --- a/src/components/home/poll-demo.tsx +++ b/src/components/home/poll-demo.tsx @@ -1,7 +1,7 @@ -import dayjs from "dayjs"; import { useTranslation } from "next-i18next"; import * as React from "react"; +import { useDayjs } from "../../utils/dayjs"; import { ParticipantRowView } from "../poll/desktop-poll/participant-row"; import { ScoreSummary } from "../poll/score-summary"; @@ -35,6 +35,7 @@ const options = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"]; const PollDemo: React.VoidFunctionComponent = () => { const { t } = useTranslation("homepage"); + const { dayjs } = useDayjs(); return (
(() => { const highScore = poll.options.reduce((acc, curr) => { diff --git a/src/components/poll.tsx b/src/components/poll.tsx index 1b0195081..00198a718 100644 --- a/src/components/poll.tsx +++ b/src/components/poll.tsx @@ -27,7 +27,6 @@ import VoteIcon from "./poll/vote-icon"; import { usePoll } from "./poll-context"; import { useSession } from "./session"; import Sharing from "./sharing"; -import StandardLayout from "./standard-layout"; const Discussion = React.lazy(() => import("@/components/discussion")); @@ -120,159 +119,151 @@ const PollPage: NextPage = () => { ); return ( - -
- - {poll.title} - - -
- {admin ? ( - <> -
- - - +
+ + {isSharingVisible ? ( + - {t("share")} - + { + setSharingVisible(false); + }} + /> + + ) : null} + + {poll.verified === false ? ( +
+
- - {isSharingVisible ? ( - - { - setSharingVisible(false); - }} - /> - - ) : null} - - {poll.verified === false ? ( -
- + ) : null} + + ) : null} + {!poll.admin && poll.adminUrlId ? ( +
+
+ {t("pollOwnerNotice", { name: poll.user.name })} +
+ + {t("goToAdmin")} → + +
+ ) : null} + {poll.closed ? ( +
+
+ +
+
+
{t("pollHasBeenLocked")}
+
+
+ ) : null} +
+
+
+
+
+ {preventWidows(poll.title)} +
+ +
+ {poll.description ? ( +
+ + {preventWidows(poll.description)} + +
+ ) : null} + {poll.location ? ( +
+
+ {t("location")} +
+ {poll.location}
) : null} - - ) : null} - {!poll.admin && poll.adminUrlId ? ( -
-
- {t("pollOwnerNotice", { name: poll.user.name })} -
- - {t("goToAdmin")} → - -
- ) : null} - {poll.closed ? ( -
-
- -
-
{t("pollHasBeenLocked")}
-
-
- ) : null} -
-
-
-
-
- {preventWidows(poll.title)} -
- +
+ {t("possibleAnswers")}
- {poll.description ? ( -
- - {preventWidows(poll.description)} - -
- ) : null} - {poll.location ? ( -
-
- {t("location")} -
- {poll.location} -
- ) : null} -
-
- {t("possibleAnswers")} -
-
- - - - {t("yes")} - +
+ + + {t("yes")} + + + + + {t("ifNeedBe")} - - - - {t("ifNeedBe")} - - - - - - {t("no")} - - -
+
+ + + {t("no")} +
- - {participants ? : null} -
- - {t("loading")}
} - > - + + {participants ? : null}
+ + {t("loading")}
}> + +
- +
); }; diff --git a/src/components/poll/language-selector.tsx b/src/components/poll/language-selector.tsx index 4256fb0bb..a993a0aeb 100644 --- a/src/components/poll/language-selector.tsx +++ b/src/components/poll/language-selector.tsx @@ -1,18 +1,16 @@ import clsx from "clsx"; import Cookies from "js-cookie"; -import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; export const LanguageSelect: React.VoidFunctionComponent<{ className?: string; onChange?: (language: string) => void; }> = ({ className, onChange }) => { - const { t } = useTranslation("common"); - const router = useRouter(); + const { t, i18n } = useTranslation("common"); return (