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 (