diff --git a/app/models/enum/locale.go b/app/models/enum/locale.go index 4bf9b7dd6..aa99b85ad 100644 --- a/app/models/enum/locale.go +++ b/app/models/enum/locale.go @@ -39,6 +39,16 @@ var ( IsRTL: false, } + // LocalePortuguesePT represents Portuguese (Portugal) + LocalePortuguesePT = Locale{ + Code: "pt-PT", + Name: "Portuguese (Portugal)", + MessageFormatCode: "pt", + PostgresConfig: "portuguese", + LinguaLanguage: lingua.Portuguese, + IsRTL: false, + } + // LocaleSpanishES represents Spanish LocaleSpanishES = Locale{ Code: "es-ES", @@ -203,6 +213,7 @@ var ( AllLocales = []Locale{ LocaleEnglish, LocalePortugueseBR, + LocalePortuguesePT, LocaleSpanishES, LocaleGerman, LocaleFrench, diff --git a/app/services/email/email_test.go b/app/services/email/email_test.go index 9756d49c9..f1d6f0905 100644 --- a/app/services/email/email_test.go +++ b/app/services/email/email_test.go @@ -10,6 +10,20 @@ import ( . "github.com/getfider/fider/app/pkg/assert" ) +func TestRenderMessage_SubjectUnescapesHTMLEntities(t *testing.T) { + RegisterT(t) + + // Reproducer for the issue where characters like '+', '"', '&', '<', '>' + // in the rendered subject ended up as their numeric HTML entities + // (e.g. "+", """) because the subject is rendered through + // html/template along with the body. SMTP headers are plain text and + // must contain the literal characters. + message := email.RenderMessage(context.Background(), "echo_test", email.NoReply, dto.Props{ + "name": `Encontrar+se "quoted" & `, + }) + Expect(message.Subject).Equals(`Message to: Encontrar+se "quoted" & `) +} + func TestRenderMessage(t *testing.T) { RegisterT(t) diff --git a/app/services/email/message.go b/app/services/email/message.go index 9374c5eb2..a9ed834d3 100644 --- a/app/services/email/message.go +++ b/app/services/email/message.go @@ -3,6 +3,7 @@ package email import ( "bytes" "context" + "html" "mime" "strings" "unicode" @@ -53,8 +54,15 @@ func RenderMessage(ctx context.Context, templateName string, fromAddress string, lines := strings.Split(bf.String(), "\n") body := strings.TrimLeft(strings.Join(lines[2:], "\n"), " ") + // The subject is rendered through html/template (because it shares the + // template set with the HTML body), which escapes characters like '+', + // '"', '&', '<', '>' to numeric entities (e.g. "+"). SMTP headers + // are plain text and clients do not decode HTML entities in them, so + // undo the escaping for the subject only. + subject := html.UnescapeString(strings.TrimLeft(lines[0], "subject: ")) + return &Message{ - Subject: strings.TrimLeft(lines[0], "subject: "), + Subject: subject, Body: body, } } diff --git a/lingui.config.js b/lingui.config.js index 56183923a..d8c40b2fb 100644 --- a/lingui.config.js +++ b/lingui.config.js @@ -16,5 +16,5 @@ export default { }, sourceLocale: "en", format: formatter({ style: "minimal", explicitIdAsDefault: true, sort: true }), - locales: ["pt-BR", "es-ES", "nl", "sv-SE", "fr", "de", "en", "pl", "ru", "ja", "sk", "tr", "el", "it", "zh-CN", "zh-TW", "ar", "fa"], + locales: ["pt-BR", "pt-PT", "es-ES", "nl", "sv-SE", "fr", "de", "en", "pl", "ru", "ja", "sk", "tr", "el", "it", "zh-CN", "zh-TW", "ar", "fa"], } diff --git a/locale/locales.ts b/locale/locales.ts index 74d1ace35..d44a894e0 100644 --- a/locale/locales.ts +++ b/locale/locales.ts @@ -12,6 +12,9 @@ const locales: { [key: string]: Locale } = { "pt-BR": { text: "Portuguese (Brazilian)", }, + "pt-PT": { + text: "Portuguese (Portugal)", + }, "es-ES": { text: "Spanish", }, diff --git a/locale/pt-PT/client.json b/locale/pt-PT/client.json new file mode 100644 index 000000000..632bbc60f --- /dev/null +++ b/locale/pt-PT/client.json @@ -0,0 +1,238 @@ +{ + "action.cancel": "Cancelar", + "action.change": "alterar", + "action.close": "Fechar", + "action.commentsfeed": "Feed de comentários", + "action.confirm": "Confirmar", + "action.copylink": "Copiar ligação", + "action.delete": "Eliminar", + "action.delete.block": "Eliminar e bloquear", + "action.edit": "Editar", + "action.markallasread": "Marcar tudo como lido", + "action.ok": "OK", + "action.postcomment": "Publicar", + "action.postsfeed": "Feed de publicações", + "action.publish": "Publicar", + "action.publish.verify": "Publicar e confiar", + "action.respond": "Responder", + "action.save": "Guardar", + "action.signin": "Iniciar sessão", + "action.signup": "Registar", + "action.submit": "Submeter", + "action.vote": "Votar nesta ideia", + "action.voted": "Votado!", + "editor.markdownmode": "Mudar para o editor markdown", + "editor.richtextmode": "Mudar para o editor de texto formatado", + "enum.poststatus.completed": "Concluído", + "enum.poststatus.declined": "Recusado", + "enum.poststatus.deleted": "Eliminado", + "enum.poststatus.duplicate": "Duplicado", + "enum.poststatus.open": "Aberto", + "enum.poststatus.planned": "Planeado", + "enum.poststatus.started": "Iniciado", + "error.accessdenied.contact": "Se considera que se trata de um erro, contacte o seu administrador.", + "error.accessdenied.text": "Não tem as permissões necessárias para aceder a este site.", + "error.accessdenied.title": "Acesso negado", + "error.expired.text": "A ligação em que clicou expirou.", + "error.expired.title": "Expirado", + "error.forbidden.text": "Não tem autorização para ver esta página.", + "error.forbidden.title": "Acesso proibido", + "error.internalerror.text": "Ocorreu um erro e estamos a trabalhar para resolver o problema! Voltaremos em breve.", + "error.internalerror.title": "Bolas! Isto não estava à espera…", + "error.notinvited.text": "Não foi possível encontrar uma conta para o seu endereço de email.", + "error.notinvited.title": "Sem convite", + "error.pagenotfound.text": "A ligação em que clicou pode estar partida ou a página pode ter sido removida.", + "error.pagenotfound.title": "Página não encontrada", + "error.unauthorized.text": "Tem de iniciar sessão antes de aceder a esta página.", + "error.unauthorized.title": "Não autorizado", + "home.filter.label": "Filtrar", + "home.filter.search.label": "Pesquisar nos filtros...", + "home.form.defaultinvitation": "Introduza aqui a sua sugestão...", + "home.form.defaultwelcomemessage": "Adoraríamos saber o que tem em mente.\n\nO que podemos fazer melhor? Este é o sítio para votar, discutir e partilhar ideias.", + "home.lonely.suggestion": "É recomendado criar <0>pelo menos 3 sugestões aqui antes de partilhar este site. O conteúdo inicial é importante para começar a envolver a sua audiência.", + "home.lonely.text": "Ainda não foram criadas publicações.", + "home.postfilter.label.moderation": "Moderação", + "home.postfilter.label.myactivity": "A minha atividade", + "home.postfilter.label.status": "Estado", + "home.postfilter.option.mostdiscussed": "Mais discutidas", + "home.postfilter.option.mostwanted": "Mais procuradas", + "home.postfilter.option.myposts": "As minhas publicações", + "home.postfilter.option.myvotes": "Os meus votos", + "home.postfilter.option.notags": "Sem etiquetas", + "home.postfilter.option.recent": "Recentes", + "home.postfilter.option.trending": "Em destaque", + "home.postscontainer.label.noresults": "Nenhum resultado correspondeu à sua pesquisa, tente algo diferente.", + "home.postscontainer.label.viewmore": "Ver mais publicações", + "home.postscontainer.query.placeholder": "Pesquisar", + "home.postsort.label": "Ordenar por:", + "home.similar.title": "Temos publicações semelhantes, a sua ideia já está na lista?", + "label.addtags": "Adicionar etiquetas...", + "label.avatar": "Avatar", + "label.custom": "Personalizado", + "label.discussion": "Discussão", + "label.edittags": "Editar etiquetas", + "label.email": "Email", + "label.follow": "Seguir", + "label.following": "A seguir", + "label.gravatar": "Gravatar", + "label.letter": "Letra", + "label.name": "Nome", + "label.none": "Nenhum", + "label.notifications": "Notificações", + "label.or": "OU", + "label.searchtags": "Pesquisar etiquetas...", + "label.subscribe": "Subscrever", + "label.tags": "Etiquetas", + "label.unread": "Não lido", + "label.unsubscribe": "Cancelar subscrição", + "label.vote": "Voto", + "label.voters": "Votantes", + "label.votes": "Votos", + "labels.notagsavailable": "Não há etiquetas disponíveis", + "legal.agreement": "Li e concordo com os <0/> e <1/>.", + "legal.notice": "Ao iniciar sessão, concorda com os <0/><1/> e <2/>.", + "legal.privacypolicy": "Política de Privacidade", + "legal.termsofservice": "Termos de Serviço", + "linkmodal.insert": "Inserir ligação", + "linkmodal.text.label": "Texto a apresentar", + "linkmodal.text.placeholder": "Introduza o texto da ligação", + "linkmodal.title": "Inserir ligação", + "linkmodal.url.label": "URL", + "linkmodal.url.placeholder": "https://exemplo.com", + "menu.administration": "Administração", + "menu.mysettings": "As minhas definições", + "menu.signout": "Terminar sessão", + "menu.sitesettings": "Definições do site", + "modal.changeemail.header": "Confirme o seu novo email", + "modal.changeemail.text": "Acabámos de enviar uma ligação de confirmação para <0>{0}. <1/> Clique na ligação para atualizar o seu email.", + "modal.completeprofile.header": "Complete o seu perfil", + "modal.completeprofile.name.placeholder": "Nome", + "modal.completeprofile.text": "Como é o seu primeiro início de sessão, introduza por favor o seu nome.", + "modal.deleteaccount.header": "Eliminar conta", + "modal.deleteaccount.text": "<0>Quando optar por eliminar a sua conta, apagaremos todas as suas informações pessoais para sempre. O conteúdo que publicou permanecerá, mas será anonimizado.<1>Este processo é irreversível. <2>Tem a certeza?", + "modal.deletecomment.header": "Eliminar comentário", + "modal.deletecomment.text": "Este processo é irreversível. <0>Tem a certeza?", + "modal.notifications.nonew": "Sem novas notificações", + "modal.notifications.previous": "Notificações anteriores", + "modal.notifications.unread": "Notificações não lidas", + "modal.rss.description": "Para subscrever este feed ATOM, copie e cole este URL no seu leitor de RSS/ATOM.", + "modal.rss.title": "Subscrever feed ATOM", + "modal.showvotes.message.zeromatches": "Não foram encontrados utilizadores correspondentes a <0>{query}.", + "modal.showvotes.query.placeholder": "Pesquisar utilizadores por nome...", + "modal.signin.header": "Junte-se à conversa", + "moderation.comment.delete.block.error": "Falha ao eliminar comentário e bloquear utilizador", + "moderation.comment.delete.error": "Falha ao eliminar comentário", + "moderation.comment.deleted": "Comentário eliminado com sucesso", + "moderation.comment.deleted.blocked": "Comentário eliminado e utilizador bloqueado", + "moderation.comment.publish.error": "Falha ao publicar comentário", + "moderation.comment.publish.verify.error": "Falha ao publicar comentário e verificar utilizador", + "moderation.comment.published": "Comentário publicado com sucesso", + "moderation.comment.published.verified": "Comentário publicado e utilizador verificado", + "moderation.empty": "Todo o conteúdo foi moderado. Está em dia!", + "moderation.fetch.error": "Falha ao obter itens de moderação", + "moderation.post.delete.block.error": "Falha ao eliminar publicação e bloquear utilizador", + "moderation.post.delete.error": "Falha ao eliminar publicação", + "moderation.post.deleted": "Publicação eliminada com sucesso", + "moderation.post.deleted.blocked": "Publicação eliminada e utilizador bloqueado", + "moderation.post.publish.error": "Falha ao publicar a publicação", + "moderation.post.publish.verify.error": "Falha ao publicar a publicação e verificar utilizador", + "moderation.post.published": "Publicação publicada com sucesso", + "moderation.post.published.verified": "Publicação publicada e utilizador verificado", + "moderation.subtitle": "Estas ideias e comentários são de pessoas que não constam da sua lista de utilizadores de confiança; pode decidir se são publicados.", + "moderation.title": "Fila de moderação", + "mynotifications.label.readrecently": "Lido nos últimos 30 dias.", + "mynotifications.message.nounread": "Sem notificações por ler.", + "mynotifications.page.subtitle": "Mantenha-se a par do que se passa", + "mynotifications.page.title": "Notificações", + "mysettings.apikey.documentation": "Para saber como utilizar a API, leia a <0>documentação oficial.", + "mysettings.apikey.generate": "Gerar nova chave API", + "mysettings.apikey.newkey": "A sua nova chave API é: <0>{0}", + "mysettings.apikey.newkeynotice": "Guarde-a em segurança nos seus servidores e nunca a armazene no lado do cliente da aplicação.", + "mysettings.apikey.notice": "A chave API só é mostrada quando é gerada. Se a sua chave for perdida ou comprometida, gere uma nova e tome nota dela.", + "mysettings.apikey.title": "Chave API", + "mysettings.dangerzone.delete": "Eliminar a minha conta", + "mysettings.dangerzone.notice": "Este processo é irreversível. Confirme com cuidado.", + "mysettings.dangerzone.text": "Quando optar por eliminar a sua conta, apagaremos todas as suas informações pessoais para sempre. O conteúdo que publicou permanecerá, mas será anonimizado.", + "mysettings.dangerzone.title": "Eliminar conta", + "mysettings.message.avatar.custom": "Aceitamos imagens JPG, GIF e PNG, com menos de 100KB e proporção 1:1, com dimensões mínimas de 50x50 pixels.", + "mysettings.message.avatar.gravatar": "Será utilizado um <0>Gravatar baseado no seu email. Se não tiver Gravatar, é gerado um avatar com a letra das suas iniciais.", + "mysettings.message.avatar.letter": "É gerado um avatar com a letra das suas iniciais.", + "mysettings.message.noemail": "A sua conta não tem email.", + "mysettings.message.privateemail": "O seu email é privado e nunca será mostrado publicamente.", + "mysettings.notification.channelemail": "Email", + "mysettings.notification.channelweb": "Web", + "mysettings.notification.event.discussion": "Novos comentários", + "mysettings.notification.event.mention": "Menções", + "mysettings.notification.event.newpost": "Nova publicação", + "mysettings.notification.event.newpostcreated": "A sua ideia foi adicionada 👍", + "mysettings.notification.event.statuschanged": "Estado alterado", + "mysettings.notification.title": "Escolha os eventos para os quais quer receber notificação.", + "mysettings.page.subtitle": "Faça a gestão das definições do seu perfil", + "mysettings.page.title": "Definições", + "newpost.modal.description.placeholder": "Conte-nos. Explique tudo sem reservas — quanta mais informação, melhor.", + "newpost.modal.submit": "Submeter a sua ideia", + "newpost.modal.title": "Partilhe a sua ideia...", + "newpost.modal.title.label": "Dê um título à sua ideia", + "newpost.modal.title.placeholder": "Algo curto e cativante, em poucas palavras", + "page.backhome": "Levar-me de volta à página inicial de <0>{0}.", + "page.pendingactivation.didntreceive": "Não recebeu o email?", + "page.pendingactivation.resend": "Reenviar email de verificação", + "page.pendingactivation.resending": "A reenviar...", + "page.pendingactivation.text": "Enviámos-lhe um email de confirmação com uma ligação para ativar o seu site.", + "page.pendingactivation.text2": "Verifique a sua caixa de entrada para o ativar.", + "page.pendingactivation.title": "A sua conta está a aguardar ativação", + "pagination.next": "Seguinte", + "pagination.prev": "Anterior", + "post.pending": "pendente", + "postdetails.backtoall": "Voltar a todas as sugestões", + "showpost.comment.copylink.error": "Não foi possível copiar a ligação do comentário; copie o URL da página", + "showpost.comment.copylink.success": "Ligação do comentário copiada para a área de transferência", + "showpost.comment.unknownhighlighted": "ID de comentário desconhecido #{id}", + "showpost.commentinput.placeholder": "Deixe um comentário", + "showpost.copylink.success": "Ligação copiada para a área de transferência", + "showpost.loading": "A carregar...", + "showpost.message.nodescription": "Sem descrição.", + "showpost.moderation.admin.description": "Esta ideia precisa da sua aprovação antes de ser publicada", + "showpost.moderation.admin.title": "Moderação", + "showpost.moderation.approved": "Publicação aprovada com sucesso", + "showpost.moderation.approveerror": "Falha ao aprovar publicação", + "showpost.moderation.awaiting": "A aguardar moderação.", + "showpost.moderation.comment.admin.description": "Este comentário precisa da sua aprovação antes de ser publicado", + "showpost.moderation.comment.approved": "Comentário aprovado com sucesso", + "showpost.moderation.comment.approveerror": "Falha ao aprovar comentário", + "showpost.moderation.comment.awaiting": "A aguardar moderação.", + "showpost.moderation.comment.declined": "Comentário recusado com sucesso", + "showpost.moderation.comment.declineerror": "Falha ao recusar comentário", + "showpost.moderation.commentsuccess": "O seu comentário está a aguardar moderação 📝", + "showpost.moderation.declined": "Publicação recusada com sucesso", + "showpost.moderation.declineerror": "Falha ao recusar publicação", + "showpost.moderation.postsuccess": "A sua ideia está a aguardar moderação 📝", + "showpost.moderationpanel.text.help": "Esta operação <0>não pode ser anulada.", + "showpost.moderationpanel.text.placeholder": "Porque está a eliminar esta publicação? (opcional)", + "showpost.notificationspanel.message.subscribed": "Está a receber notificações sobre a atividade desta publicação.", + "showpost.notificationspanel.message.unsubscribed": "Não receberá nenhuma notificação sobre esta publicação.", + "showpost.postedby": "Publicado por", + "showpost.postsearch.numofvotes": "{0} votos", + "showpost.postsearch.query.placeholder": "Pesquisar publicação original...", + "showpost.responseform.message.mergedvotes": "Os votos desta publicação serão fundidos na publicação original.", + "showpost.responseform.text.placeholder": "O que se passa com esta publicação? Diga aos seus utilizadores quais são os seus planos...", + "showpost.save.success": "Publicação atualizada com sucesso", + "signin.code.edit": "Editar", + "signin.code.getnew": "Obter novo código", + "signin.code.instruction": "Introduza o código que acabámos de enviar para <0>{email}", + "signin.code.placeholder": "Introduza aqui o código", + "signin.code.sent": "Foi enviado um novo código para o seu email.", + "signin.email.placeholder": "Endereço de email", + "signin.message.email": "Continuar com email", + "signin.message.emaildisabled": "A autenticação por email foi desativada por um administrador. Se tem uma conta de administrador e precisa de contornar esta restrição, <0>clique aqui.", + "signin.message.emailsent": "Acabámos de enviar uma ligação de confirmação para <0>{email}. Clique na ligação e iniciará sessão.", + "signin.message.locked.text": "Para reativar este site, inicie sessão com uma conta de administrador e atualize as definições necessárias.", + "signin.message.locked.title": "<0>{0} está atualmente bloqueado.", + "signin.message.onlyadmins": "De momento, apenas é permitido iniciar sessão com uma conta de administrador", + "signin.message.private.text": "Se tem uma conta ou um convite, pode utilizar as opções seguintes para iniciar sessão.", + "signin.message.private.title": "<0>{0} é um espaço privado, tem de iniciar sessão para participar e votar.", + "signin.message.socialbutton.intro": "Continuar com", + "signin.name.placeholder": "O seu nome", + "validation.custom.maxattachments": "É permitido um máximo de {number} anexos.", + "validation.custom.maximagesize": "O tamanho da imagem tem de ser inferior a {kilobytes}KB." +} diff --git a/locale/pt-PT/server.json b/locale/pt-PT/server.json new file mode 100644 index 000000000..234d20193 --- /dev/null +++ b/locale/pt-PT/server.json @@ -0,0 +1,74 @@ +{ + "property.avatarType": "Tipo de avatar", + "property.name": "Nome", + "property.image": "Imagem", + "property.customdomain": "Domínio personalizado", + "property.key": "Chave", + "property.email": "Email", + "property.title": "Título", + "property.comment": "Comentário", + "property.status": "Estado", + "validation.required": "{name} é obrigatório.", + "validation.invalid": "{name} é inválido.", + "validation.invalidvalue": "{name} tem um valor inválido '{value}'.", + "validation.maxstringlen": "{name} tem de ter menos de {len} carateres.", + "validation.custom.maxattachments": "É permitido um máximo de {number} anexos por publicação.", + "validation.custom.differentemail": "Escolha um email diferente.", + "validation.custom.emailtaken": "Este email já está a ser utilizado por outra pessoa", + "validation.custom.descriptivetitle": "O título tem de ser mais descritivo.", + "validation.custom.duplicatetitle": "Isto já foi publicado anteriormente.", + "validation.custom.selfduplicate": "Não pode ser duplicado de si próprio.", + "validation.custom.originalpostnotfound": "Publicação original não encontrada.", + "validation.custom.cannotdeleteduplicatepost": "Esta publicação não pode ser eliminada porque está a ser referenciada por uma publicação duplicada.", + "validation.custom.unknownsettings": "Definição desconhecida com o nome '{name}'", + "validation.custom.invalidemail": "'{email}' não é um endereço de email válido.", + "validation.custom.invalidurl": "'{url}' não é um URL válido.", + "validation.custom.invalidcustomdomain": "'{domain}' não é um domínio personalizado válido.", + "validation.custom.customdomaintaken": "Este domínio personalizado já está a ser utilizado por outra pessoa.", + "validation.custom.unsupportedfileformat": "Este formato de ficheiro não é suportado.", + "validation.custom.minimagedimensions": "A imagem tem de ter dimensões mínimas de {width}x{height} pixels.", + "validation.custom.imagesquareratio": "A imagem tem de ter uma proporção de 1:1.", + "validation.custom.maximagesize": "O tamanho da imagem tem de ser inferior a {kilobytes}KB.", + "validation.custom.invalidemoji": "Reação emoji inválida.", + "enum.poststatus.open": "Aberto", + "enum.poststatus.started": "Iniciado", + "enum.poststatus.completed": "Concluído", + "enum.poststatus.declined": "Recusado", + "enum.poststatus.planned": "Planeado", + "enum.poststatus.duplicate": "Duplicado", + "enum.poststatus.deleted": "Eliminado", + "email.change_emailaddress.subject": "Confirme o seu novo email", + "email.change_emailaddress.request": "Pediu para alterar o seu email de {oldEmail} para {newEmail}.", + "email.subscription.view": "vê-lo no navegador", + "email.subscription.change": "alterar as suas preferências de notificação", + "email.subscription.unsubscribe": "cancelar a subscrição", + "email.greetings": "Olá!", + "email.new_mention.text": "{userName} mencionou-o em {title} ({postLink}).", + "email.greetings_name": "Olá, {name}!", + "email.operation_confirmation": "Clique na ligação abaixo para confirmar esta operação.", + "email.footer.noreply": "Este email foi enviado de um endereço apenas de notificação que não aceita emails recebidos. Por favor, não responda a esta mensagem.", + "email.change_status.duplicate": "{title} ({postLink}) foi fechada como duplicada de {duplicate}.", + "email.change_status.others": "O estado de {title} ({postLink}) foi alterado para {status}.", + "email.delete_post.text": "{title} foi eliminada.", + "email.new_comment.text": "{userName} deixou um comentário em {title} ({postLink}).", + "email.new_post.text": "{userName} criou uma nova publicação {title} ({postLink}).", + "email.signin_email.subject": "O seu código de início de sessão para {siteName} é {code}", + "email.signin_email.text": "Aqui está o seu código de início de sessão.", + "email.signin_email.confirmation": "Utilize o código de utilização única abaixo para iniciar sessão em {siteName}.", + "email.signup_email.subject": "O seu novo site Fider", + "email.signup_email.text": "Está a um passo de ativar o seu site Fider.", + "email.signin_email.your_code": "O seu código de início de sessão é:", + "email.signin_email.code_expires": "Este código expira em 15 minutos.", + "email.signin_email.alternative": "Em alternativa, pode clicar na ligação abaixo para iniciar sessão diretamente:", + "email.signup_email.confirmation": "Através da ligação abaixo pode verificar o seu endereço de email e concluir o processo de ativação.", + "email.footer.subscription_notice": "Está a receber este email porque está subscrito a esta publicação. Pode {view}, {unsubscribe} ou {change}.", + "email.footer.subscription_notice2": "Está a receber este email porque está subscrito a esta publicação. Pode {change}.", + "email.footer.subscription_notice3": "Está a receber este email porque está subscrito a esta publicação. Pode {view} ou {change}.", + "feed.global.title": "{count, plural, one {({count} Voto) {title}} other {({count} Votos) {title}}}", + "feed.comment.title": "Comentário de {author}", + "feed.comment.op": "Publicação original por {author}", + "feed.comment.response": "Resposta de {author}", + "feed.post.title": "# {title}\n{votes, plural, one {# voto} other {# votos}}, {comments, plural, one {# comentário} other {# comentários}}\n\n---\n", + "feed.post.footer.response": "Resposta de {responder} em {date}:\n\n>{response}\n", + "feed.post.footer": "\n\n---\n{response_footer}\n{votes, plural, one {# voto} other {# votos}}, {comments, plural, one {# comentário} other {# comentários}} - ver [na web]({web_link}) ou [como feed]({feed_link})" +} diff --git a/public/ssr.tsx b/public/ssr.tsx index 751eeebdf..1e54339fa 100644 --- a/public/ssr.tsx +++ b/public/ssr.tsx @@ -13,6 +13,7 @@ import { I18nProvider } from "@lingui/react" const messages: { [key: string]: any } = { en: require(`../locale/en/client`), "pt-BR": require(`../locale/pt-BR/client`), + "pt-PT": require(`../locale/pt-PT/client`), "sv-SE": require(`../locale/sv-SE/client`), it: require(`../locale/it/client`), "es-ES": require(`../locale/es-ES/client`),