diff --git a/next-i18next.config.js b/next-i18next.config.js
index 34fb8cdc8..f672d2625 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", "es", "de", "fr", "sv"],
+ locales: ["en", "es", "de", "fr", "sv", "pt-BR"],
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..3f61b1d53 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -11,6 +11,7 @@
"italian": "Italian",
"language": "Language",
"links": "Links",
+ "portugueseBr": "Portuguese (Brazil)",
"poweredBy": "Powered by",
"privacyPolicy": "Privacy Policy",
"spanish": "Spanish",
diff --git a/public/locales/pt-BR/app.json b/public/locales/pt-BR/app.json
new file mode 100644
index 000000000..b73182304
--- /dev/null
+++ b/public/locales/pt-BR/app.json
@@ -0,0 +1,128 @@
+{
+ "12h": "12-horas",
+ "24h": "24-horas",
+ "addParticipant": "Adicionar participante",
+ "addTimeOption": "Adicionar opção de horário",
+ "alreadyVoted": "Você já votou",
+ "applyToAllDates": "Aplicar a todas as datas",
+ "areYouSure": "Você tem certeza?",
+ "back": "Voltar",
+ "calendarHelp": "Você não pode criar uma enquete sem quaisquer opções. Adicione pelo menos uma opção para continuar.",
+ "calendarHelpTitle": "Esqueceu algo?",
+ "cancel": "Cancelar",
+ "comment": "Comentar",
+ "commentPlaceholder": "Deixe um comentário nesta enquete (visível para todos)",
+ "comments": "Comentários",
+ "continue": "Continuar",
+ "copied": "Copiado",
+ "copyLink": "Copiar link",
+ "createdBy": "por {{name}}",
+ "createPoll": "Criar enquete",
+ "creatingDemo": "Criando enquete de demonstração…",
+ "delete": "Excluir",
+ "deleteComment": "Apagar comentário",
+ "deleteDate": "Excluir data",
+ "deletedPoll": "Enquete excluída",
+ "deletedPollInfo": "Esta enquete não existe mais.",
+ "deletePoll": "Excluir enquete",
+ "deletePollDescription": "Todos os dados relacionados a esta enquete serão excluídos. Para confirmar, digite “{{confirmText}}” no espaço abaixo:",
+ "deletingOptionsWarning": "Você está excluindo opções que outros participantes já votaram. Esses votos serão excluídos também.",
+ "demoPollNotice": "Enquetes de demonstração são excluídas automaticamente após 1 dia",
+ "description": "Descrição",
+ "descriptionPlaceholder": "Olá a todos! Por gentileza, escolha as datas que sirva para você!",
+ "donate": "Doe",
+ "editDetails": "Editar detalhes",
+ "editOptions": "Editar opções",
+ "email": "E-mail",
+ "emailPlaceholder": "fulano@email.com.br",
+ "endingGuestSessionNotice": "Uma vez que uma sessão de convidado termine, ela não poderá ser retomada. Você não poderá editar nenhum voto ou comentário que tenha feito nesta sessão.",
+ "endSession": "Encerrar sessão",
+ "errorCreate": "Ops! Houve um problema ao criar sua enquete. O erro foi registrado e tentaremos corrigi-lo.",
+ "exportToCsv": "Exportar para CSV",
+ "finish": "Concluir",
+ "forgetMe": "Esqueça-me",
+ "goToAdmin": "Ir para Admin",
+ "guest": "Convidado",
+ "guestSessionNotice": "Você está usando uma sessão de convidado. Isso nos permite reconhecê-lo caso você voltar mais tarde e poder editar seus votos.",
+ "guestSessionReadMore": "Leia mais sobre sessões de convidado.",
+ "hide": "Ocultar",
+ "ifNeedBe": "Se for necessário",
+ "linkHasExpired": "Seu link expirou ou não é mais válido",
+ "loading": "Carregando…",
+ "loadingParticipants": "Carregando participantes…",
+ "location": "Local",
+ "locationPlaceholder": "Loja de Café do Júlio",
+ "lockPoll": "Bloquear enquete",
+ "login": "Logar",
+ "loginCheckInbox": "Por gentileza, verifique sua caixa de entrada.",
+ "loginMagicLinkSent": "Um link mágico foi enviado para:",
+ "loginSendMagicLink": "Envie-me um link mágico",
+ "loginViaMagicLink": "Login via link mágico",
+ "loginViaMagicLinkDescription": "Enviaremos um e-mail com um link mágico que você possa usar para logar.",
+ "loginWithValidEmail": "Por favor, insira um endereço de e-mail válido",
+ "logout": "Deslogar",
+ "manage": "Gerenciar",
+ "menu": "Menu",
+ "mixedOptionsDescription": "Você não pode ter ambas as opções de data e hora na mesma enquete. Qual você gostaria de manter?",
+ "mixedOptionsKeepDates": "Manter opções de data",
+ "mixedOptionsKeepTimes": "Manter opções de hora",
+ "mixedOptionsTitle": "Aguarde um minuto…🤔",
+ "monday": "Segunda-feira",
+ "monthView": "Visão mensal",
+ "name": "Nome",
+ "namePlaceholder": "Fulano de Tal",
+ "newPoll": "Nova enquete",
+ "next": "Avançar",
+ "nextMonth": "Próximo mês",
+ "no": "Não",
+ "noDatesSelected": "Nenhuma data selecionada",
+ "notificationsDisabled": "As notificações foram desabilitadas",
+ "notificationsOff": "Notificações desativadas",
+ "notificationsOn": "Notificações ativadas",
+ "notificationsOnDescription": "Um e-mail será enviado para {{email}} quando houver atividade nesta enquete.",
+ "notificationsVerifyEmail": "Você precisa confirmar seu e-mail para ativar as notificações",
+ "ok": "Ok",
+ "options": "Opções",
+ "participant": "Participante",
+ "participantCount_other": "{{count}} participantes",
+ "participantCount": "{{count}} participante",
+ "pollHasBeenLocked": "Esta enquete foi bloqueada",
+ "pollHasBeenVerified": "Sua enquete foi verificada",
+ "pollOwnerNotice": "Oi {{name}}, parece que você é o proprietário desta enquete.",
+ "pollsEmpty": "Nenhuma enquete criada",
+ "possibleAnswers": "Possíveis respostas",
+ "preferences": "Preferências",
+ "previousMonth": "Mês anterior",
+ "profileLogin": "Perfil - Login",
+ "profileUser": "Perfil - {{username}}",
+ "requiredNameError": "Nome é obrigatório",
+ "save": "Salvar",
+ "saveInstruction": "Selecione sua disponibilidade e clique {{save}}",
+ "share": "Compartilhar",
+ "shareDescription": "Dê este link para os seus participantes para permitir que eles votem na sua enquete.",
+ "shareLink": "Compartilhar via link",
+ "specifyTimes": "Especificar horários",
+ "specifyTimesDescription": "Incluir os horários de início e fim para cada opção",
+ "stepSummary": "Passo {{current}} de {{total}}",
+ "sunday": "Domingo",
+ "timeAndDate": "Hora & data",
+ "timeFormat": "Formato de hora:",
+ "timeZone": "Fuso horário:",
+ "title": "Título",
+ "titlePlaceholder": "Reunião mensal",
+ "today": "Hoje",
+ "unlockPoll": "Desbloquear enquete",
+ "unverifiedMessage": "Um e-mail foi enviado para {{email}} com um link para verificar o endereço de e-mail.",
+ "user": "Usuário",
+ "vote": "Votar",
+ "voteCount_other": "{{count}} votos",
+ "voteCount": "{{count}} voto",
+ "weekStartsOn": "A semana começa em",
+ "weekView": "Visão semanal",
+ "whatsThis": "O que é isso?",
+ "yes": "Sim",
+ "you": "Você",
+ "yourDetails": "Seus detalhes",
+ "yourName": "Seu nome…",
+ "yourPolls": "Suas enquetes"
+}
diff --git a/public/locales/pt-BR/common.json b/public/locales/pt-BR/common.json
new file mode 100644
index 000000000..503672c64
--- /dev/null
+++ b/public/locales/pt-BR/common.json
@@ -0,0 +1,21 @@
+{
+ "blog": "Blog",
+ "discussions": "Discussões",
+ "donate": "Doe",
+ "english": "English",
+ "footerCredit": "Feito por @imlukevella",
+ "footerSponsor": "Este projeto é financiado pelo usuário. Por gentileza, considere apoiá-lo fazendo uma doação.",
+ "french": "Français",
+ "german": "Deutsch",
+ "home": "Início",
+ "italian": "L'italiano",
+ "language": "Idioma",
+ "links": "Links",
+ "poweredBy": "Desenvolvido por",
+ "privacyPolicy": "Política de Privacidade",
+ "spanish": "Español",
+ "starOnGithub": "Qualifique-nos no GitHub",
+ "support": "Suporte",
+ "swedish": "Svenska",
+ "volunteerTranslator": "Ajude a traduzir esta página"
+}
diff --git a/public/locales/pt-BR/errors.json b/public/locales/pt-BR/errors.json
new file mode 100644
index 000000000..bfcad457e
--- /dev/null
+++ b/public/locales/pt-BR/errors.json
@@ -0,0 +1,6 @@
+{
+ "notFoundTitle": "404 Página não encontrada",
+ "notFoundDescription": "Não conseguimos encontrar a página que você está procurando.",
+ "goToHome": "Ir para o início",
+ "startChat": "Iniciar chat"
+}
diff --git a/public/locales/pt-BR/homepage.json b/public/locales/pt-BR/homepage.json
new file mode 100644
index 000000000..4c0657b8f
--- /dev/null
+++ b/public/locales/pt-BR/homepage.json
@@ -0,0 +1,36 @@
+{
+ "3Ls": "Sim—com 3 Ls",
+ "adFree": "Sem anúncios",
+ "adFreeDescription": "Você pode dar um descanso ao seu bloqueador de anúncios — Você não vai precisar dele aqui.",
+ "comments": "Comentários",
+ "commentsDescription": "Participantes podem comentar na sua enquete e os comentários ficarão visíveis para todos.",
+ "features": "Funcionalidades",
+ "featuresSubheading": "Agendamento, da maneira inteligente",
+ "follow": "Seguir",
+ "getStarted": "Comece agora",
+ "heroSubText": "Encontre o dia certo sem contratempos",
+ "heroText": "Agende
reuniões
com facilidade",
+ "links": "Links",
+ "liveDemo": "Demonstração ao vivo",
+ "metaDescription": "Crie enquetes e vote para encontrar o melhor dia ou hora. Uma alternativa gratuita ao Doodle.",
+ "metaTitle": "Rallly - Agende reuniões de grupo",
+ "mobileFriendly": "Compatível com dispositivos móveis",
+ "mobileFriendlyDescription": "Funciona muito bem em dispositivos móveis para que os participantes possam responder às enquetes onde estiverem.",
+ "new": "Novo",
+ "noLoginRequired": "Não requer login",
+ "noLoginRequiredDescription": "Você não precisa fazer login para criar ou participar de uma enquete",
+ "notifications": "Notificações",
+ "notificationsDescription": "Saiba quem respondeu. Seja notificado quando os participantes votarem ou comentarem na sua enquete.",
+ "openSource": "Código aberto",
+ "openSourceDescription": "O projeto é totalmente de código aberto e disponível no GitHub.",
+ "participant": "Participante",
+ "participantCount_other": "{{count}} participantes",
+ "participantCount": "{{count}} participante",
+ "perfect": "Perfeito!",
+ "principles": "Princípios",
+ "principlesSubheading": "Não somos como os outros",
+ "selfHostable": "Auto-hospedável",
+ "selfHostableDescription": "Execute em seu próprio servidor para ter controle total dos seus dados",
+ "timeSlots": "Intervalos de tempo",
+ "timeSlotsDescription": "Defina os horários de início e fim individuais para cada opção em sua enquete. Os horários podem ser automaticamente ajustados ao fuso horário de cada participante ou podem ser definidos para ignorar completamente o fuso horário."
+}
diff --git a/src/components/poll/language-selector.tsx b/src/components/poll/language-selector.tsx
index a993a0aeb..f32cbf351 100644
--- a/src/components/poll/language-selector.tsx
+++ b/src/components/poll/language-selector.tsx
@@ -20,8 +20,9 @@ export const LanguageSelect: React.VoidFunctionComponent<{
>
-
+
+
);
diff --git a/src/middleware.ts b/src/middleware.ts
index 30cf2de7d..305c6ed70 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
-const supportedLocales = ["en", "es", "de", "fr", "sv"];
+const supportedLocales = ["en", "es", "de", "fr", "pt-BR", "sv"];
export function middleware({ headers, cookies, nextUrl }: NextRequest) {
- const locale =
+ const language =
cookies.get("NEXT_LOCALE") ??
(headers
.get("accept-language")
@@ -14,8 +14,11 @@ export function middleware({ headers, cookies, nextUrl }: NextRequest) {
const newUrl = nextUrl.clone();
- if (supportedLocales.includes(locale)) {
- newUrl.pathname = `/${locale}${newUrl.pathname}`;
+ if (supportedLocales.includes(language)) {
+ newUrl.pathname = `/${language}${newUrl.pathname}`;
+ } else if (language === "pt") {
+ // For now we send all portuguese language requests to pt-BR
+ newUrl.pathname = `/pt-BR${newUrl.pathname}`;
}
return NextResponse.rewrite(newUrl);
diff --git a/src/utils/dayjs.tsx b/src/utils/dayjs.tsx
index ff6564792..2fef89004 100644
--- a/src/utils/dayjs.tsx
+++ b/src/utils/dayjs.tsx
@@ -50,6 +50,11 @@ const dayjsLocales: Record<
timeFormat: "24h",
import: () => import("dayjs/locale/sv"),
},
+ "pt-BR": {
+ weekStartsOn: "sunday",
+ timeFormat: "24h",
+ import: () => import("dayjs/locale/pt-br"),
+ },
};
dayjs.extend(localizedFormat);
@@ -84,17 +89,17 @@ export const DayjsProvider: React.VoidFunctionComponent<{
// Using language instead of router.locale because when transitioning from homepage to
// the app via it will be set to "en" instead of the current locale.
- const locale = i18n.language;
+ const localeConfig = dayjsLocales[i18n.language];
- const [weekStartsOn = dayjsLocales[locale].weekStartsOn, , setWeekStartsOn] =
+ const [weekStartsOn = localeConfig.weekStartsOn, , setWeekStartsOn] =
useLocalStorage("rallly-week-starts-on");
- const [timeFormat = dayjsLocales[locale].timeFormat, setTimeFormat] =
+ const [timeFormat = localeConfig.timeFormat, setTimeFormat] =
useLocalStorage("rallly-time-format");
const { value: dayjsLocale } = useAsync(async () => {
- return await dayjsLocales[locale ?? "en"].import();
- }, [locale]);
+ return await localeConfig.import();
+ }, [localeConfig]);
if (!dayjsLocale) {
// wait for locale to load before rendering content
@@ -103,19 +108,26 @@ export const DayjsProvider: React.VoidFunctionComponent<{
dayjs.locale({
...dayjsLocale,
- weekStart: weekStartsOn ? (weekStartsOn === "monday" ? 1 : 0) : undefined,
- formats: {
- ...dayjsLocale.formats,
- LT: timeFormat === "12h" ? "h:mm A" : "H:mm",
- },
+ weekStart: weekStartsOn
+ ? weekStartsOn === "monday"
+ ? 1
+ : 0
+ : dayjsLocale.weekStart,
+ formats:
+ localeConfig.timeFormat !== timeFormat
+ ? {
+ ...dayjsLocale.formats,
+ LT: timeFormat === "12h" ? "h:mm A" : "H:mm",
+ }
+ : dayjsLocale.formats,
});
return (