diff --git a/Makefile b/Makefile index c7c00ffa..c8dbf23d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build clean ui -VERSION=1.0.5 +VERSION=1.0.6 BIN=answer DIR_SRC=./cmd/answer DOCKER_CMD=docker diff --git a/docs/docs.go b/docs/docs.go index d6741817..b28986b7 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5984,9 +5984,6 @@ const docTemplate = `{ "schema.GetOtherUserInfoResp": { "type": "object", "properties": { - "has": { - "type": "boolean" - }, "info": { "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" } @@ -7688,7 +7685,6 @@ const docTemplate = `{ ], "properties": { "status": { - "description": "user status", "type": "string", "enum": [ "normal", @@ -7698,7 +7694,6 @@ const docTemplate = `{ ] }, "user_id": { - "description": "user id", "type": "string" } } @@ -7994,6 +7989,10 @@ const docTemplate = `{ "label": { "type": "string" }, + "progress": { + "description": "Translation completion percentage", + "type": "integer" + }, "value": { "type": "string" } diff --git a/docs/swagger.json b/docs/swagger.json index 8433f7c5..21be9a5c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5972,9 +5972,6 @@ "schema.GetOtherUserInfoResp": { "type": "object", "properties": { - "has": { - "type": "boolean" - }, "info": { "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" } @@ -7676,7 +7673,6 @@ ], "properties": { "status": { - "description": "user status", "type": "string", "enum": [ "normal", @@ -7686,7 +7682,6 @@ ] }, "user_id": { - "description": "user id", "type": "string" } } @@ -7982,6 +7977,10 @@ "label": { "type": "string" }, + "progress": { + "description": "Translation completion percentage", + "type": "integer" + }, "value": { "type": "string" } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f3663b28..c57892fa 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -488,8 +488,6 @@ definitions: type: object schema.GetOtherUserInfoResp: properties: - has: - type: boolean info: $ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp' type: object @@ -1691,7 +1689,6 @@ definitions: schema.UpdateUserStatusReq: properties: status: - description: user status enum: - normal - suspended @@ -1699,7 +1696,6 @@ definitions: - inactive type: string user_id: - description: user id type: string required: - status @@ -1911,6 +1907,9 @@ definitions: properties: label: type: string + progress: + description: Translation completion percentage + type: integer value: type: string type: object diff --git a/go.mod b/go.mod index baaa08c5..a980e167 100644 --- a/go.mod +++ b/go.mod @@ -35,13 +35,13 @@ require ( github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 github.com/spf13/cobra v1.6.1 github.com/stretchr/testify v1.8.1 - github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a + github.com/swaggo/files v1.0.0 github.com/swaggo/gin-swagger v1.5.3 - github.com/swaggo/swag v1.8.7 + github.com/swaggo/swag v1.8.10 github.com/tidwall/gjson v1.14.4 github.com/yuin/goldmark v1.4.13 golang.org/x/crypto v0.1.0 - golang.org/x/net v0.1.0 + golang.org/x/net v0.2.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.14.2 @@ -121,7 +121,7 @@ require ( go.uber.org/zap v1.23.0 // indirect golang.org/x/image v0.1.0 // indirect golang.org/x/mod v0.6.0 // indirect - golang.org/x/sys v0.1.0 // indirect + golang.org/x/sys v0.2.0 // indirect golang.org/x/text v0.5.0 // indirect golang.org/x/tools v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index 95de6076..58423a80 100644 --- a/go.sum +++ b/go.sum @@ -668,13 +668,14 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4= +github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc= github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU= -github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= +github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= +github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= @@ -850,8 +851,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -944,11 +945,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index c4f5da5c..18d311c3 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -35,6 +35,10 @@ backend: other: Email and password do not match. error: admin: + cannot_update_their_password: + other: You cannot modify your password. + cannot_modify_self_status: + other: You cannot modify your status. email_or_password_wrong: other: Email and password do not match. answer: @@ -81,6 +85,8 @@ backend: new_password_same_as_previous_setting: other: The new password is the same as the previous one. question: + already_deleted: + other: This post has been deleted. not_found: other: Question not found. cannot_deleted: @@ -819,6 +825,7 @@ ui: approve: Approve reject: Reject skip: Skip + discard_draft: Discard draft search: title: Search Results keywords: Keywords @@ -1015,9 +1022,11 @@ ui: answers: answers accepted: Accepted page_404: + http_error: HTTP Error 404 desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: + http_error: HTTP Error 500 desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: @@ -1407,6 +1416,10 @@ ui: reputation: reputation votes: votes prompt: - leave_page: "Are you sure you want to leave the page?" - changes_not_save: "Your changes may not be saved." + leave_page: Are you sure you want to leave the page? + changes_not_save: Your changes may not be saved. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. diff --git a/i18n/i18n.yaml b/i18n/i18n.yaml index 87db7100..a0b7a80f 100644 --- a/i18n/i18n.yaml +++ b/i18n/i18n.yaml @@ -1,26 +1,41 @@ # all support language language_options: - - label: "English(US)" + - label: "English" value: "en_US" - - label: "Español(ES)" + progress: 100 + - label: "Español" value: "es_ES" - - label: "Português(PT)" + progress: 0 + - label: "Português(BR)" + value: "pt_BR" + progress: 0 + - label: "Português" value: "pt_PT" - - label: "Deutsch(DE)" + progress: 0 + - label: "Deutsch" value: "de_DE" - - label: "Français(FR)" + progress: 4 + - label: "Français" value: "fr_FR" - - label: "日本語(JA)" + progress: 100 + - label: "日本語" value: "ja_JP" - - label: "Italiano(IT)" + progress: 0 + - label: "Italiano" value: "it_IT" - - label: "Русский(RU)" + progress: 16 + - label: "Русский" value: "ru_RU" - - label: "简体中文(CN)" + progress: 13 + - label: "简体中文" value: "zh_CN" - - label: "繁體中文(CN)" + progress: 100 + - label: "繁體中文" value: "zh_TW" - - label: "한국어(KO)" + progress: 100 + - label: "한국어" value: "ko_KR" - - label: "Tiếng Việt(VI)" + progress: 0 + - label: "Tiếng Việt" value: "vi_VN" + progress: 0 diff --git a/i18n/pt_BR.yaml b/i18n/pt_BR.yaml index 14c553f5..49a4bd4f 100644 --- a/i18n/pt_BR.yaml +++ b/i18n/pt_BR.yaml @@ -2,914 +2,914 @@ backend: base: success: - other: Success. + other: Sucesso. unknown: - other: Unknown error. + other: Erro desconhecido. request_format_error: - other: Request format is not valid. + other: O formato da Requisição não é válido. unauthorized_error: - other: Unauthorized. + other: Não autorizado. database_error: - other: Data server error. + other: Erro no servidor de dados. role: name: user: - other: User + other: Usuário admin: - other: Admin + other: Administrador moderator: - other: Moderator + other: Moderador description: user: - other: Default with no special access. + other: Padrão sem acesso especial. admin: - other: Have the full power to access the site. + other: Possui controle total para acessar o site. moderator: - other: Has access to all posts except admin settings. + other: Não possui controle a todas as postagens exceto configurações de administrador. email: - other: Email + other: E-mail password: - other: Password + other: Senha email_or_password_wrong_error: - other: Email and password do not match. + other: E-mail e Senha inválidos. error: admin: email_or_password_wrong: - other: Email and password do not match. + other: E-mail e Senha inválidos. answer: not_found: - other: Answer do not found. + other: Resposta não encontrada. cannot_deleted: - other: No permission to delete. + other: Permissão insuficiente para deletar. cannot_update: - other: No permission to update. + other: Permissão insuficiente para atualizar. comment: edit_without_permission: - other: Comment are not allowed to edit. + other: Não é permitido atualizar comentários. not_found: - other: Comment not found. + other: Comentário não encontrado. cannot_edit_after_deadline: - other: The comment time has been too long to modify. + other: O tempo para comentários expirou. email: duplicate: - other: Email already exists. + other: E-mail já existe na base de dados. need_to_be_verified: - other: Email should be verified. + other: E-mail precisa ser verificado. verify_url_expired: - other: Email verified URL has expired, please resend the email. + other: A URL de validação do e-mail expirou, por favor, re-envie o e-mail. lang: not_found: - other: Language file not found. + other: Arquivo de idioma não encontrado. object: captcha_verification_failed: - other: Captcha wrong. + other: Captcha inválido. disallow_follow: - other: You are not allowed to follow. + other: Você não possui permissão para seguir. disallow_vote: - other: You are not allowed to vote. + other: Você não possui permissão para votar. disallow_vote_your_self: - other: You can't vote for your own post. + other: Você não pode votar na sua própria postagem. not_found: - other: Object not found. + other: Objeto não encontrado. verification_failed: - other: Verification failed. + other: Falha na verificação. email_or_password_incorrect: - other: Email and password do not match. + other: E-mail e Senha não conferem. old_password_verification_failed: - other: The old password verification failed + other: Verficação de senhas antigas falhou. new_password_same_as_previous_setting: - other: The new password is the same as the previous one. + other: A nova senha é idêntica à senha anterior. question: not_found: - other: Question not found. + other: Pergunta não encontrada. cannot_deleted: - other: No permission to delete. + other: Não possui permissão para deletar. cannot_close: - other: No permission to close. + other: Não possui permissão para fechar. cannot_update: - other: No permission to update. + other: Não possui permissão para atualizar.. rank: fail_to_meet_the_condition: - other: Rank fail to meet the condition. + other: A classificação não atende à condição. report: handle_failed: - other: Report handle failed. + other: Falha no processamento do relatório. not_found: - other: Report not found. + other: Relatório não encontrado. tag: not_found: - other: Tag not found. + other: Marcador não encontada. recommend_tag_not_found: - other: Recommend Tag is not exist. + other: Marcador recomendado não existe. recommend_tag_enter: - other: Please enter at least one required tag. + other: Por favor, insira ao menos um marcador obrigatório. not_contain_synonym_tags: - other: Should not contain synonym tags. + other: Não pode contar marcadores de sinônimos. cannot_update: - other: No permission to update. + other: Não possui permissão para atualizar. cannot_set_synonym_as_itself: - other: You cannot set the synonym of the current tag as itself. + other: Você não pode definir o sinônimo do marcador atual como ela mesma. smtp: config_from_name_cannot_be_email: - other: The From Name cannot be a email address. - theme: + other: O De Nome não pode ser um endereço de e-mail. + Tema: not_found: - other: Theme not found. + other: Tema não encontrado. revision: review_underway: - other: Can't edit currently, there is a version in the review queue. + other: Não é possível editar no momento, há uma versão na fila de revisão. no_permission: - other: No permission to Revision. + other: Sem pemissão para revisar. user: email_or_password_wrong: other: - other: Email and password do not match. + other: E-mail e senha não correspondem. not_found: - other: User not found. + other: Usuário não encontrado. suspended: - other: User has been suspended. + other: Usuário foi suspenso. username_invalid: - other: Username is invalid. + other: Nome de usuário inválido. username_duplicate: - other: Username is already in use. + other: Nome de usuário já está em uso. set_avatar: - other: Avatar set failed. + other: Falha ao configurar Avatar. cannot_update_your_role: - other: You cannot modify your role. + other: Você não pode modificar a sua função. not_allowed_registration: - other: Currently the site is not open for registration + other: Atualmente o site não está aberto para cadastro config: read_config_failed: - other: Read config failed + other: A leitura da configuração falhou database: connection_failed: - other: Database connection failed + other: Falha na conexão com o banco de dados create_table_failed: - other: Create table failed + other: Falha na criação de tabela install: create_config_failed: - other: Can't create the config.yaml file. + other: Não foi possível criar o arquivo config.yaml. upload: unsupported_file_format: - other: Unsupported file format. + other: Formato de arquivo não suportado. report: spam: name: other: spam desc: - other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + other: Este post é um anúncio, ou vandalismo. Não é útil ou relevante para o tópico atual. rude: name: - other: rude or abusive + other: rude ou abusivo desc: - other: A reasonable person would find this content inappropriate for respectful discourse. + other: Uma pessoa razoável acharia este conteúdo impróprio para um discurso respeitoso. duplicate: name: - other: a duplicate + other: uma duplicidade desc: - other: This question has been asked before and already has an answer. + other: Esta pergunta já foi feita antes e já tem uma resposta. not_answer: name: - other: not an answer + other: não é uma resposta desc: - other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + other: Isso foi postado como uma resposta, mas não tenta responder à pergunta. Possivelmente deve ser uma edição, um comentário, outra pergunta ou excluído completamente. not_need: name: - other: no longer needed + other: não é mais necessário desc: - other: This comment is outdated, conversational or not relevant to this post. + other: Este comentário está desatualizado, coloquial ou não é relevante para esta postagem. other: name: - other: something else + other: algo mais desc: - other: This post requires staff attention for another reason not listed above. + other: Esta postagem requer atenção da equipe por outro motivo não listado acima. question: close: duplicate: name: other: spam desc: - other: This question has been asked before and already has an answer. + other: Esta pergunta já foi feita antes e já tem uma resposta. guideline: name: - other: a community-specific reason + other: um motivo específico da comunidade desc: - other: This question doesn't meet a community guideline. + other: Esta pergunta não atende a uma diretriz da comunidade. multiple: name: - other: needs details or clarity + other: precisa de detalhes ou clareza desc: - other: This question currently includes multiple questions in one. It should focus on one problem only. + other: Esta pergunta atualmente inclui várias perguntas em uma. Deve se concentrar em apenas um problema. other: name: - other: something else + other: algo mais desc: - other: This post requires another reason not listed above. + other: Este post requer outro motivo não listado acima. operation_type: asked: - other: asked + other: perguntado answered: - other: answered + other: respondido modified: - other: modified + other: modificado notification: action: update_question: - other: updated question + other: pergunta atualizada answer_the_question: - other: answered question + other: pergunta respondida update_answer: - other: updated answer + other: resposta atualizada accept_answer: - other: accepted answer + other: resposta aceita comment_question: - other: commented question + other: pergunta comentada comment_answer: - other: commented answer + other: resposta comentada reply_to_you: - other: replied to you + other: respondeu a você mention_you: - other: mentioned you + other: mencionou você your_question_is_closed: - other: Your question has been closed + other: A sua pergunta foi fechada your_question_was_deleted: - other: Your question has been deleted + other: A sua pergunta foi deletada your_answer_was_deleted: - other: Your answer has been deleted + other: A sua resposta foi deletada your_comment_was_deleted: - other: Your comment has been deleted + other: O seu comentário foi deletado #The following fields are used for interface presentation(Front-end) ui: how_to_format: - title: How to Format + title: Como formatar desc: >- - + pagination: - prev: Prev - next: Next + prev: Anterior + next: Próximo page_title: - question: Question - questions: Questions - tag: Tag - tags: Tags - tag_wiki: tag wiki - edit_tag: Edit Tag - ask_a_question: Add Question - edit_question: Edit Question - edit_answer: Edit Answer - search: Search - posts_containing: Posts containing - settings: Settings - notifications: Notifications - login: Log In - sign_up: Sign Up - account_recovery: Account Recovery - account_activation: Account Activation - confirm_email: Confirm Email - account_suspended: Account Suspended - admin: Admin - change_email: Modify Email - install: Answer Installation - upgrade: Answer Upgrade - maintenance: Website Maintenance - users: Users + question: Pergunta + questions: Perguntas + tag: Marcador + tags: Marcadores + tag_wiki: marcador wiki + edit_tag: Editar Marcador + ask_a_question: Adicionar Pergunta + edit_question: Editar Pergunta + edit_answer: Editar Resposta + search: Busca + posts_containing: Postagens contendo + settings: Configurações + notifications: Notificações + login: Entrar + sign_up: Criar conta + account_recovery: Recuperação de conta + account_activation: Ativação de conta + confirm_email: Confirmação de e-mail + account_suspended: Conta suspensa + admin: Administrador + change_email: Modificar e-mail + install: Instalação do Resposta + upgrade: Atualização do Resposta + maintenance: Manutençã do Website + users: Usuários notifications: - title: Notifications - inbox: Inbox - achievement: Achievements - all_read: Mark all as read - show_more: Show more + title: Notificações + inbox: Caixa de entrada + achievement: Conquistas + all_read: Marcar todas como lidas + show_more: Mostrar mais suspended: - title: Your Account has been Suspended - until_time: "Your account was suspended until {{ time }}." - forever: This user was suspended forever. - end: You don't meet a community guideline. + title: A sua conta foi suspensa + until_time: "A sua conta foi suspensa até {{ time }}." + forever: Este usuário foi suspenso por tempo indeterminado. + end: Você não atende a uma diretriz da comunidade. editor: blockquote: - text: Blockquote + text: Bloco de citação bold: - text: Strong + text: Forte chart: - text: Chart - flow_chart: Flow chart - sequence_diagram: Sequence diagram - class_diagram: Class diagram - state_diagram: State diagram - entity_relationship_diagram: Entity relationship diagram - user_defined_diagram: User defined diagram - gantt_chart: Gantt chart - pie_chart: Pie chart + text: Gráfico + flow_chart: Fluxograma + sequence_diagram: Diagrama de sequência + class_diagram: Diagrama de classe + state_diagram: Diagrama de estado + entity_relationship_diagram: Diagrama de relacionamento de entidade + user_defined_diagram: Diagrama definido pelo usuário + gantt_chart: Diagrama de Gantt + pie_chart: Gráfico de pizza code: - text: Code Sample - add_code: Add code sample + text: Código de exemplo + add_code: Adicione código de exemplo form: fields: code: - label: Code + label: Código msg: - empty: Code cannot be empty. + empty: Código não pode ser vazio. language: - label: Language (optional) - placeholder: Automatic detection - btn_cancel: Cancel - btn_confirm: Add + label: Idioma (opcional) + placeholder: Tetecção automática + btn_cancel: Cancelar + btn_confirm: Adicionar formula: - text: Formula + text: Fórmula options: - inline: Inline formula - block: Block formula - heading: - text: Heading + inline: Fórmula em linha + block: Fórmula em bloco + Cabeçalho: + text: Cabeçalho options: - h1: Heading 1 - h2: Heading 2 - h3: Heading 3 - h4: Heading 4 - h5: Heading 5 - h6: Heading 6 + h1: Cabeçalho 1 + h2: Cabeçalho 2 + h3: Cabeçalho 3 + h4: Cabeçalho 4 + h5: Cabeçalho 5 + h6: Cabeçalho 6 help: - text: Help + text: Ajuda hr: - text: Horizontal Rule + text: Régua horizontal image: - text: Image - add_image: Add image - tab_image: Upload image + text: Imagem + add_image: Adicionar imagem + tab_image: Enviar image, form_image: fields: file: - label: Image File - btn: Select image + label: Arquivo de imagem + btn: Selecione imagem msg: - empty: File cannot be empty. - only_image: Only image files are allowed. - max_size: File size cannot exceed 4MB. + empty: Arquivo não pode ser vazio. + only_image: Somente um arquivo de imagem é permitido. + max_size: O tamanho do arquivo não pode exceder 4MB. desc: - label: Description (optional) - tab_url: Image URL + label: Descrição (opcional) + tab_url: URL da imagem form_url: fields: url: - label: Image URL + label: URL da imagem msg: - empty: Image URL cannot be empty. + empty: URL da imagem não pode ser vazia. name: - label: Description (optional) - btn_cancel: Cancel - btn_confirm: Add - uploading: Uploading + label: Descrição (opcional) + btn_cancel: Cancelar + btn_confirm: Adicionar + uploading: Enviando indent: - text: Indent + text: Identação outdent: - text: Outdent + text: Não identado italic: - text: Emphasis + text: Emphase link: - text: Hyperlink - add_link: Add hyperlink + text: Superlink (Hyperlink) + add_link: Adicionar superlink (hyperlink) form: fields: url: label: URL msg: - empty: URL cannot be empty. + empty: URL não pode ser vazia. name: - label: Description (optional) - btn_cancel: Cancel - btn_confirm: Add + label: Descrição (opcional) + btn_cancel: Cancelar + btn_confirm: Adicionar ordered_list: - text: Numbered List + text: Lista numerada unordered_list: - text: Bulleted List + text: Lista com marcadores table: - text: Table - heading: Heading - cell: Cell + text: Tabela + Cabeçalho: Cabeçalho + cell: Célula close_modal: - title: I am closing this post as... - btn_cancel: Cancel - btn_submit: Submit + title: Estou fechando este post como... + btn_cancel: Cancelar + btn_submit: Enviar remark: - empty: Cannot be empty. + empty: Não pode ser vazio. msg: - empty: Please select a reason. + empty: Por favor selecione um motivo. report_modal: flag_title: I am flagging to report this post as... - close_title: I am closing this post as... - review_question_title: Review question - review_answer_title: Review answer - review_comment_title: Review comment - btn_cancel: Cancel - btn_submit: Submit + close_title: Estou fechando este post como... + review_question_title: Revisar pergunta + review_answer_title: Revisar resposta + review_comment_title: Revisar comentário + btn_cancel: Cancelar + btn_submit: Enviar remark: - empty: Cannot be empty. + empty: Não pode ser vazio. msg: - empty: Please select a reason. + empty: Por favor selecione um motivo. tag_modal: - title: Create new tag + title: Criar novo marcador form: fields: display_name: - label: Display Name + label: Nome de exibição msg: - empty: Display name cannot be empty. - range: Display name up to 35 characters. + empty: Nome de exibição não pode ser vazio. + range: Nome de exibição tem que ter até 35 caracteres. slug_name: - label: URL Slug - desc: 'Must use the character set "a-z", "0-9", "+ # - ."' + label: Slug de URL + desc: 'Deve usar o conjunto de caracteres "a-z", "0-9", "+ # - ."' msg: - empty: URL slug cannot be empty. - range: URL slug up to 35 characters. - character: URL slug contains unallowed character set. + empty: URL slug não pode ser vazio. + range: URL slug até 35 caracteres. + character: URL slug contém conjunto de caracteres não permitido. desc: - label: Description (optional) - btn_cancel: Cancel - btn_submit: Submit + label: Descrição (opcional) + btn_cancel: Cancelar + btn_submit: Enviar tag_info: - created_at: Created - edited_at: Edited - history: History + created_at: Criado + edited_at: Editado + history: Histórico synonyms: - title: Synonyms - text: The following tags will be remapped to - empty: No synonyms found. - btn_add: Add a synonym - btn_edit: Edit - btn_save: Save - synonyms_text: The following tags will be remapped to + title: Sinônimos + text: Os marcadores a seguir serão re-mapeados para + empty: Sinônimos não encotrados. + btn_add: Adicionar um sinônimo + btn_edit: Editar + btn_save: Salvar + synonyms_text: Os marcadores a seguir serão re-mapeados para delete: - title: Delete this tag + title: Deletar este marcador content: >- -

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

- content2: Are you sure you wish to delete? - close: Close +

Não permitimos a exclusão de marcadores com postagens.

Por favor, remova este marcador das postagens primeiro.

+ content2: Você tem certeza que deseja deletar? + close: Fechar edit_tag: - title: Edit Tag - default_reason: Edit tag + title: Editar marcador + default_reason: Editar marcador form: fields: revision: - label: Revision + label: Revisão display_name: - label: Display Name + label: Nome de exibição slug_name: - label: URL Slug - info: 'Must use the character set "a-z", "0-9", "+ # - ."' + label: Slug de URL + info: 'Deve usar o conjunto de caracteres "a-z", "0-9", "+ # - ."' desc: - label: Description + label: Descrição edit_summary: - label: Edit Summary + label: Resumo da edição placeholder: >- - Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) - btn_save_edits: Save edits - btn_cancel: Cancel + Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) + btn_save_edits: Salvar edições + btn_cancel: Cancelar dates: - long_date: MMM D - long_date_with_year: "MMM D, YYYY" - long_date_with_time: "MMM D, YYYY [at] HH:mm" - now: now - x_seconds_ago: "{{count}}s ago" - x_minutes_ago: "{{count}}m ago" - x_hours_ago: "{{count}}h ago" - hour: hour - day: day + long_date: D MMM + long_date_with_year: "D MMM, YYYY" + long_date_with_time: "D MMM, YYYY [at] HH:mm" + now: agora + x_seconds_ago: "{{count}}s atrás" + x_minutes_ago: "{{count}}m atrás" + x_hours_ago: "{{count}}h atrás" + hour: hora + day: dia comment: - btn_add_comment: Add comment - reply_to: Reply to - btn_reply: Reply - btn_edit: Edit - btn_delete: Delete - btn_flag: Flag - btn_save_edits: Save edits - btn_cancel: Cancel - show_more: Show more comments + btn_add_comment: Adicionar comentário + reply_to: Responder a + btn_reply: Responder + btn_edit: Editar + btn_delete: Deletar + btn_flag: Marcador + btn_save_edits: Salvar edições + btn_cancel: Cancelar + show_more: Mostrar mais comentários tip_question: >- - Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + Use os comentários para pedir mais informações ou sugerir melhorias. Evite responder perguntas nos comentários. tip_answer: >- - Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + Use comentários para responder a outros usuários ou notificá-los sobre alterações. Se você estiver adicionando novas informações, edite sua postagem em vez de comentar. edit_answer: - title: Edit Answer - default_reason: Edit answer + title: Editar Resposta + default_reason: Editar Resposta form: fields: revision: - label: Revision + label: Revisão answer: - label: Answer + label: Resposta feedback: - characters: content must be at least 6 characters in length. + caracteres: conteúdo deve ser pelo menos 6 caracteres em comprimento. edit_summary: - label: Edit Summary + label: Resumo da edição placeholder: >- - Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) - btn_save_edits: Save edits - btn_cancel: Cancel + Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) + btn_save_edits: Salvar edições + btn_cancel: Cancelar tags: - title: Tags + title: Marcadores sort_buttons: popular: Popular - name: Name - newest: newest - button_follow: Follow - button_following: Following - tag_label: questions - search_placeholder: Filter by tag name - no_desc: The tag has no description. - more: More + name: Nome + newest: mais recente + button_follow: Seguir + button_following: Seguindo + tag_label: perguntas + search_placeholder: Filtrar por nome de marcador + no_desc: O marcador não possui descrição. + more: Mais ask: - title: Add Question - edit_title: Edit Question - default_reason: Edit question - similar_questions: Similar questions + title: Adicionar Pergunta + edit_title: Editar Pergunta + default_reason: Editar pergunta + similar_questions: Similar perguntas form: fields: revision: - label: Revision + label: Revisão title: - label: Title - placeholder: Be specific and imagine you're asking a question to another person + label: Título + placeholder: Seja específico e tenha em mente que está fazendo uma pergunta a outra pessoa msg: - empty: Title cannot be empty. - range: Title up to 150 characters + empty: Título não pode ser vazio. + range: Título até 150 caracteres body: - label: Body + label: Corpo msg: - empty: Body cannot be empty. + empty: Corpo da mensagem não pode ser vazio. tags: - label: Tags + label: Marcadores msg: - empty: Tags cannot be empty. + empty: Marcadores não podes ser vazios. answer: - label: Answer + label: Resposta msg: - empty: Answer cannot be empty. + empty: Resposta não pode ser vazia. edit_summary: - label: Edit Summary + label: Resumo da edição placeholder: >- - Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) - btn_post_question: Post your question - btn_save_edits: Save edits - answer_question: Answer your own question - post_question&answer: Post your question and answer + Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) + btn_post_question: Publicação a sua pergunta + btn_save_edits: Salvar edições + answer_question: Responda a sua própria pergunta + post_question&answer: Publicação a sua pergunta e resposta tag_selector: - add_btn: Add tag - create_btn: Create new tag - search_tag: Search tag - hint: "Describe what your question is about, at least one tag is required." - no_result: No tags matched - tag_required_text: Required tag (at least one) + add_btn: Adicionar marcador + create_btn: Criar novo marcador + search_tag: Procurar marcador + hint: "Descreva do quê se trata a sua pergunta, ao menos um marcador é requirido." + no_result: Nenhum marcador correspondente + tag_required_text: Marcador obrigatório (ao menos um) header: nav: - question: Questions - tag: Tags - user: Users - profile: Profile - setting: Settings - logout: Log out - admin: Admin - review: Review + question: Perguntas + tag: Marcadores + user: Usuários + profile: Perfil + setting: Configurações + logout: Sair + admin: Administrador + review: Revisar search: - placeholder: Search + placeholder: Procurar footer: build_on: >- Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. upload_img: - name: Change - loading: loading... + name: Mudar + loading: carregando... pic_auth_code: title: Captcha - placeholder: Type the text above + placeholder: Escreva o texto acima msg: - empty: Captcha cannot be empty. + empty: Captcha não pode ser vazio. inactive: first: >- - You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. - info: "If it doesn't arrive, check your spam folder." + Você está quase pronto! Enviamos um e-mail de ativação para {{mail}}. Por favor, siga as instruções no e-mail para ativar uma conta sua. + info: "Se não chegar, verifique sua pasta de spam." another: >- - We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + Enviamos outro e-mail de ativação para você em {{mail}}. Pode levar alguns minutos para chegar; certifique-se de verificar sua pasta de spam. btn_name: Resend activation email - change_btn_name: Change email + change_btn_name: Mudar email msg: - empty: Cannot be empty. + empty: Não pode ser vazio. login: - page_title: Welcome to {{site_name}} - login_to_continue: Log in to continue - info_sign: Don't have an account? <1>Sign up - info_login: Already have an account? <1>Log in - agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. - forgot_pass: Forgot password? + page_title: Bem vindo ao {{site_name}} + login_to_continue: Entre para continue + info_sign: Não possui uma conta? <1>Cadastrar-se + info_login: Já possui uma conta? <1>Entre + agreements: Ao se registrar, você concorda com as <1>políticas de privacidades e os <3>termos de serviços. + forgot_pass: Esqueceu a sua senha? name: - label: Name + label: Nome msg: - empty: Name cannot be empty. - range: Name up to 30 characters. + empty: Nome não pode ser vazio. + range: Nome até 30 caracteres. email: - label: Email + label: E-mail msg: - empty: Email cannot be empty. + empty: E-mail não pode ser vazio. password: - label: Password + label: Senha msg: - empty: Password cannot be empty. - different: The passwords entered on both sides are inconsistent + empty: Senha não pode ser vazia. + different: As senhas inseridas em ambos os campos são inconsistentes account_forgot: - page_title: Forgot Your Password - btn_name: Send me recovery email + page_title: Esqueceu a sua senha + btn_name: Enviar e-mail de recuperação de senha send_success: >- - If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. email: - label: Email + label: E-mail msg: - empty: Email cannot be empty. + empty: E-mail não pode ser vazio. change_email: - page_title: Welcome to {{site_name}} - btn_cancel: Cancel - btn_update: Update email address + page_title: Bem vindo ao {{site_name}} + btn_cancel: Cancelar + btn_update: Atualiza email address send_success: >- - If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. email: - label: New Email + label: Novo E-mail msg: - empty: Email cannot be empty. + empty: E-mail não pode ser vazio. password_reset: - page_title: Password Reset - btn_name: Reset my password + page_title: Redefinir senha + btn_name: Redefinir minha senha reset_success: >- - You successfully changed your password; you will be redirected to the log in page. + Você alterou com sucesso uma senha sua; você será redirecionado para a página de login. link_invalid: >- - Sorry, this password reset link is no longer valid. Perhaps your password is already reset? - to_login: Continue to log in page + Desculpe, este link de redefinição de senha não é mais válido. Talvez uma senha sua já tenha sido redefinida? + to_login: Continuar para a tela de login password: - label: Password + label: Senha msg: - empty: Password cannot be empty. - length: The length needs to be between 8 and 32 - different: The passwords entered on both sides are inconsistent + empty: Senha não pode ser vazio. + length: O comprimento deve estar entre 8 e 32 + different: As senhas inseridas em ambos os campos são inconsistentes password_confirm: - label: Confirm New Password + label: Confirmar Nova senha settings: - page_title: Settings + page_title: Configurações nav: - profile: Profile - notification: Notifications - account: Account + profile: Perfil + notification: Notificações + account: Conta interface: Interface profile: - heading: Profile - btn_name: Save + Cabeçalho: Perfil + btn_name: Salvar display_name: - label: Display Name - msg: Display name cannot be empty. - msg_range: Display name up to 30 characters. + label: Nome de exibição + msg: Nome de exibição não pode ser vazio. + msg_range: Nome de exibição tem que ter até 30 caracteres. username: - label: Username - caption: People can mention you as "@username". - msg: Username cannot be empty. - msg_range: Username up to 30 characters. - character: 'Must use the character set "a-z", "0-9", " - . _"' + label: Nome de usuário + caption: As pessoas poderão mensionar você com "@usuário". + msg: Nome de usuário não pode ser vazio. + msg_range: Nome de usuário até 30 caracteres. + character: 'Deve usar o conjunto de caracteres "a-z", "0-9", " - . _"' avatar: - label: Profile Image + label: Perfil Imagem gravatar: Gravatar - gravatar_text: You can change image on <1>gravatar.com - custom: Custom - btn_refresh: Refresh - custom_text: You can upload your image. + gravatar_text: Você pode mudar a imagem em <1>gravatar.com + custom: Customizado + btn_refresh: Atualizar + custom_text: Você pode enviar a sua image. default: System - msg: Please upload an avatar + msg: Por favor envie um avatar bio: - label: About Me (optional) + label: Sobre mim (opcional) website: - label: Website (optional) - placeholder: "https://example.com" - msg: Website incorrect format + label: Website (opcional) + placeholder: "https://exemplo.com.br" + msg: Formato incorreto de endereço de Website location: - label: Location (optional) - placeholder: "City, Country" + label: Localização (opcional) + placeholder: "Cidade, País" notification: - heading: Notifications + Cabeçalho: Notificações email: - label: Email Notifications - radio: "Answers to your questions, comments, and more" + label: E-mail Notificações + radio: "Responda as suas perguntas, comentários, e mais" account: - heading: Account - change_email_btn: Change email - change_pass_btn: Change password + Cabeçalho: Conta + change_email_btn: Mudar e-mail + change_pass_btn: Mudar senha change_email_info: >- - We've sent an email to that address. Please follow the confirmation instructions. + Enviamos um e-mail para esse endereço. Siga as instruções de confirmação. email: - label: Email - msg: Email cannot be empty. - password_title: Password + label: E-mail + msg: E-mail não pode ser vazio. + password_title: Senha current_pass: - label: Current Password + label: Senha atual msg: - empty: Current Password cannot be empty. - length: The length needs to be between 8 and 32. - different: The two entered passwords do not match. + empty: A Senha não pode ser vazia. + length: O comprimento deve estar entre 8 and 32. + different: As duas senhas inseridas não correspondem. new_pass: - label: New Password + label: Nova Senha pass_confirm: - label: Confirm New Password + label: Confirmar nova Senha interface: - heading: Interface + Cabeçalho: Interface lang: - label: Interface Language - text: User interface language. It will change when you refresh the page. + label: Idioma da Interface + text: Idioma da interface do usuário. A interface mudará quando você atualizar a página. toast: - update: update success - update_password: Password changed successfully. - flag_success: Thanks for flagging. - forbidden_operate_self: Forbidden to operate on yourself - review: Your revision will show after review. + update: atualização realizada com sucesso + update_password: Senha alterada com sucesso. + flag_success: Obrigado por marcar. + forbidden_operate_self: Proibido para operar por você mesmo + review: A sua resposta irá aparecer após a revisão. related_question: - title: Related Questions - btn: Add question - answers: answers + title: Perguntas relacionadas + btn: Adicionar pegunta + answers: respostas question_detail: - Asked: Asked - asked: asked - update: Modified - edit: edited - Views: Viewed - Follow: Follow - Following: Following - answered: answered - closed_in: Closed in - show_exist: Show existing question. + Perguntado: Perguntado + asked: perguntado + update: Modificado + edit: modificado + Views: Visualizado + Seguir: Seguir + Seguindo: Seguindo + answered: respondido + closed_in: Fechado em + show_exist: Mostrar pergunta existente. answers: - title: Answers - score: Score - newest: Newest - btn_accept: Accept - btn_accepted: Accepted + title: Respostas + score: Pontuação + newest: Mais recente + btn_accept: Aceito + btn_accepted: Aceito write_answer: - title: Your Answer - btn_name: Post your answer - add_another_answer: Add another answer - confirm_title: Continue to answer - continue: Continue + title: A sua Resposta + btn_name: Publicação a sua resposta + add_another_answer: Adicionar outra resposta + confirm_title: Continuar a responder + continue: Continuar confirm_info: >- -

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

- empty: Answer cannot be empty. - characters: content must be at least 6 characters in length. +

Tem certeza de que deseja adicionar outra resposta?

Você pode usar o link de edição para refinar e melhorar uma resposta existente.

+ empty: Resposta não pode ser vazio. + caracteres: conteúdo deve ser pelo menos 6 caracteres em comprimento. reopen: - title: Reopen this post - content: Are you sure you want to reopen? - success: This post has been reopened + title: Reabrir esta postagem + content: Tem certeza que deseja reabrir? + success: Esta postagem foi reaberta delete: - title: Delete this post + title: Excluir esta postagem question: >- - We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + Nós não recomendamos excluindo perguntas com respostas porque isso priva os futuros leitores desse conhecimento.

Repeated deletion of answered questions can result in a sua account being blocked from asking. Você tem certeza que deseja deletar? answer_accepted: >- -

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? - other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted - tip_answer_deleted: This answer has been deleted +

Nós não recomendamos deleting accepted answer porque isso priva os futuros leitores desse conhecimento.

Repeated deletion of accepted answers can result in a sua account being blocked from answering. Você tem certeza que deseja deletar? + other: Você tem certeza que deseja deletar? + tip_question_deleted: Esta postagem foi deletada + tip_answer_deleted: Esta resposta foi deletada btns: - confirm: Confirm - cancel: Cancel - save: Save - delete: Delete - login: Log in - signup: Sign up - logout: Log out - verify: Verify - add_question: Add question - approve: Approve - reject: Reject - skip: Skip + confirm: Confirmar + cancel: Cancelar + save: Salvar + delete: Deletar + login: Entrar + signup: Cadastrar-se + logout: Sair + verify: Verificar + add_question: Adicionar pergunta + approve: Aprovar + reject: Rejetar + skip: Pular search: - title: Search Results - keywords: Keywords - options: Options - follow: Follow - following: Following - counts: "{{count}} Results" - more: More + title: Procurar Resultados + keywords: Palavras-chave + options: Opções + follow: Seguir + following: Seguindo + counts: "{{count}} Resultados" + more: Mais sort_btns: - relevance: Relevance - newest: Newest - active: Active - score: Score - more: More + relevance: Relevância + newest: Mais recente + active: Ativar + score: Pontuação + more: Mais tips: - title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" - user: "<1>user:username search by author" - answer: "<1>answers:0 unanswered questions" - score: "<1>score:3 posts with a 3+ score" - question: "<1>is:question search questions" - is_answer: "<1>is:answer search answers" - empty: We couldn't find anything.
Try different or less specific keywords. + title: Dicas de Pesquisa Avançada + tag: "<1>[tag] pesquisar dentro de um marcador" + user: "<1>user:username buscar por autor" + answer: "<1>answers:0 perguntas não respondidas" + score: "<1>score:3 postagens com mais de 3+ placares" + question: "<1>is:question buscar perguntas" + is_answer: "<1>is:answer buscar respostas" + empty: Não conseguimos encontrar nada.
Tente palavras-chave diferentes ou menos específicas. share: - name: Share - copy: Copy link - via: Share post via... - copied: Copied - facebook: Share to Facebook - twitter: Share to Twitter - cannot_vote_for_self: You can't vote for your own post + name: Compartilhar + copy: Copiar link + via: Compartilhar postagem via... + copied: Copiado + facebook: Compartilhar no Facebook + twitter: Compartilhar no Twitter + cannot_vote_for_self: Você não pode votar na sua própria postagem modal_confirm: - title: Error... + title: Erro... account_result: - page_title: Welcome to {{site_name}} - success: Your new account is confirmed; you will be redirected to the home page. - link: Continue to homepage + page_title: Bem vindo ao {{site_name}} + success: A sua nova conta está confirmada; você será redirecionado para a página inicial. + link: Continuar para a página inicial. invalid: >- - Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + Desculpe, este link de confirmação não é mais válido. Talvez a sua já está ativa. confirm_new_email: Your email has been updated. confirm_new_email_invalid: >- - Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + Desculpe, este link de confirmação não é mais válido. Talvez o seu e-mail já tenha sido alterado. unsubscribe: - page_title: Unsubscribe - success_title: Unsubscribe Successful - success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. - link: Change settings + page_title: Cancelar subscrição + success_title: Cancelamento de inscrição bem-sucedido + success_desc: Você foi removido com sucesso desta lista de assinantes e não receberá mais nenhum e-mail nosso. + link: Mudar configurações question: - following_tags: Following Tags - edit: Edit - save: Save - follow_tag_tip: Follow tags to curate your list of questions. - hot_questions: Hot Questions - all_questions: All Questions - x_questions: "{{ count }} Questions" - x_answers: "{{ count }} answers" - questions: Questions - answers: Answers - newest: Newest - active: Active - frequent: Frequent - score: Score - unanswered: Unanswered - modified: modified - answered: answered - asked: asked - closed: closed - follow_a_tag: Follow a tag - more: More + following_tags: Seguindo Marcadores + edit: Editar + save: Salvar + follow_tag_tip: Seguir tags to curate a sua lista de perguntas. + hot_questions: Perguntas quentes + all_questions: Todas Perguntas + x_questions: "{{ count }} perguntas" + x_answers: "{{ count }} respostas" + questions: Perguntas + answers: Respostas + newest: Mais recente + active: Ativo + frequent: Frequente + score: Pontuação + unanswered: Não Respondido + modified: modificado + answered: respondido + asked: perguntado + closed: fechado + follow_a_tag: Seguir o marcador + more: Mais personal: - overview: Overview - answers: Answers - answer: answer - questions: Questions - question: question - bookmarks: Bookmarks - reputation: Reputation - comments: Comments - votes: Votes - newest: Newest - score: Score - edit_profile: Edit Profile - visited_x_days: "Visited {{ count }} days" + overview: Visão geral + answers: Respostas + answer: resposta + questions: Perguntas + question: pergunta + bookmarks: Favoritas + reputation: Reputação + comments: Comentários + Votos: Votos + newest: Mais recente + score: Pontuação + edit_profile: Editar Perfil + visited_x_days: "Visitado {{ count }} dias" viewed: Viewed - joined: Joined - last_login: Seen - about_me: About Me - about_me_empty: "// Hello, World !" - top_answers: Top Answers - top_questions: Top Questions - stats: Stats - list_empty: No posts found.
Perhaps you'd like to select a different tab? - accepted: Accepted - answered: answered - asked: asked - upvote: upvote - downvote: downvote - mod_short: Mod - mod_long: Moderators - x_reputation: reputation - x_votes: votes received - x_answers: answers - x_questions: questions + joined: Ingressou + last_login: Visto + about_me: Sobre mim + about_me_empty: "// Olá, Mundo !" + top_answers: Melhores Respostas + top_questions: Melhores Perguntas + stats: Estatísticas + list_empty: Postagens não encontradas.
Talvez você queira selecionar uma guia diferente? + accepted: Aceito + answered: respondido + asked: perguntado + upvote: voto positivo + downvote: voto negativo + mod_short: Moderador + mod_long: Moderadores + x_reputation: reputação + x_Votos: votos recebidos + x_answers: respostas + x_questions: perguntas install: - title: Answer - next: Next - done: Done - config_yaml_error: Can't create the config.yaml file. + title: Resposta + next: Proximo + done: Completo + config_yaml_error: Não é possível criar o arquivo config.yaml. lang: - label: Please Choose a Language + label: Por favor Escolha um Idioma db_type: label: Database Engine db_username: - label: Username + label: Nome de usuário placeholder: root - msg: Username cannot be empty. + msg: Nome de usuário não pode ser vazio. db_password: - label: Password + label: Senha placeholder: root - msg: Password cannot be empty. + msg: Senha não pode ser vazio. db_host: label: Database Host placeholder: "db:3306" - msg: Database Host cannot be empty. + msg: Database Host não pode ser vazio. db_name: - label: Database Name + label: Database Nome placeholder: answer - msg: Database Name cannot be empty. + msg: Database Nome não pode ser vazio. db_file: label: Database File placeholder: /data/answer.db - msg: Database File cannot be empty. + msg: Database File não pode ser vazio. config_yaml: title: Create config.yaml label: The config.yaml file created. @@ -917,37 +917,37 @@ ui: You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information - admin_account: Admin Account + admin_account: Administrador Conta site_name: - label: Site Name - msg: Site Name cannot be empty. + label: Site Nome + msg: Site Nome não pode ser vazio. site_url: label: Site URL - text: The address of your site. + text: The address of a sua site. msg: - empty: Site URL cannot be empty. + empty: Site URL não pode ser vazio. incorrect: Site URL incorrect format. contact_email: - label: Contact Email + label: E-mail par contato text: Email address of key contact responsible for this site. msg: - empty: Contact Email cannot be empty. - incorrect: Contact Email incorrect format. + empty: E-mail par contato não pode ser vazio. + incorrect: E-mail par contato incorrect format. admin_name: - label: Name - msg: Name cannot be empty. + label: Nome + msg: Nome não pode ser vazio. admin_password: - label: Password + label: Senha text: >- - You will need this password to log in. Please store it in a secure location. - msg: Password cannot be empty. + You will need this password to log in. Por favor store it in a secure location. + msg: Senha não pode ser vazio. admin_email: label: Email text: You will need this email to log in. msg: - empty: Email cannot be empty. + empty: Email não pode ser vazio. incorrect: Email incorrect format. - ready_title: Your Answer is Ready! + ready_title: Your Resposta is Ready! ready_desc: >- If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: "Have fun, and good luck!" @@ -957,272 +957,272 @@ ui: install_now: You may try <1>installing now. installed: Already installed installed_desc: >- - You appear to have already installed. To reinstall please clear your old database tables first. + You appear to have already installed. To reinstall please clear a sua old database tables first. db_failed: Database connection failed db_failed_desc: >- - This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down. + This either means that the database information in a sua <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean a sua host’s database server is down. counts: - views: views - votes: votes - answers: answers - accepted: Accepted + views: visualizações + Votos: votos + answers: respostas + accepted: Aceito page_404: - desc: "Unfortunately, this page doesn't exist." - back_home: Back to homepage + desc: "Infelizmente, esta postagem não existe mais." + back_home: Voltar para a página inicial page_50X: - desc: The server encountered an error and could not complete your request. - back_home: Back to homepage + desc: O servidor encontrou um erro e não pôde concluir uma solicitação sua. + back_home: Voltar para a página inicial page_maintenance: - desc: "We are under maintenance, we'll be back soon." + desc: "Estamos em manutenção, voltaremos em breve." nav_menus: - dashboard: Dashboard - contents: Contents - questions: Questions - answers: Answers - users: Users - flags: Flags - settings: Settings - general: General + dashboard: Painel + contents: Conteúdos + questions: Perguntas + answers: Respostas + users: Usuários + flags: Marcadores + settings: Configurações + general: Geral interface: Interface smtp: SMTP - branding: Branding + branding: Marca legal: Legal - write: Write - tos: Terms of Service - privacy: Privacy + write: Escrever + tos: Termos de Serviços + privacy: Privacidade seo: SEO - customize: Customize - themes: Themes + customize: Personalização + Temas: Temas css-html: CSS/HTML login: Login admin: admin_header: - title: Admin + title: Administrador dashboard: - title: Dashboard - welcome: Welcome to Answer Admin! - site_statistics: Site Statistics - questions: "Questions:" - answers: "Answers:" - comments: "Comments:" - votes: "Votes:" - active_users: "Active users:" - flags: "Flags:" - site_health_status: Site Health Status - version: "Version:" + title: Painel + welcome: Bem vindo ao Answer Administrador! + site_statistics: Estatísticas do Site + questions: "Perguntas:" + answers: "Respostas:" + comments: "Comentários:" + Votos: "Votos:" + active_users: "Usuários ativos:" + flags: "Marcadores:" + site_health_status: Estatísticas da saúde do site + version: "Versão:" https: "HTTPS:" - uploading_files: "Uploading files:" + uploading_files: "Enviando arquivos:" smtp: "SMTP:" - timezone: "Timezone:" - system_info: System Info - storage_used: "Storage used:" - uptime: "Uptime:" - answer_links: Answer Links - documents: Documents - feedback: Feedback - support: Support - review: Review - config: Config - update_to: Update to - latest: Latest - check_failed: Check failed - "yes": "Yes" - "no": "No" - not_allowed: Not allowed - allowed: Allowed - enabled: Enabled - disabled: Disabled + timezone: "Fuso horário:" + system_info: Informações do Sistema + storage_used: "Armazenamento usado:" + uptime: "Tempo de atividade:" + answer_links: Links das Respostas + documents: Documentos + feedback: Opinião + support: Supporte + review: Revisar + config: Configurações + update_to: Atualizar ao + latest: Ultimo + check_failed: Falha na verificação + "yes": "Sim" + "no": "Não" + not_allowed: Não permitido + allowed: Permitido + enabled: Ativo + disabled: Disponível flags: - title: Flags - pending: Pending - completed: Completed - flagged: Flagged - created: Created - action: Action - review: Review + title: Marcadores + pending: Pendente + completed: Completo + flagged: Marcado + created: Criado + action: Ação + review: Revisar change_modal: - title: Change user status to... - btn_cancel: Cancel - btn_submit: Submit + title: Mudar user status to... + btn_cancel: Cancelar + btn_submit: Enviar normal_name: normal - normal_desc: A normal user can ask and answer questions. - suspended_name: suspended - suspended_desc: A suspended user can't log in. - deleted_name: deleted - deleted_desc: "Delete profile, authentication associations." - inactive_name: inactive - inactive_desc: An inactive user must re-validate their email. - confirm_title: Delete this user - confirm_content: Are you sure you want to delete this user? This is permanent! - confirm_btn: Delete + normal_desc: Um usuário normal pode fazer e responder perguntas. + suspended_name: suspenso + suspended_desc: Um usuário suspenso não pode fazer login. + deleted_name: removido + deleted_desc: "Deletar perfil, associações de autenticação." + inactive_name: inativo + inactive_desc: Um usuário inativo deve revalidar seu e-mail. + confirm_title: Remover este usuário + confirm_content: Tem certeza de que deseja excluir este usuário? Isso é permanente! + confirm_btn: Deletar msg: - empty: Please select a reason. + empty: Por favor selecione um motivo. status_modal: - title: "Change {{ type }} status to..." + title: "Mudar {{ type }} status para..." normal_name: normal - normal_desc: A normal post available to everyone. - closed_name: closed - closed_desc: "A closed question can't answer, but still can edit, vote and comment." - deleted_name: deleted - deleted_desc: All reputation gained and lost will be restored. - btn_cancel: Cancel - btn_submit: Submit - btn_next: Next + normal_desc: Um post normal disponível para todos. + closed_name: fechado + closed_desc: "Uma pergunta fechada não pode responder, mas ainda pode editar, votar e comentar." + deleted_name: removido + deleted_desc: Toda reputação ganha e perdida será restaurada. + btn_cancel: Cancelar + btn_submit: Enviar + btn_next: Próximo user_role_modal: - title: Change user role to... - btn_cancel: Cancel - btn_submit: Submit + title: Altere a função do usuário para... + btn_cancel: Cancelar + btn_submit: Enviar users: - title: Users - name: Name + title: Usuários + name: Nome email: Email - reputation: Reputation - created_at: Created Time - delete_at: Deleted Time - suspend_at: Suspended Time + reputation: Reputação + created_at: Hora de criação + delete_at: Hora da remoção + suspend_at: Hora da suspensão status: Status - role: Role - action: Action - change: Change - all: All - staff: Staff - inactive: Inactive - suspended: Suspended - deleted: Deleted + role: Função + action: Ação + change: Mudar + all: Todos + staff: Funcionários + inactive: Inativo + suspended: Suspenso + deleted: Removido normal: Normal - Moderator: Moderator - Admin: Admin - User: User + Moderador: Moderador + Administrador: Administrador + Usuário: Usuário filter: - placeholder: "Filter by name, user:id" - set_new_password: Set new password - change_status: Change status - change_role: Change role - show_logs: Show logs - add_user: Add user + placeholder: "Filtrar por nome, user:id" + set_new_password: Configurar nova senha + change_status: Mudar status + change_role: Mudar função + show_logs: Mostrar registros + add_user: Adicionar usuário new_password_modal: - title: Set new password + title: Configurar nova senha form: fields: password: - label: Password - text: The user will be logged out and need to login again. - msg: Password must be at 8-32 characters in length. - btn_cancel: Cancel - btn_submit: Submit + label: Senha + text: O usuário será desconectado e precisará fazer login novamente. + msg: Senha de ver entre 8-32 caracteres em comprimento. + btn_cancel: Cancelar + btn_submit: Enviar user_modal: - title: Add new user + title: Adicionar novo usuário form: fields: display_name: - label: Display Name - msg: Display Name must be at 3-30 characters in length. + label: Nome de exibição + msg: Nome de exibição deve ser 3-30 caracteres em comprimento. email: label: Email - msg: Email is not valid. + msg: Email não é válido. password: - label: Password - msg: Password must be at 8-32 characters in length. - btn_cancel: Cancel - btn_submit: Submit + label: Senha + msg: Senha deve ser 8-32 caracteres em comprimento. + btn_cancel: Cancelar + btn_submit: Enviar questions: - page_title: Questions + page_title: Perguntas normal: Normal - closed: Closed - deleted: Deleted - post: Post - votes: Votes - answers: Answers - created: Created + closed: Fechado + deleted: Removido + post: Publicação + Votos: Votos + answers: Respostas + created: Criado status: Status - action: Action - change: Change + action: Ação + change: Mudar filter: - placeholder: "Filter by title, question:id" + placeholder: "Filtrar por título, question:id" answers: - page_title: Answers + page_title: Respostas normal: Normal - deleted: Deleted - post: Post - votes: Votes - created: Created + deleted: Removido + post: Publicação + Votos: Votos + created: Criado status: Status - action: Action - change: Change + action: Ação + change: Mudar filter: - placeholder: "Filter by title, answer:id" + placeholder: "Filtrar por título, answer:id" general: page_title: General name: - label: Site Name - msg: Site name cannot be empty. - text: "The name of this site, as used in the title tag." + label: Site Nome + msg: Site name não pode ser vazio. + text: "O nome deste site, conforme usado na tag de título." site_url: - label: Site URL - msg: Site url cannot be empty. - validate: Please enter a valid URL. - text: The address of your site. + label: URL do Site + msg: Site url não pode ser vazio. + validate: Por favor digite uma URL válida. + text: O endereço do seu site. short_desc: - label: Short Site Description (optional) - msg: Short site description cannot be empty. - text: "Short description, as used in the title tag on homepage." + label: Breve Descrição do site (opcional) + msg: Breve Descrição do site não pode ser vazio. + text: "Breve descrição, conforme usado na tag de título na página inicial." desc: - label: Site Description (optional) - msg: Site description cannot be empty. - text: "Describe this site in one sentence, as used in the meta description tag." + label: Site Descrição (opcional) + msg: Descrição do site não pode ser vazio. + text: "Descreva este site em uma única sentença, conforme usado na meta tag de descrição." contact_email: - label: Contact Email - msg: Contact email cannot be empty. - validate: Contact email is not valid. - text: Email address of key contact responsible for this site. + label: E-mail para contato + msg: E-mail par contato não pode ser vazio. + validate: E-mail par contato não é válido. + text: Endereço de e-mail do principal contato responsável por este site. interface: page_title: Interface logo: - label: Logo (optional) - msg: Site logo cannot be empty. - text: You can upload your image or <1>reset it to the site title text. - theme: - label: Theme - msg: Theme cannot be empty. - text: Select an existing theme. + label: Logo (opcional) + msg: Site logo não pode ser vazio. + text: Você pode enviar a sua image ou <1>reiniciar ao texto do título do site. + Tema: + label: Tema + msg: Tema não pode ser vazio. + text: Selecione um tema existente. language: - label: Interface Language - msg: Interface language cannot be empty. - text: User interface language. It will change when you refresh the page. + label: Idioma da interface + msg: Idioma da Interface não pode ser vazio. + text: Idioma da interface do Usuário. Ele mudará quando você atualizar a página. time_zone: - label: Timezone - msg: Timezone cannot be empty. - text: Choose a city in the same timezone as you. + label: Fuso horário + msg: Fuso horário não pode ser vazio. + text: Escolha a cidade no mesmo fuso horário que você. smtp: page_title: SMTP from_email: - label: From Email - msg: From email cannot be empty. - text: The email address which emails are sent from. + label: E-mail de origem + msg: E-mail de origem não pode ser vazio. + text: O endereço de e-mail de onde os e-mails são enviados. from_name: - label: From Name - msg: From name cannot be empty. - text: The name which emails are sent from. + label: Nome de origem + msg: Nome de origem não pode ser vazio. + text: O nome de onde os e-mails são enviados. smtp_host: label: SMTP Host - msg: SMTP host cannot be empty. - text: Your mail server. + msg: SMTP host não pode ser vazio. + text: O seu servidor de e-mails. encryption: - label: Encryption - msg: Encryption cannot be empty. - text: For most servers SSL is the recommended option. + label: Criptografia + msg: Criptografia não pode ser vazio. + text: Para a maioria dos servidores SSL é a opção recomendada. ssl: SSL none: None smtp_port: label: SMTP Port msg: SMTP port must be number 1 ~ 65535. - text: The port to your mail server. + text: The port to a sua mail server. smtp_username: - label: SMTP Username - msg: SMTP username cannot be empty. + label: SMTP Nome de usuário + msg: SMTP username não pode ser vazio. smtp_password: - label: SMTP Password - msg: SMTP password cannot be empty. + label: SMTP Senha + msg: SMTP password não pode ser vazio. test_email_recipient: label: Test Email Recipients text: Provide email address that will receive test sends. @@ -1230,25 +1230,25 @@ ui: smtp_authentication: label: Enable authentication title: SMTP Authentication - msg: SMTP authentication cannot be empty. + msg: SMTP authentication não pode ser vazio. "yes": "Yes" "no": "No" branding: page_title: Branding logo: - label: Logo (optional) - msg: Logo cannot be empty. - text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + label: Logo (opcional) + msg: Logo não pode ser vazio. + text: The logo image at the top left of a sua site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. mobile_logo: - label: Mobile Logo (optional) - text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + label: Mobile Logo (opcional) + text: The logo used on mobile version of a sua site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. square_icon: - label: Square Icon (optional) - msg: Square icon cannot be empty. - text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + label: Square Icon (opcional) + msg: Square icon não pode ser vazio. + text: Imagem used as the base for metadata icons. Should ideally be larger than 512x512. favicon: - label: Favicon (optional) - text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + label: Favicon (opcional) + text: A favicon for a sua site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. legal: page_title: Legal terms_of_service: @@ -1260,34 +1260,34 @@ ui: write: page_title: Write recommend_tags: - label: Recommend Tags - text: "Please input tag slug above, one tag per line." + label: Recommend Marcadores + text: "Por favor input tag slug above, one tag per line." required_tag: title: Required Tag - label: Set recommend tag as required - text: "Every new question must have at least one recommend tag." + label: Set recommend tag as requirido + text: "Every new question must have ao menos one recommend tag." reserved_tags: - label: Reserved Tags + label: Reserved Marcadores text: "Reserved tags can only be added to a post by moderator." seo: page_title: SEO permalink: label: Permalink - text: Custom URL structures can improve the usability, and forward-compatibility of your links. + text: Custom URL structures can improve the usability, and forward-compatibility of a sua links. robots: label: robots.txt text: This will permanently override any related site settings. - themes: - page_title: Themes - themes: - label: Themes - text: Select an existing theme. + Temas: + page_title: Temas + Temas: + label: Temas + text: Select an existing Tema. navbar_style: label: Navbar Style - text: Select an existing theme. + text: Select an existing Tema. primary_color: label: Primary Color - text: Modify the colors used by your themes + text: Modify the colors used by a sua Temas css_and_html: page_title: CSS and HTML custom_css: @@ -1310,23 +1310,23 @@ ui: text: Turn off to prevent anyone from creating a new account. private: title: Private - label: Login required + label: Login requirido text: Only logged in users can access this community. form: - empty: cannot be empty + empty: não pode ser vazio invalid: is invalid - btn_submit: Save + btn_submit: Salvar not_found_props: "Required property {{ key }} not found." page_review: - review: Review + review: Revisar proposed: proposed - question_edit: Question edit - answer_edit: Answer edit + question_edit: Pergunta edit + answer_edit: Resposta edit tag_edit: Tag edit - edit_summary: Edit summary - edit_question: Edit question - edit_answer: Edit answer - edit_tag: Edit tag + edit_summary: Editar summary + edit_question: Editar question + edit_answer: Editar answer + edit_tag: Editar tag empty: No review tasks left. timeline: undeleted: undeleted @@ -1343,9 +1343,9 @@ ui: closed: closed reopened: reopened created: created - title: "History for" + title: "Histórico for" tag_title: "Timeline for" - show_votes: "Show votes" + show_Votos: "Show Votos" n_or_a: N/A title_for_question: "Timeline for" title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" @@ -1356,11 +1356,11 @@ ui: comment: Comment no_data: "We couldn't find anything." users: - title: Users - users_with_the_most_reputation: Users with the highest reputation scores - users_with_the_most_vote: Users who voted the most + title: Usuários + users_with_the_most_reputation: Usuários with the highest reputation scores + users_with_the_most_vote: Usuários who voted the most staffs: Our community staff reputation: reputation - votes: votes + Votos: Votos diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 8143518b..604c7e23 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1,4 +1,5 @@ #The following fields are used for back-end + backend: base: success: @@ -80,6 +81,8 @@ backend: new_password_same_as_previous_setting: other: 新密码与之前的设置相同 question: + already_deleted: + other: 该内容已被删除 not_found: other: 问题未找到 cannot_deleted: diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index a33e33cc..17c00070 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -21,6 +21,7 @@ const ( QuestionCannotDeleted = "error.question.cannot_deleted" QuestionCannotClose = "error.question.cannot_close" QuestionCannotUpdate = "error.question.cannot_update" + QuestionAlreadyDeleted = "error.question.already_deleted" AnswerNotFound = "error.answer.not_found" AnswerCannotDeleted = "error.answer.cannot_deleted" AnswerCannotUpdate = "error.answer.cannot_update" @@ -64,4 +65,6 @@ const ( TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself" NotAllowedRegistration = "error.user.not_allowed_registration" SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email" + AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password" + AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status" ) diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index bff57486..e992aec3 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -20,6 +20,8 @@ var GlobalTrans i18n.Translator type LangOption struct { Label string `json:"label"` Value string `json:"value"` + // Translation completion percentage + Progress int `json:"progress"` } // DefaultLangOption default language option. If user config the language is default, the language option is admin choose. @@ -94,6 +96,11 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) { return nil, fmt.Errorf("i18n file parsing failed: %s", err) } LanguageOptions = s.LangOption + for _, option := range LanguageOptions { + if option.Progress != 100 { + option.Label = fmt.Sprintf("%s (%d%%)", option.Label, option.Progress) + } + } return GlobalTrans, err } diff --git a/internal/controller/comment_controller.go b/internal/controller/comment_controller.go index 79ed9681..6b74b754 100644 --- a/internal/controller/comment_controller.go +++ b/internal/controller/comment_controller.go @@ -112,6 +112,18 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) { req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetIsAdminFromContext(ctx) + canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.CommentAdd, + permission.CommentEdit, + permission.CommentDelete, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanAdd = canList[0] + req.CanEdit = canList[1] + req.CanDelete = canList[2] can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentEdit, req.CommentID) if err != nil { handler.HandleResponse(ctx, err, nil) @@ -122,8 +134,8 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) { return } - err = cc.commentService.UpdateComment(ctx, req) - handler.HandleResponse(ctx, err, nil) + resp, err := cc.commentService.UpdateComment(ctx, req) + handler.HandleResponse(ctx, err, resp) } // GetCommentWithPage get comment page diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index b48080ee..0cf2637a 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -41,7 +41,6 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) { // @Tags Lang // @Produce json // @Success 200 {object} handler.RespBody{} -// @Router /answer/api/v1/language/options [get] // @Router /answer/admin/api/language/options [get] func (u *LangController) GetAdminLangOptions(ctx *gin.Context) { handler.HandleResponse(ctx, nil, translator.LanguageOptions) diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index 894b4930..0ccd5ad3 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -404,22 +404,17 @@ func (tc *TemplateController) UserInfo(ctx *gin.Context) { req := &schema.GetOtherUserInfoByUsernameReq{} req.Username = username userinfo, err := tc.templateRenderController.UserInfo(ctx, req) - if err != nil { tc.Page404(ctx) return } - if !userinfo.Has { - tc.Page404(ctx) - return - } siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username) siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name) tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{ "userinfo": userinfo, - "bio": template.HTML(userinfo.Info.BioHTML), + "bio": template.HTML(userinfo.BioHTML), }) } @@ -451,6 +446,7 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI if !ok { data["path"] = "" } + ctx.Header("X-Frame-Options", "DENY") ctx.HTML(code, tpl, data) } diff --git a/internal/controller/template_render/userinfo.go b/internal/controller/template_render/userinfo.go index e10e2dfa..2186fac5 100644 --- a/internal/controller/template_render/userinfo.go +++ b/internal/controller/template_render/userinfo.go @@ -5,6 +5,6 @@ import ( "golang.org/x/net/context" ) -func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoResp, err error) { +func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoByUsernameResp, err error) { return q.userService.GetOtherUserInfoByUsername(ctx, req.Username) } diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index c2cf25bf..a1489e41 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -157,7 +157,7 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) { return } _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP()) - _, err := uc.userService.RetrievePassWord(ctx, req) + err := uc.userService.RetrievePassWord(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -203,6 +203,7 @@ func (uc *UserController) UserLogout(ctx *gin.Context) { return } _ = uc.authService.RemoveUserCacheInfo(ctx, accessToken) + _ = uc.authService.RemoveAdminUserCacheInfo(ctx, accessToken) handler.HandleResponse(ctx, nil, nil) } diff --git a/internal/controller_admin/user_backyard_controller.go b/internal/controller_admin/user_backyard_controller.go index 848319b5..eaa06f73 100644 --- a/internal/controller_admin/user_backyard_controller.go +++ b/internal/controller_admin/user_backyard_controller.go @@ -34,6 +34,8 @@ func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) { return } + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + err := uc.userService.UpdateUserStatus(ctx, req) handler.HandleResponse(ctx, err, nil) } diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go index 8c3bcdb1..43567efd 100644 --- a/internal/entity/answer_entity.go +++ b/internal/entity/answer_entity.go @@ -35,9 +35,11 @@ type Answer struct { type AnswerSearch struct { Answer - Order string `json:"order_by" ` // default or updated - Page int `json:"page" form:"page"` // Query number of pages - PageSize int `json:"page_size" form:"page_size"` // Search page size + IncludeDeleted bool `json:"include_deleted"` + LoginUserID string `json:"login_user_id"` + Order string `json:"order_by"` // default or updated + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size } type AdminAnswerSearch struct { diff --git a/internal/install/install_req.go b/internal/install/install_req.go index a465d6bd..96e16638 100644 --- a/internal/install/install_req.go +++ b/internal/install/install_req.go @@ -78,7 +78,7 @@ type InitEnvironmentResp struct { // InitBaseInfoReq init base info request type InitBaseInfoReq struct { Language string `validate:"required,gt=0,lte=30" json:"lang"` - SiteName string `validate:"required,gt=0,lte=30" json:"site_name"` + SiteName string `validate:"required,sanitizer,gt=0,lte=30" json:"site_name"` SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"` ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"` AdminName string `validate:"required,gt=3,lte=30" json:"name"` diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index 97734343..48a54d6a 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -206,7 +206,9 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc default: session = session.OrderBy("adopted desc,vote_count desc,created_at asc") } - session = session.And("status = ?", entity.AnswerStatusAvailable) + if !search.IncludeDeleted { + session = session.And("status = ? OR user_id = ?", entity.AnswerStatusAvailable, search.LoginUserID) + } session = session.Limit(search.PageSize, offset) count, err = session.FindAndCount(&rows) diff --git a/internal/repo/captcha/captcha.go b/internal/repo/captcha/captcha.go index bda41d1e..382e45e7 100644 --- a/internal/repo/captcha/captcha.go +++ b/internal/repo/captcha/captcha.go @@ -68,3 +68,11 @@ func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha stri } return captcha, nil } + +func (cr *captchaRepo) DelCaptcha(ctx context.Context, key string) (err error) { + err = cr.data.Cache.Del(ctx, key) + if err != nil { + log.Debug(err) + } + return nil +} diff --git a/internal/router/ui.go b/internal/router/ui.go index 2654a37e..86740dbc 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -106,6 +106,7 @@ func (a *UIRouter) Register(r *gin.Engine) { default: filePath = UIIndexFilePath c.Header("content-type", "text/html;charset=utf-8") + c.Header("X-Frame-Options", "DENY") } file, err := ui.Build.ReadFile(filePath) if err != nil { diff --git a/internal/schema/answer_schema.go b/internal/schema/answer_schema.go index 7fc6a12d..3a67f2bf 100644 --- a/internal/schema/answer_schema.go +++ b/internal/schema/answer_schema.go @@ -83,6 +83,7 @@ type AnswerInfo struct { VoteStatus string `json:"vote_status"` VoteCount int `json:"vote_count"` QuestionInfo *QuestionInfo `json:"question_info,omitempty"` + Status int `json:"status"` // MemberActions MemberActions []*PermissionMemberAction `json:"member_actions"` diff --git a/internal/schema/backyard_user_schema.go b/internal/schema/backyard_user_schema.go index 57f3f183..995f0187 100644 --- a/internal/schema/backyard_user_schema.go +++ b/internal/schema/backyard_user_schema.go @@ -2,10 +2,9 @@ package schema // UpdateUserStatusReq update user request type UpdateUserStatusReq struct { - // user id - UserID string `validate:"required" json:"user_id"` - // user status - Status string `validate:"required,oneof=normal suspended deleted inactive" json:"status" enums:"normal,suspended,deleted,inactive"` + UserID string `validate:"required" json:"user_id"` + Status string `validate:"required,oneof=normal suspended deleted inactive" json:"status" enums:"normal,suspended,deleted,inactive"` + LoginUserID string `json:"-"` } const ( diff --git a/internal/schema/comment_schema.go b/internal/schema/comment_schema.go index dcc44bd5..5b045a07 100644 --- a/internal/schema/comment_schema.go +++ b/internal/schema/comment_schema.go @@ -53,6 +53,12 @@ type UpdateCommentReq struct { // user id UserID string `json:"-"` IsAdmin bool `json:"-"` + + CanAdd bool `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` } func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) { diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index b656b834..b0101141 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -176,11 +176,20 @@ type AdminQuestionInfo struct { UserInfo *UserBasicInfo `json:"user_info"` } +type OperationLevel string + +const ( + OperationLevelInfo OperationLevel = "info" + OperationLevelDanger OperationLevel = "danger" + OperationLevelWarning OperationLevel = "warning" +) + type Operation struct { - OperationType string `json:"operation_type"` - OperationDescription string `json:"operation_description"` - OperationMsg string `json:"operation_msg"` - OperationTime int64 `json:"operation_time"` + Type string `json:"type"` + Description string `json:"description"` + Msg string `json:"msg"` + Time int64 `json:"time"` + Level OperationLevel `json:"level"` } type GetCloseTypeResp struct { diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go index cc046b37..3eb8d389 100644 --- a/internal/schema/tag_schema.go +++ b/internal/schema/tag_schema.go @@ -90,7 +90,7 @@ type GetTagResp struct { } func (tr *GetTagResp) GetExcerpt() { - excerpt := strings.TrimSpace(tr.OriginalText) + excerpt := strings.TrimSpace(tr.ParsedText) idx := strings.Index(excerpt, "\n") if idx >= 0 { excerpt = excerpt[0:idx] diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 308568c8..e12a9051 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -309,7 +309,7 @@ func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, er return errFields, errors.BadRequest(reason.UsernameInvalid) } } - req.BioHTML = converter.Markdown2HTML(req.Bio) + req.BioHTML = converter.Markdown2BasicHTML(req.Bio) return nil, nil } @@ -386,7 +386,6 @@ type GetOtherUserInfoByUsernameReq struct { type GetOtherUserInfoResp struct { Info *GetOtherUserInfoByUsernameResp `json:"info"` - Has bool `json:"has"` } type UserChangeEmailSendCodeReq struct { diff --git a/internal/service/action/captcha_service.go b/internal/service/action/captcha_service.go index a6ee8e04..84a189ed 100644 --- a/internal/service/action/captcha_service.go +++ b/internal/service/action/captcha_service.go @@ -16,6 +16,7 @@ import ( type CaptchaRepo interface { SetCaptcha(ctx context.Context, key, captcha string) (err error) GetCaptcha(ctx context.Context, key string) (captcha string, err error) + DelCaptcha(ctx context.Context, key string) (err error) SetActionType(ctx context.Context, ip, actionType string, amount int) (err error) GetActionType(ctx context.Context, ip, actionType string) (amount int, err error) DelActionType(ctx context.Context, ip, actionType string) (err error) @@ -143,6 +144,12 @@ func (cs *CaptchaService) GenerateCaptcha(ctx context.Context) (key, captchaBase func (cs *CaptchaService) VerifyCaptcha(ctx context.Context, key, captcha string) (isCorrect bool, err error) { realCaptcha, err := cs.captchaRepo.GetCaptcha(ctx, key) if err != nil { + log.Error("VerifyCaptcha GetCaptcha Error", err.Error()) + return false, nil + } + err = cs.captchaRepo.DelCaptcha(ctx, key) + if err != nil { + log.Error("VerifyCaptcha DelCaptcha Error", err.Error()) return false, nil } return strings.TrimSpace(captcha) == realCaptcha, nil diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index 758908e1..357484f2 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -73,6 +73,7 @@ func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *sc } info.UserID = data.UserID info.UpdateUserID = data.LastEditUserID + info.Status = data.Status return &info } diff --git a/internal/service/answer_service.go b/internal/service/answer_service.go index 974482c9..f92784e1 100644 --- a/internal/service/answer_service.go +++ b/internal/service/answer_service.go @@ -164,7 +164,7 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( if err != nil { log.Error("UpdateLastAnswer error", err.Error()) } - err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID) + err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID) if err != nil { return insertData.ID, err } @@ -232,6 +232,11 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq return "", nil } + if answerInfo.Status == entity.AnswerStatusDeleted { + err = errors.BadRequest(reason.AnswerCannotUpdate) + return "", err + } + //If the content is the same, ignore it if answerInfo.OriginalText == req.Content { return "", nil @@ -268,7 +273,7 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}); err != nil { return "", err } - err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID) + err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID) if err != nil { return insertData.ID, err } @@ -473,6 +478,8 @@ func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListR dbSearch.Page = req.Page dbSearch.PageSize = req.PageSize dbSearch.Order = req.Order + dbSearch.IncludeDeleted = req.CanDelete + dbSearch.LoginUserID = req.UserID answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch) if err != nil { return list, count, err diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index 18b755ab..6a33ee90 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -209,24 +209,40 @@ func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveC } // UpdateComment update comment -func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (err error) { +func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) ( + resp *schema.GetCommentResp, err error) { + resp = &schema.GetCommentResp{} + old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID) if err != nil { return } if !exist { - return errors.BadRequest(reason.CommentNotFound) + return resp, errors.BadRequest(reason.CommentNotFound) } // user can edit the comment that was posted by himself before deadline. if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) { - return errors.BadRequest(reason.CommentCannotEditAfterDeadline) + return resp, errors.BadRequest(reason.CommentCannotEditAfterDeadline) } comment := &entity.Comment{} _ = copier.Copy(comment, req) comment.ID = req.CommentID - return cs.commentRepo.UpdateComment(ctx, comment) + resp.SetFromComment(comment) + resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, + time.Now(), req.CanEdit, req.CanDelete) + userInfo, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID) + if err != nil { + return nil, err + } + if exist { + resp.Username = userInfo.Username + resp.UserDisplayName = userInfo.DisplayName + resp.UserAvatar = userInfo.Avatar + resp.UserStatus = userInfo.Status + } + return resp, cs.commentRepo.UpdateComment(ctx, comment) } // GetComment get comment one diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index 11868acd..e47645fe 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -89,6 +89,7 @@ func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.Das } startTime := time.Now().Unix() - schema.AppStartTime.Unix() dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) + dashboardInfo.VersionInfo.Version = constant.Version return dashboardInfo, nil } diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index 23424d2c..926ca650 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -86,7 +86,7 @@ func NewQuestionCommon(questionRepo QuestionRepo, } } -func (qs *QuestionCommon) UpdataPv(ctx context.Context, questionID string) error { +func (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error { return qs.questionRepo.UpdatePvCount(ctx, questionID) } @@ -112,14 +112,14 @@ func (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionID, Answ return qs.questionRepo.UpdateLastAnswer(ctx, question) } -func (qs *QuestionCommon) UpdataPostTime(ctx context.Context, questionID string) error { +func (qs *QuestionCommon) UpdatePostTime(ctx context.Context, questionID string) error { questioninfo := &entity.Question{} now := time.Now() questioninfo.ID = questionID questioninfo.PostUpdateTime = now return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"}) } -func (qs *QuestionCommon) UpdataPostSetTime(ctx context.Context, questionID string, setTime time.Time) error { +func (qs *QuestionCommon) UpdatePostSetTime(ctx context.Context, questionID string, setTime time.Time) error { questioninfo := &entity.Question{} questioninfo.ID = questionID questioninfo.PostUpdateTime = setTime @@ -148,7 +148,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser return showinfo, err } if !has { - return showinfo, errors.BadRequest(reason.QuestionNotFound) + return showinfo, errors.NotFound(reason.QuestionNotFound) } showinfo = qs.ShowFormat(ctx, dbinfo) @@ -170,10 +170,11 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser log.Error("json.Unmarshal QuestionCloseJson error", err.Error()) } else { operation := &schema.Operation{} - operation.OperationType = closeinfo.Name - operation.OperationDescription = closeinfo.Description - operation.OperationMsg = closemsg.CloseMsg - operation.OperationTime = metainfo.CreatedAt.Unix() + operation.Type = closeinfo.Name + operation.Description = closeinfo.Description + operation.Msg = closemsg.CloseMsg + operation.Time = metainfo.CreatedAt.Unix() + operation.Level = schema.OperationLevelInfo showinfo.Operation = operation } diff --git a/internal/service/question_service.go b/internal/service/question_service.go index a21523b5..36bab816 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -470,6 +470,10 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest if !has { return } + if dbinfo.Status == entity.QuestionStatusDeleted { + err = errors.BadRequest(reason.QuestionCannotUpdate) + return nil, err + } now := time.Now() question := &entity.Question{} @@ -614,12 +618,23 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s if err != nil { return } + // If the question is deleted, only the administrator and the author can view it + if question.Status == entity.QuestionStatusDeleted && !per.CanReopen && question.UserID != userID { + return nil, errors.NotFound(reason.QuestionNotFound) + } if question.Status != entity.QuestionStatusClosed { per.CanReopen = false } if question.Status == entity.QuestionStatusClosed { per.CanClose = false } + if question.Status == entity.QuestionStatusDeleted { + operation := &schema.Operation{} + operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionAlreadyDeleted) + operation.Level = schema.OperationLevelDanger + question.Operation = operation + } + question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240) question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID, per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen) @@ -630,7 +645,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string, per schema.QuestionPermission) ( resp *schema.QuestionInfo, err error) { - err = qs.questioncommon.UpdataPv(ctx, questionID) + err = qs.questioncommon.UpdatePv(ctx, questionID) if err != nil { log.Error(err) } diff --git a/internal/service/revision_service.go b/internal/service/revision_service.go index bb65db32..3e8c3605 100644 --- a/internal/service/revision_service.go +++ b/internal/service/revision_service.go @@ -191,7 +191,7 @@ func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem if saveerr != nil { return saveerr } - saveerr = rs.questionCommon.UpdataPostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime) + saveerr = rs.questionCommon.UpdatePostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime) if saveerr != nil { return saveerr } diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index 1fb118ec..db28ef09 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -102,7 +102,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq) return nil, err } if !exist { - return nil, errors.BadRequest(reason.TagNotFound) + return nil, errors.NotFound(reason.TagNotFound) } resp = &schema.GetTagResp{} @@ -113,7 +113,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq) return nil, err } if !exist { - return nil, errors.BadRequest(reason.TagNotFound) + return nil, errors.NotFound(reason.TagNotFound) } resp.MainTagSlugName = tagInfo.SlugName } diff --git a/internal/service/user_admin/user_backyard.go b/internal/service/user_admin/user_backyard.go index ae09ce85..76d35859 100644 --- a/internal/service/user_admin/user_backyard.go +++ b/internal/service/user_admin/user_backyard.go @@ -61,6 +61,10 @@ func NewUserAdminService( // UpdateUserStatus update user func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) { + // Admin cannot modify their status + if req.UserID == req.LoginUserID { + return errors.BadRequest(reason.AdminCannotModifySelfStatus) + } userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return @@ -153,6 +157,10 @@ func (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq) // UpdateUserPassword update user password func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) { + // Users cannot modify their password + if req.UserID == req.LoginUserID { + return errors.BadRequest(reason.AdminCannotUpdateTheirPassword) + } userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return err diff --git a/internal/service/user_service.go b/internal/service/user_service.go index e7bd4374..7532c80f 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -86,19 +86,17 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st } func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) ( - resp *schema.GetOtherUserInfoResp, err error, + resp *schema.GetOtherUserInfoByUsernameResp, err error, ) { userInfo, exist, err := us.userRepo.GetByUsername(ctx, username) if err != nil { return nil, err } - resp = &schema.GetOtherUserInfoResp{} if !exist { - return resp, nil + return nil, errors.NotFound(reason.UserNotFound) } - resp.Has = true - resp.Info = &schema.GetOtherUserInfoByUsernameResp{} - resp.Info.GetFromUserEntity(userInfo) + resp = &schema.GetOtherUserInfoByUsernameResp{} + resp.GetFromUserEntity(userInfo) return resp, nil } @@ -149,13 +147,13 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi } // RetrievePassWord . -func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRetrievePassWordRequest) (string, error) { +func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRetrievePassWordRequest) error { userInfo, has, err := us.userRepo.GetByEmail(ctx, req.Email) if err != nil { - return "", err + return err } if !has { - return "", errors.BadRequest(reason.UserNotFound) + return nil } // send email @@ -167,10 +165,10 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL) if err != nil { - return "", err + return err } go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString()) - return code, nil + return nil } // UseRePassword diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go index d61d79f9..b1e2a418 100644 --- a/pkg/converter/markdown.go +++ b/pkg/converter/markdown.go @@ -35,6 +35,17 @@ func Markdown2HTML(source string) string { return buf.String() } +// Markdown2BasicHTML convert markdown to html ,Only basic syntax can be used +func Markdown2BasicHTML(source string) string { + content := Markdown2HTML(source) + filter := bluemonday.NewPolicy() + filter.AllowElements("p", "b", "br") + filter.AllowAttrs("src").OnElements("img") + filter.AddSpaceWhenStrippingTag(true) + content = filter.Sanitize(content) + return content +} + type DangerousHTMLFilterExtension struct { } diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 23844d4f..4b448c7e 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -6,6 +6,21 @@ export const LOGGED_USER_STORAGE_KEY = '_a_lui_'; export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_'; +export const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_'; +export const DRAFT_ANSWER_STORAGE_KEY = '_a_da_'; +export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|'; + +export const IGNORE_PATH_LIST = [ + '/users/login', + '/users/register', + '/users/account-recovery', + '/users/change-email', + '/users/password-reset', + '/users/account-activation', + '/users/account-activation/success', + '/users/account-activation/failed', + '/users/confirm-new-email', +]; export const ADMIN_LIST_STATUS = { // normal; diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index 6f42a559..2101f4da 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -5,7 +5,6 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { unionBy } from 'lodash'; -import { marked } from 'marked'; import * as Types from '@/common/interface'; import { Modal } from '@/components'; @@ -108,15 +107,11 @@ const Comment = ({ objectId, mode, commentId }) => { const users = matchedUsers(item.value); const userNames = unionBy(users.map((user) => user.userName)); const commentMarkDown = parseUserInfo(item.value); - const html = marked.parse(commentMarkDown); - // if (!commentMarkDown || !html) { - // return; - // } + const params = { object_id: objectId, original_text: commentMarkDown, mention_username_list: userNames, - parsed_text: html, ...(item.type === 'reply' ? { reply_comment_id: item.comment_id, @@ -128,13 +123,13 @@ const Comment = ({ objectId, mode, commentId }) => { return updateComment({ ...params, comment_id: item.comment_id, - }).then(() => { + }).then((res) => { setComments( comments.map((comment) => { if (comment.comment_id === item.comment_id) { comment.showEdit = false; - comment.parsed_text = html; - comment.original_text = item.value; + comment.parsed_text = res.parsed_text; + comment.original_text = res.original_text; } return comment; }), diff --git a/ui/src/components/DiffContent/index.tsx b/ui/src/components/DiffContent/index.tsx index 2a3b3bd9..e784c731 100644 --- a/ui/src/components/DiffContent/index.tsx +++ b/ui/src/components/DiffContent/index.tsx @@ -69,7 +69,10 @@ const Index: FC = ({ {objectType !== 'answer' && opts?.showTitle && (
diff --git a/ui/src/components/Editor/utils/index.ts b/ui/src/components/Editor/utils/index.ts index 10ec9664..56fe47a7 100644 --- a/ui/src/components/Editor/utils/index.ts +++ b/ui/src/components/Editor/utils/index.ts @@ -114,19 +114,8 @@ export function htmlRender(el: HTMLElement | null) { }, ); - el.querySelectorAll('table').forEach((table) => { - if ( - (table.parentNode as HTMLDivElement)?.classList.contains( - 'table-responsive', - ) - ) { - return; - } - - table.classList.add('table', 'table-bordered'); - const div = document.createElement('div'); - div.className = 'table-responsive'; - table.parentNode?.replaceChild(div, table); - div.appendChild(table); - }); + // remove change table style to htmlToReact function + /** + * @description: You modify the DOM with other scripts after React has rendered the DOM. This way, on the next render cycle (re-render), React cannot find the DOM node it rendered before, because it has been modified or removed by other scripts. + */ } diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index eda36a03..ced8ffc0 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -71,6 +71,10 @@ const Header: FC = () => { }; const onLoginClick = (evt) => { evt.preventDefault(); + if (location.pathname === '/users/login') { + window.location.reload(); + return; + } floppyNavigation.navigateToLogin((loginPath) => { navigate(loginPath, { replace: true }); }); diff --git a/ui/src/components/SchemaForm/index.tsx b/ui/src/components/SchemaForm/index.tsx index a004deb6..7ea53322 100644 --- a/ui/src/components/SchemaForm/index.tsx +++ b/ui/src/components/SchemaForm/index.tsx @@ -206,7 +206,6 @@ const SchemaForm: ForwardRefRenderFunction = ( const errors = requiredValidator(); if (errors.length > 0) { formData = errors.reduce((acc, cur) => { - console.log('schema.properties[cur]', cur); acc[cur] = { ...formData[cur], isInvalid: true, diff --git a/ui/src/hooks/index.ts b/ui/src/hooks/index.ts index 8144a4f0..ca4fa1ea 100644 --- a/ui/src/hooks/index.ts +++ b/ui/src/hooks/index.ts @@ -9,6 +9,7 @@ import useUserModal from './useUserModal'; import useChangePasswordModal from './useChangePasswordModal'; import usePageTags from './usePageTags'; import usePromptWithUnload from './usePrompt'; +import useImgViewer from './useImgViewer'; export { useTagModal, @@ -22,4 +23,5 @@ export { useChangePasswordModal, usePageTags, usePromptWithUnload, + useImgViewer, }; diff --git a/ui/src/hooks/useImgViewer/index.tsx b/ui/src/hooks/useImgViewer/index.tsx new file mode 100644 index 00000000..6b3a30c2 --- /dev/null +++ b/ui/src/hooks/useImgViewer/index.tsx @@ -0,0 +1,78 @@ +import { useLayoutEffect, useState, MouseEvent, useEffect } from 'react'; +import { Modal } from 'react-bootstrap'; +import { useLocation } from 'react-router-dom'; + +import ReactDOM from 'react-dom/client'; + +const div = document.createElement('div'); +const root = ReactDOM.createRoot(div); + +const useImgViewer = () => { + const location = useLocation(); + const [visible, setVisible] = useState(false); + const [imgSrc, setImgSrc] = useState(''); + const onClose = () => { + setVisible(false); + setImgSrc(''); + }; + + const checkIfInLink = (target) => { + let ret = false; + let el = target.parentElement; + while (el) { + if (el.nodeName.toLowerCase() === 'a') { + ret = true; + break; + } + el = el.parentElement; + } + return ret; + }; + + const checkClickForImgView = (evt: MouseEvent) => { + const { target } = evt; + // @ts-ignore + if (target.nodeName.toLowerCase() !== 'img') { + return; + } + const img = target as HTMLImageElement; + if (!img.naturalWidth || !img.naturalHeight) { + img.classList.add('broken'); + return; + } + const src = img.currentSrc || img.src; + if (src && checkIfInLink(img) === false) { + setImgSrc(src); + setVisible(true); + } + }; + + useLayoutEffect(() => { + root.render( + + + {imgSrc} + + , + ); + }); + useEffect(() => { + onClose(); + }, [location]); + return { + onClose, + checkClickForImgView, + }; +}; + +export default useImgViewer; diff --git a/ui/src/hooks/usePrompt/index.tsx b/ui/src/hooks/usePrompt/index.tsx index 9b34db07..4eddc9c5 100644 --- a/ui/src/hooks/usePrompt/index.tsx +++ b/ui/src/hooks/usePrompt/index.tsx @@ -6,7 +6,7 @@ import { import { useTranslation } from 'react-i18next'; // https://gist.github.com/chaance/2f3c14ec2351a175024f62fd6ba64aa6 -// The link above is an example of implementing usePromt with useBlocer. +// The link above is an example of implementing usePrompt with useBlocker. interface PromptProps { when: boolean; beforeUnload?: boolean; diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx index 28b85eb5..748564d9 100644 --- a/ui/src/hooks/useReportModal/index.tsx +++ b/ui/src/hooks/useReportModal/index.tsx @@ -47,7 +47,11 @@ const useReportModal = (callback?: () => void) => { setShow(true); }); }; - + const asyncCallback = () => { + setTimeout(() => { + callback?.(); + }); + }; const handleRadio = (val) => { setInvalidState(false); setContent({ @@ -93,8 +97,8 @@ const useReportModal = (callback?: () => void) => { close_type: reportType.type, close_msg: content.value, }).then(() => { - callback?.(); onClose(); + asyncCallback(); }); return; } @@ -109,8 +113,8 @@ const useReportModal = (callback?: () => void) => { msg: t('flag_success', { keyPrefix: 'toast' }), variant: 'warning', }); - callback?.(); onClose(); + asyncCallback(); }); } @@ -121,8 +125,8 @@ const useReportModal = (callback?: () => void) => { flagged_type: reportType.type, id: params.id, }).then(() => { - callback?.(); onClose(); + asyncCallback(); }); } }; diff --git a/ui/src/index.scss b/ui/src/index.scss index 1344917e..40d2e5e6 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -120,6 +120,14 @@ a { cursor: pointer; } +.cursor-zoom-out { + cursor: zoom-out !important; +} + +img:not(a img, img.broken) { + cursor: zoom-in; +} + .resize-none { resize: none; } diff --git a/ui/src/pages/404/index.jsx b/ui/src/pages/404/index.jsx index d2aa8a85..90722e77 100644 --- a/ui/src/pages/404/index.jsx +++ b/ui/src/pages/404/index.jsx @@ -1,17 +1,30 @@ +import { useEffect } from 'react'; import { Container, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; const Index = () => { const { t } = useTranslation('translation', { keyPrefix: 'page_404' }); + useEffect(() => { + // auto height of container + const pageWrap = document.querySelector('.page-wrap'); + pageWrap.style.display = 'contents'; + + return () => { + pageWrap.style.display = 'block'; + }; + }, []); return ( - +
(=‘x‘=)
-
{t('desc')}
+

{t('http_error')}

+
{t('desc')}
+ {isEdit && ( + + )} - + {hasDraft && ( + + )}
)} {!isEdit && ( @@ -411,7 +463,6 @@ const Ask = () => { }} />