Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/models/enum/locale.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -203,6 +213,7 @@ var (
AllLocales = []Locale{
LocaleEnglish,
LocalePortugueseBR,
LocalePortuguesePT,
LocaleSpanishES,
LocaleGerman,
LocaleFrench,
Expand Down
14 changes: 14 additions & 0 deletions app/services/email/email_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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. "&#43;", "&#34;") 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" & <tagged>`,
})
Expect(message.Subject).Equals(`Message to: Encontrar+se "quoted" & <tagged>`)
}

func TestRenderMessage(t *testing.T) {
RegisterT(t)

Expand Down
10 changes: 9 additions & 1 deletion app/services/email/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package email
import (
"bytes"
"context"
"html"
"mime"
"strings"
"unicode"
Expand Down Expand Up @@ -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. "&#43;"). 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,
}
}
2 changes: 1 addition & 1 deletion lingui.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
}
3 changes: 3 additions & 0 deletions locale/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const locales: { [key: string]: Locale } = {
"pt-BR": {
text: "Portuguese (Brazilian)",
},
"pt-PT": {
text: "Portuguese (Portugal)",
},
"es-ES": {
text: "Spanish",
},
Expand Down
238 changes: 238 additions & 0 deletions locale/pt-PT/client.json

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions locale/pt-PT/server.json
Original file line number Diff line number Diff line change
@@ -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": "<strong>{userName}</strong> mencionou-o em <strong>{title} ({postLink})</strong>.",
"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": "<strong>{title} ({postLink})</strong> foi fechada como <strong>duplicada</strong> de {duplicate}.",
"email.change_status.others": "O estado de <strong>{title} ({postLink})</strong> foi alterado para <strong>{status}</strong>.",
"email.delete_post.text": "<strong>{title}</strong> foi <strong>eliminada</strong>.",
"email.new_comment.text": "<strong>{userName}</strong> deixou um comentário em <strong>{title} ({postLink})</strong>.",
"email.new_post.text": "<strong>{userName}</strong> criou uma nova publicação <strong>{title} ({postLink})</strong>.",
"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 <strong>{siteName}</strong>.",
"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})"
}
1 change: 1 addition & 0 deletions public/ssr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`),
Expand Down