Merge branch 'dev' into feature-plugin

This commit is contained in:
LinkinStars 2023-03-07 15:39:07 +08:00
commit 4a8ae2b166
89 changed files with 3152 additions and 2556 deletions

View File

@ -1,6 +1,6 @@
.PHONY: build clean ui .PHONY: build clean ui
VERSION=1.0.5 VERSION=1.0.6
BIN=answer BIN=answer
DIR_SRC=./cmd/answer DIR_SRC=./cmd/answer
DOCKER_CMD=docker DOCKER_CMD=docker

View File

@ -6429,9 +6429,6 @@ const docTemplate = `{
"schema.GetOtherUserInfoResp": { "schema.GetOtherUserInfoResp": {
"type": "object", "type": "object",
"properties": { "properties": {
"has": {
"type": "boolean"
},
"info": { "info": {
"$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp"
} }
@ -8222,7 +8219,6 @@ const docTemplate = `{
], ],
"properties": { "properties": {
"status": { "status": {
"description": "user status",
"type": "string", "type": "string",
"enum": [ "enum": [
"normal", "normal",
@ -8232,7 +8228,6 @@ const docTemplate = `{
] ]
}, },
"user_id": { "user_id": {
"description": "user id",
"type": "string" "type": "string"
} }
} }
@ -8528,6 +8523,10 @@ const docTemplate = `{
"label": { "label": {
"type": "string" "type": "string"
}, },
"progress": {
"description": "Translation completion percentage",
"type": "integer"
},
"value": { "value": {
"type": "string" "type": "string"
} }

View File

@ -6417,9 +6417,6 @@
"schema.GetOtherUserInfoResp": { "schema.GetOtherUserInfoResp": {
"type": "object", "type": "object",
"properties": { "properties": {
"has": {
"type": "boolean"
},
"info": { "info": {
"$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp"
} }
@ -8210,7 +8207,6 @@
], ],
"properties": { "properties": {
"status": { "status": {
"description": "user status",
"type": "string", "type": "string",
"enum": [ "enum": [
"normal", "normal",
@ -8220,7 +8216,6 @@
] ]
}, },
"user_id": { "user_id": {
"description": "user id",
"type": "string" "type": "string"
} }
} }
@ -8516,6 +8511,10 @@
"label": { "label": {
"type": "string" "type": "string"
}, },
"progress": {
"description": "Translation completion percentage",
"type": "integer"
},
"value": { "value": {
"type": "string" "type": "string"
} }

View File

@ -557,8 +557,6 @@ definitions:
type: object type: object
schema.GetOtherUserInfoResp: schema.GetOtherUserInfoResp:
properties: properties:
has:
type: boolean
info: info:
$ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp' $ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp'
type: object type: object
@ -1819,7 +1817,6 @@ definitions:
schema.UpdateUserStatusReq: schema.UpdateUserStatusReq:
properties: properties:
status: status:
description: user status
enum: enum:
- normal - normal
- suspended - suspended
@ -1827,7 +1824,6 @@ definitions:
- inactive - inactive
type: string type: string
user_id: user_id:
description: user id
type: string type: string
required: required:
- status - status
@ -2039,6 +2035,9 @@ definitions:
properties: properties:
label: label:
type: string type: string
progress:
description: Translation completion percentage
type: integer
value: value:
type: string type: string
type: object type: object

8
go.mod
View File

@ -36,13 +36,13 @@ require (
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.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/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/tidwall/gjson v1.14.4
github.com/yuin/goldmark v1.4.13 github.com/yuin/goldmark v1.4.13
golang.org/x/crypto v0.1.0 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/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.14.2 modernc.org/sqlite v1.14.2
@ -123,7 +123,7 @@ require (
go.uber.org/zap v1.23.0 // indirect go.uber.org/zap v1.23.0 // indirect
golang.org/x/image v0.1.0 // indirect golang.org/x/image v0.1.0 // indirect
golang.org/x/mod v0.6.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/text v0.5.0 // indirect
golang.org/x/tools v0.2.0 // indirect golang.org/x/tools v0.2.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect

16
go.sum
View File

@ -670,13 +670,14 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 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 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 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q=
github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= 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.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU= github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= 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/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 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
@ -852,8 +853,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-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-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.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.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -946,11 +947,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-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-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.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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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-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.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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -107,6 +107,8 @@ backend:
other: Should not contain synonym tags. other: Should not contain synonym tags.
cannot_update: cannot_update:
other: No permission to update. other: No permission to update.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: You cannot set the synonym of the current tag as itself. other: You cannot set the synonym of the current tag as itself.
smtp: smtp:
@ -309,7 +311,7 @@ ui:
msg: msg:
empty: Code cannot be empty. empty: Code cannot be empty.
language: language:
label: Language (optional) label: Language
placeholder: Automatic detection placeholder: Automatic detection
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
@ -345,7 +347,7 @@ ui:
only_image: Only image files are allowed. only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB. max_size: File size cannot exceed 4MB.
desc: desc:
label: Description (optional) label: Description
tab_url: Image URL tab_url: Image URL
form_url: form_url:
fields: fields:
@ -354,7 +356,7 @@ ui:
msg: msg:
empty: Image URL cannot be empty. empty: Image URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
uploading: Uploading uploading: Uploading
@ -374,7 +376,7 @@ ui:
msg: msg:
empty: URL cannot be empty. empty: URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
ordered_list: ordered_list:
@ -422,7 +424,7 @@ ui:
range: URL slug up to 35 characters. range: URL slug up to 35 characters.
character: URL slug contains unallowed character set. character: URL slug contains unallowed character set.
desc: desc:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_submit: Submit btn_submit: Submit
tag_info: tag_info:
@ -439,9 +441,11 @@ ui:
synonyms_text: The following tags will be remapped to synonyms_text: The following tags will be remapped to
delete: delete:
title: Delete this tag title: Delete this tag
content: >- tip_with_posts: >-
<p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: Are you sure you wish to delete? tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Close close: Close
edit_tag: edit_tag:
title: Edit Tag title: Edit Tag
@ -677,13 +681,13 @@ ui:
default: System default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me
website: website:
label: Website (optional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Website incorrect format msg: Website incorrect format
location: location:
label: Location (optional) label: Location
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1313,6 +1309,7 @@ ui:
label: Login required label: Login required
text: Only logged in users can access this community. text: Only logged in users can access this community.
form: form:
optional: (optional)
empty: cannot be empty empty: cannot be empty
invalid: is invalid invalid: is invalid
btn_submit: Save btn_submit: Save
@ -1362,5 +1359,7 @@ ui:
staffs: Our community staff staffs: Our community staff
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

View File

@ -35,6 +35,10 @@ backend:
other: Email and password do not match. other: Email and password do not match.
error: error:
admin: 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: email_or_password_wrong:
other: Email and password do not match. other: Email and password do not match.
answer: answer:
@ -81,6 +85,8 @@ backend:
new_password_same_as_previous_setting: new_password_same_as_previous_setting:
other: The new password is the same as the previous one. other: The new password is the same as the previous one.
question: question:
already_deleted:
other: This post has been deleted.
not_found: not_found:
other: Question not found. other: Question not found.
cannot_deleted: cannot_deleted:
@ -109,7 +115,7 @@ backend:
cannot_update: cannot_update:
other: No permission to update. other: No permission to update.
is_used_cannot_delete: is_used_cannot_delete:
other: "You cannot delete a tag that is in use" other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: You cannot set the synonym of the current tag as itself. other: You cannot set the synonym of the current tag as itself.
smtp: smtp:
@ -837,6 +843,7 @@ ui:
approve: Approve approve: Approve
reject: Reject reject: Reject
skip: Skip skip: Skip
discard_draft: Discard draft
search: search:
title: Search Results title: Search Results
keywords: Keywords keywords: Keywords
@ -1033,9 +1040,11 @@ ui:
answers: answers answers: answers
accepted: Accepted accepted: Accepted
page_404: page_404:
http_error: HTTP Error 404
desc: "Unfortunately, this page doesn't exist." desc: "Unfortunately, this page doesn't exist."
back_home: Back to homepage back_home: Back to homepage
page_50X: page_50X:
http_error: HTTP Error 500
desc: The server encountered an error and could not complete your request. desc: The server encountered an error and could not complete your request.
back_home: Back to homepage back_home: Back to homepage
page_maintenance: page_maintenance:
@ -1453,6 +1462,10 @@ ui:
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt: prompt:
leave_page: "Are you sure you want to leave the page?" leave_page: Are you sure you want to leave the page?
changes_not_save: "Your changes may not be saved." 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.

View File

@ -107,6 +107,8 @@ backend:
other: No debe contener etiquetas sinónimas. other: No debe contener etiquetas sinónimas.
cannot_update: cannot_update:
other: Sin permiso para actualizar. other: Sin permiso para actualizar.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: No se puede establecer como sinónimo de una etiqueta la propia etiqueta. other: No se puede establecer como sinónimo de una etiqueta la propia etiqueta.
smtp: smtp:
@ -330,7 +332,7 @@ ui:
msg: msg:
empty: Código no puede estar vacío. empty: Código no puede estar vacío.
language: language:
label: Lenguage (opcional) label: Language
placeholder: Detección automática placeholder: Detección automática
btn_cancel: Cancelar btn_cancel: Cancelar
btn_confirm: Añadir btn_confirm: Añadir
@ -366,7 +368,7 @@ ui:
only_image: Solo se permiten archivos de imagen. only_image: Solo se permiten archivos de imagen.
max_size: El tamaño del archivo no puede superar 4MB. max_size: El tamaño del archivo no puede superar 4MB.
desc: desc:
label: Descripción (opcional) label: Description
tab_url: URL de la imagen tab_url: URL de la imagen
form_url: form_url:
fields: fields:
@ -375,7 +377,7 @@ ui:
msg: msg:
empty: La URL de la imagen no puede estar vacía. empty: La URL de la imagen no puede estar vacía.
name: name:
label: Descripción (opcional) label: Description
btn_cancel: Cancelar btn_cancel: Cancelar
btn_confirm: Añadir btn_confirm: Añadir
uploading: Subiendo uploading: Subiendo
@ -395,7 +397,7 @@ ui:
msg: msg:
empty: La dirección no puede estar vacía. empty: La dirección no puede estar vacía.
name: name:
label: Descripción (opcional) label: Description
btn_cancel: Cancelar btn_cancel: Cancelar
btn_confirm: Añadir btn_confirm: Añadir
ordered_list: ordered_list:
@ -443,7 +445,7 @@ ui:
range: URL slug hasta 35 caracteres. range: URL slug hasta 35 caracteres.
character: La URL amigable contiene caracteres no permitidos. character: La URL amigable contiene caracteres no permitidos.
desc: desc:
label: Descripción (opcional) label: Description
btn_cancel: Cancelar btn_cancel: Cancelar
btn_submit: Enviar btn_submit: Enviar
tag_info: tag_info:
@ -460,9 +462,11 @@ ui:
synonyms_text: Las siguientes etiquetas serán reasignadas a synonyms_text: Las siguientes etiquetas serán reasignadas a
delete: delete:
title: Eliminar esta etiqueta title: Eliminar esta etiqueta
content: >- tip_with_posts: >-
<p>No se permite la eliminación de etiquetas con posts.</p><p>Por favor antes elimina esta etiqueta del post.</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: '¿Estás seguro de que deseas borrarlo?' tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Cerrar close: Cerrar
edit_tag: edit_tag:
title: Editar etiqueta title: Editar etiqueta
@ -698,13 +702,13 @@ ui:
default: Sistema default: Sistema
msg: Por favor, sube una imagen msg: Por favor, sube una imagen
bio: bio:
label: Sobre mí (opcional) label: About Me
website: website:
label: Sitio web (opcional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Formato del sitio web incorrecto msg: Formato del sitio web incorrecto
location: location:
label: Ubicación (opcional) label: Location
placeholder: "Ciudad, País" placeholder: "Ciudad, País"
notification: notification:
heading: Notificaciones heading: Notificaciones
@ -1188,11 +1192,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1202,14 +1206,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1261,18 +1257,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1338,6 +1334,7 @@ ui:
label: Inicio de sesión requerido label: Inicio de sesión requerido
text: Sólo usuarios con sesión iniciada pueden acceder a esta comunidad. text: Sólo usuarios con sesión iniciada pueden acceder a esta comunidad.
form: form:
optional: (optional)
empty: no puede estar en blanco empty: no puede estar en blanco
invalid: no es válido invalid: no es válido
btn_submit: Guardar btn_submit: Guardar
@ -1387,5 +1384,7 @@ ui:
staffs: Nuestor equipo de la comunidad staffs: Nuestor equipo de la comunidad
reputation: reputación reputation: reputación
votes: votos votes: votos
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

View File

@ -107,6 +107,8 @@ backend:
other: Ne dois pas contenir de tags synonymes. other: Ne dois pas contenir de tags synonymes.
cannot_update: cannot_update:
other: Pas de permission pour mettre à jour. other: Pas de permission pour mettre à jour.
is_used_cannot_delete:
other: Vous ne pouvez pas supprimer un tag utilisé
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: Vous ne pouvez pas définir le synonyme de la balise actuelle comme elle-même. other: Vous ne pouvez pas définir le synonyme de la balise actuelle comme elle-même.
smtp: smtp:
@ -309,7 +311,7 @@ ui:
msg: msg:
empty: Le code ne peut pas être vide. empty: Le code ne peut pas être vide.
language: language:
label: Langue (facultatif) label: Langage
placeholder: Détection automatique placeholder: Détection automatique
btn_cancel: Annuler btn_cancel: Annuler
btn_confirm: Ajouter btn_confirm: Ajouter
@ -345,7 +347,7 @@ ui:
only_image: Seules les images sont autorisées. only_image: Seules les images sont autorisées.
max_size: La taille du fichier ne doit pas dépasser 4 Mo. max_size: La taille du fichier ne doit pas dépasser 4 Mo.
desc: desc:
label: Description (optionnel) label: Description
tab_url: URL de l'image tab_url: URL de l'image
form_url: form_url:
fields: fields:
@ -354,7 +356,7 @@ ui:
msg: msg:
empty: L'URL de l'image ne peut pas être vide. empty: L'URL de l'image ne peut pas être vide.
name: name:
label: Description (optionnel) label: Description
btn_cancel: Annuler btn_cancel: Annuler
btn_confirm: Ajouter btn_confirm: Ajouter
uploading: Téléversement en cours uploading: Téléversement en cours
@ -374,7 +376,7 @@ ui:
msg: msg:
empty: L'URL ne peut pas être vide. empty: L'URL ne peut pas être vide.
name: name:
label: Description (facultatif) label: Description
btn_cancel: Annuler btn_cancel: Annuler
btn_confirm: Ajouter btn_confirm: Ajouter
ordered_list: ordered_list:
@ -422,7 +424,7 @@ ui:
range: Titre de 35 caractères maximum. range: Titre de 35 caractères maximum.
character: Le slug d'URL contient un jeu de caractères non autorisé. character: Le slug d'URL contient un jeu de caractères non autorisé.
desc: desc:
label: Description (facultatif) label: Description
btn_cancel: Annuler btn_cancel: Annuler
btn_submit: Valider btn_submit: Valider
tag_info: tag_info:
@ -439,9 +441,11 @@ ui:
synonyms_text: Les balises suivantes seront remappées en synonyms_text: Les balises suivantes seront remappées en
delete: delete:
title: Supprimer cette étiquette title: Supprimer cette étiquette
content: >- tip_with_posts: >-
<p>Nous ne permettons pas de supprimer le tag avec les posts.</p><p>Veuillez d'abord supprimer ce tag des posts.</p> <p>Nous ne permettons pas de <strong>supprimer un tag avec des posts.</strong></p><p>Veuillez d'abord supprimer ce tag des posts.</p>
content2: Etes-vous sûr de vouloir supprimer ? tip_with_synonyms: >-
<p>Nous ne permettons pas de <strong>supprimer un tag avec des synonymes</strong>.</p> <p>Veuillez d'abord supprimer les synonymes de ce tag.</p>
tip: Êtes-vous sûr de vouloir supprimer ?
close: Fermer close: Fermer
edit_tag: edit_tag:
title: Editer le tag title: Editer le tag
@ -677,13 +681,13 @@ ui:
default: Système default: Système
msg: Veuillez charger un avatar msg: Veuillez charger un avatar
bio: bio:
label: À propos de moi (optionnel) label: À propos de moi
website: website:
label: Site web (facultatif) label: Site Web
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Format du site web incorrect msg: Format du site web incorrect
location: location:
label: Emplacement (facultatif) label: Position
placeholder: "Ville, Pays" placeholder: "Ville, Pays"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Indiquez une URL valide. validate: Indiquez une URL valide.
text: L'adresse de ce site. text: L'adresse de ce site.
short_desc: short_desc:
label: Description courte (optionnel) label: Description Courte
msg: La description courte ne peut pas être vide. msg: La description courte ne peut pas être vide.
text: "La description courte, telle qu'elle est utilisée dans le tag titre de la page d'accueil." text: "La description courte, telle qu'elle est utilisée dans le tag titre de la page d'accueil."
desc: desc:
label: Description du site (optionnel) label: Description du Site
msg: La description du site ne peut pas être vide. msg: La description du site ne peut pas être vide.
text: "Décrivez ce site en une phrase, telle qu'elle est utilisée dans la balise meta description." text: "Décrivez ce site en une phrase, telle qu'elle est utilisée dans la balise meta description."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: L'adresse email du responsable du site. text: L'adresse email du responsable du site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optionnel)
msg: Le logo ne peut pas être vide.
text: Vous pouvez télécharger votre image ou la <1>réinitialiser</1> sur le texte du titre du site.
theme:
label: Thème
msg: Le thème ne peut pas être vide.
text: Sélectionne un thème existant.
language: language:
label: Langue de l'interface label: Langue de l'interface
msg: La langue de l'interface ne peut pas être vide. msg: La langue de l'interface ne peut pas être vide.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Marque page_title: Marque
logo: logo:
label: Logo (optionnel) label: Logo
msg: Le logo ne peut pas être vide. msg: Le logo ne peut pas être vide.
text: L'image du logo en haut à gauche de votre site. Utilisez une grande image rectangulaire avec une hauteur de 56 et un ratio d'aspect supérieur à 3:1. Si laissé vide, le titre du site sera affiché. text: L'image du logo en haut à gauche de votre site. Utilisez une grande image rectangulaire avec une hauteur de 56 et un ratio d'aspect supérieur à 3:1. Si laissé vide, le titre du site sera affiché.
mobile_logo: mobile_logo:
label: Logo mobile (optionnel) label: Logo Mobile
text: Le logo utilisé sur la version mobile de votre site. Utilisez une image rectangulaire large avec une hauteur de 56. Si laissé vide, l'image du paramètre « logo » sera utilisée. text: Le logo utilisé sur la version mobile de votre site. Utilisez une image rectangulaire large avec une hauteur de 56. Si laissé vide, l'image du paramètre « logo » sera utilisée.
square_icon: square_icon:
label: Icône carrée (optionnel) label: Icône carrée
msg: L'icône carrée ne peut pas être vide. msg: L'icône carrée ne peut pas être vide.
text: Image utilisée comme base pour les icônes de métadonnées. Idéalement supérieure à 512x512. text: Image utilisée comme base pour les icônes de métadonnées. Idéalement supérieure à 512x512.
favicon: favicon:
label: Favicon (optionnel) label: Favicon
text: Une favicon pour votre site. Pour fonctionner correctement sur un CDN, il doit s'agir d'un png. Sera redimensionné en 32x32. Si laissé vide, « icône carrée » sera utilisé. text: Une favicon pour votre site. Pour fonctionner correctement sur un CDN, il doit s'agir d'un png. Sera redimensionné en 32x32. Si laissé vide, « icône carrée » sera utilisé.
legal: legal:
page_title: Légal page_title: Légal
@ -1313,6 +1309,7 @@ ui:
label: Connexion requise label: Connexion requise
text: Seuls les utilisateurs connectés peuvent accéder à cette communauté. text: Seuls les utilisateurs connectés peuvent accéder à cette communauté.
form: form:
optional: (optionnel)
empty: ne peut pas être vide empty: ne peut pas être vide
invalid: est invalide invalid: est invalide
btn_submit: Sauvegarder btn_submit: Sauvegarder
@ -1362,5 +1359,7 @@ ui:
staffs: Staff de la communauté staffs: Staff de la communauté
reputation: réputation reputation: réputation
votes: votes votes: votes
prompt:
leave_page: "Voulez-vous vraiment quitter la page ?"
changes_not_save: "Impossible d'enregistrer vos modifications."

View File

@ -1,26 +1,41 @@
# all support language # all support language
language_options: language_options:
- label: "English(US)" - label: "English"
value: "en_US" value: "en_US"
- label: "Español(ES)" progress: 100
- label: "Español"
value: "es_ES" value: "es_ES"
- label: "Português(PT)" progress: 0
- label: "Português(BR)"
value: "pt_BR"
progress: 0
- label: "Português"
value: "pt_PT" value: "pt_PT"
- label: "Deutsch(DE)" progress: 0
- label: "Deutsch"
value: "de_DE" value: "de_DE"
- label: "Français(FR)" progress: 4
- label: "Français"
value: "fr_FR" value: "fr_FR"
- label: "日本語(JA)" progress: 100
- label: "日本語"
value: "ja_JP" value: "ja_JP"
- label: "Italiano(IT)" progress: 0
- label: "Italiano"
value: "it_IT" value: "it_IT"
- label: "Русский(RU)" progress: 16
- label: "Русский"
value: "ru_RU" value: "ru_RU"
- label: "简体中文(CN)" progress: 13
- label: "简体中文"
value: "zh_CN" value: "zh_CN"
- label: "繁體中文(CN)" progress: 100
- label: "繁體中文"
value: "zh_TW" value: "zh_TW"
- label: "한국어(KO)" progress: 100
- label: "한국어"
value: "ko_KR" value: "ko_KR"
- label: "Tiếng Việt(VI)" progress: 0
- label: "Tiếng Việt"
value: "vi_VN" value: "vi_VN"
progress: 0

View File

@ -107,6 +107,8 @@ backend:
other: Tidak boleh mengandung Tag sinonim. other: Tidak boleh mengandung Tag sinonim.
cannot_update: cannot_update:
other: Tidak memiliki izin untuk memperbaharui. other: Tidak memiliki izin untuk memperbaharui.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: Anda tidak bisa menetapkan sinonim dari tag saat ini dengan tag yang sama. other: Anda tidak bisa menetapkan sinonim dari tag saat ini dengan tag yang sama.
smtp: smtp:
@ -309,7 +311,7 @@ ui:
msg: msg:
empty: Code tidak boleh kosong. empty: Code tidak boleh kosong.
language: language:
label: Bahasa (opsional) label: Language
placeholder: Deteksi otomatis placeholder: Deteksi otomatis
btn_cancel: Batal btn_cancel: Batal
btn_confirm: Tambah btn_confirm: Tambah
@ -345,7 +347,7 @@ ui:
only_image: Hanya file Gambar yang diperbolehkan. only_image: Hanya file Gambar yang diperbolehkan.
max_size: Ukuran file tidak boleh melebihi 4MB. max_size: Ukuran file tidak boleh melebihi 4MB.
desc: desc:
label: Deskripsi (opsional) label: Description
tab_url: URL gambar tab_url: URL gambar
form_url: form_url:
fields: fields:
@ -354,7 +356,7 @@ ui:
msg: msg:
empty: URL gambar tidak boleh kosong. empty: URL gambar tidak boleh kosong.
name: name:
label: Deskripsi (opsional) label: Description
btn_cancel: Batal btn_cancel: Batal
btn_confirm: Tambah btn_confirm: Tambah
uploading: Sedang mengunggah uploading: Sedang mengunggah
@ -374,7 +376,7 @@ ui:
msg: msg:
empty: URL cannot be empty. empty: URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Batal btn_cancel: Batal
btn_confirm: Tambah btn_confirm: Tambah
ordered_list: ordered_list:
@ -422,7 +424,7 @@ ui:
range: URL slug up to 35 characters. range: URL slug up to 35 characters.
character: URL slug contains unallowed character set. character: URL slug contains unallowed character set.
desc: desc:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_submit: Submit btn_submit: Submit
tag_info: tag_info:
@ -439,9 +441,11 @@ ui:
synonyms_text: Tag berikut akan dipetakan ulang ke synonyms_text: Tag berikut akan dipetakan ulang ke
delete: delete:
title: Hapus tagar ini title: Hapus tagar ini
content: >- tip_with_posts: >-
<p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: Anda yakin ingin menghapusnya? tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Tutup close: Tutup
edit_tag: edit_tag:
title: Ubah Tag title: Ubah Tag
@ -677,13 +681,13 @@ ui:
default: System default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me
website: website:
label: Website (optional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Website incorrect format msg: Website incorrect format
location: location:
label: Location (optional) label: Location
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1313,6 +1309,7 @@ ui:
label: Login required label: Login required
text: Only logged in users can access this community. text: Only logged in users can access this community.
form: form:
optional: (optional)
empty: cannot be empty empty: cannot be empty
invalid: is invalid invalid: is invalid
btn_submit: Save btn_submit: Save
@ -1362,5 +1359,7 @@ ui:
staffs: Our community staff staffs: Our community staff
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

View File

@ -107,6 +107,8 @@ backend:
other: Non deve contenere tag sinonimi. other: Non deve contenere tag sinonimi.
cannot_update: cannot_update:
other: Nessun permesso per l'aggiornamento. other: Nessun permesso per l'aggiornamento.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: Non puoi impostare il sinonimo del tag corrente come se stesso. other: Non puoi impostare il sinonimo del tag corrente come se stesso.
smtp: smtp:
@ -309,7 +311,7 @@ ui:
msg: msg:
empty: Code cannot be empty. empty: Code cannot be empty.
language: language:
label: Language (optional) label: Language
placeholder: Automatic detection placeholder: Automatic detection
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
@ -345,7 +347,7 @@ ui:
only_image: Only image files are allowed. only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB. max_size: File size cannot exceed 4MB.
desc: desc:
label: Description (optional) label: Description
tab_url: Image URL tab_url: Image URL
form_url: form_url:
fields: fields:
@ -354,7 +356,7 @@ ui:
msg: msg:
empty: Image URL cannot be empty. empty: Image URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
uploading: Uploading uploading: Uploading
@ -374,7 +376,7 @@ ui:
msg: msg:
empty: URL cannot be empty. empty: URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
ordered_list: ordered_list:
@ -422,7 +424,7 @@ ui:
range: URL slug up to 35 characters. range: URL slug up to 35 characters.
character: URL slug contains unallowed character set. character: URL slug contains unallowed character set.
desc: desc:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_submit: Submit btn_submit: Submit
tag_info: tag_info:
@ -439,9 +441,11 @@ ui:
synonyms_text: The following tags will be remapped to synonyms_text: The following tags will be remapped to
delete: delete:
title: Delete this tag title: Delete this tag
content: >- tip_with_posts: >-
<p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: Are you sure you wish to delete? tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Close close: Close
edit_tag: edit_tag:
title: Edit Tag title: Edit Tag
@ -677,13 +681,13 @@ ui:
default: System default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me
website: website:
label: Website (optional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Website incorrect format msg: Website incorrect format
location: location:
label: Location (optional) label: Location
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1313,6 +1309,7 @@ ui:
label: Login required label: Login required
text: Only logged in users can access this community. text: Only logged in users can access this community.
form: form:
optional: (optional)
empty: cannot be empty empty: cannot be empty
invalid: is invalid invalid: is invalid
btn_submit: Save btn_submit: Save
@ -1362,5 +1359,7 @@ ui:
staffs: Our community staff staffs: Our community staff
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

View File

@ -107,6 +107,8 @@ backend:
other: Should not contain synonym tags. other: Should not contain synonym tags.
cannot_update: cannot_update:
other: No permission to update. other: No permission to update.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: You cannot set the synonym of the current tag as itself. other: You cannot set the synonym of the current tag as itself.
smtp: smtp:
@ -309,7 +311,7 @@ ui:
msg: msg:
empty: Code cannot be empty. empty: Code cannot be empty.
language: language:
label: Language (optional) label: Language
placeholder: Automatic detection placeholder: Automatic detection
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
@ -345,7 +347,7 @@ ui:
only_image: Only image files are allowed. only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB. max_size: File size cannot exceed 4MB.
desc: desc:
label: Description (optional) label: Description
tab_url: Image URL tab_url: Image URL
form_url: form_url:
fields: fields:
@ -354,7 +356,7 @@ ui:
msg: msg:
empty: Image URL cannot be empty. empty: Image URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
uploading: Uploading uploading: Uploading
@ -374,7 +376,7 @@ ui:
msg: msg:
empty: URL cannot be empty. empty: URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
ordered_list: ordered_list:
@ -422,7 +424,7 @@ ui:
range: URL slug up to 35 characters. range: URL slug up to 35 characters.
character: URL slug contains unallowed character set. character: URL slug contains unallowed character set.
desc: desc:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_submit: Submit btn_submit: Submit
tag_info: tag_info:
@ -439,9 +441,11 @@ ui:
synonyms_text: The following tags will be remapped to synonyms_text: The following tags will be remapped to
delete: delete:
title: Delete this tag title: Delete this tag
content: >- tip_with_posts: >-
<p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: Are you sure you wish to delete? tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Close close: Close
edit_tag: edit_tag:
title: Edit Tag title: Edit Tag
@ -677,13 +681,13 @@ ui:
default: System default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me
website: website:
label: Website (optional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Website incorrect format msg: Website incorrect format
location: location:
label: Location (optional) label: Location
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1313,6 +1309,7 @@ ui:
label: Login required label: Login required
text: Only logged in users can access this community. text: Only logged in users can access this community.
form: form:
optional: (optional)
empty: cannot be empty empty: cannot be empty
invalid: is invalid invalid: is invalid
btn_submit: Save btn_submit: Save
@ -1362,5 +1359,7 @@ ui:
staffs: Our community staff staffs: Our community staff
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

View File

@ -107,6 +107,8 @@ backend:
other: Should not contain synonym tags. other: Should not contain synonym tags.
cannot_update: cannot_update:
other: No permission to update. other: No permission to update.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: You cannot set the synonym of the current tag as itself. other: You cannot set the synonym of the current tag as itself.
smtp: smtp:
@ -309,7 +311,7 @@ ui:
msg: msg:
empty: Code cannot be empty. empty: Code cannot be empty.
language: language:
label: Language (optional) label: Language
placeholder: Automatic detection placeholder: Automatic detection
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
@ -345,7 +347,7 @@ ui:
only_image: Only image files are allowed. only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB. max_size: File size cannot exceed 4MB.
desc: desc:
label: Description (optional) label: Description
tab_url: Image URL tab_url: Image URL
form_url: form_url:
fields: fields:
@ -354,7 +356,7 @@ ui:
msg: msg:
empty: Image URL cannot be empty. empty: Image URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
uploading: Uploading uploading: Uploading
@ -374,7 +376,7 @@ ui:
msg: msg:
empty: URL cannot be empty. empty: URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
ordered_list: ordered_list:
@ -422,7 +424,7 @@ ui:
range: URL slug up to 35 characters. range: URL slug up to 35 characters.
character: URL slug contains unallowed character set. character: URL slug contains unallowed character set.
desc: desc:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_submit: Submit btn_submit: Submit
tag_info: tag_info:
@ -439,9 +441,11 @@ ui:
synonyms_text: The following tags will be remapped to synonyms_text: The following tags will be remapped to
delete: delete:
title: Delete this tag title: Delete this tag
content: >- tip_with_posts: >-
<p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: Are you sure you wish to delete? tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Close close: Close
edit_tag: edit_tag:
title: Edit Tag title: Edit Tag
@ -677,13 +681,13 @@ ui:
default: System default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me
website: website:
label: Website (optional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Website incorrect format msg: Website incorrect format
location: location:
label: Location (optional) label: Location
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1313,6 +1309,7 @@ ui:
label: Login required label: Login required
text: Only logged in users can access this community. text: Only logged in users can access this community.
form: form:
optional: (optional)
empty: cannot be empty empty: cannot be empty
invalid: is invalid invalid: is invalid
btn_submit: Save btn_submit: Save
@ -1362,5 +1359,7 @@ ui:
staffs: Our community staff staffs: Our community staff
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

File diff suppressed because it is too large Load Diff

View File

@ -14,153 +14,155 @@ backend:
role: role:
name: name:
user: user:
other: User other: Usuário
admin: admin:
other: Admin other: Administrador
moderator: moderator:
other: Moderator other: Moderador
description: description:
user: user:
other: Default with no special access. other: Padrão sem acesso especial.
admin: admin:
other: Have the full power to access the site. other: Possui acesso total ao site.
moderator: moderator:
other: Has access to all posts except admin settings. other: Possui acesso a todas as postagens exceto às configurações de usuários.
email: email:
other: Email other: E-mail
password: password:
other: Password other: Senha
email_or_password_wrong_error: email_or_password_wrong_error:
other: Email and password do not match. other: O e-mail e a palavra-passe não coincidem.
error: error:
admin: admin:
email_or_password_wrong: email_or_password_wrong:
other: Email and password do not match. other: O e-mail e a palavra-passe não coincidem.
answer: answer:
not_found: not_found:
other: Answer do not found. other: Resposta não encontrada.
cannot_deleted: cannot_deleted:
other: No permission to delete. other: Sem permissão para remover.
cannot_update: cannot_update:
other: No permission to update. other: Sem permissão para atualizar.
comment: comment:
edit_without_permission: edit_without_permission:
other: Comment are not allowed to edit. other: Não é possível alterar comentários.
not_found: not_found:
other: Comment not found. other: Comentário não encontrado.
cannot_edit_after_deadline: cannot_edit_after_deadline:
other: The comment time has been too long to modify. other: O tempo do comentário foi muito longo para ser modificado.
email: email:
duplicate: duplicate:
other: Email already exists. other: O e-mail já existe.
need_to_be_verified: need_to_be_verified:
other: Email should be verified. other: O e-mail deve ser verificado.
verify_url_expired: verify_url_expired:
other: Email verified URL has expired, please resend the email. other: O e-mail verificado URL expirou, por favor, reenvie o e-mail.
lang: lang:
not_found: not_found:
other: Language file not found. other: Arquivo de idioma não encontrado.
object: object:
captcha_verification_failed: captcha_verification_failed:
other: Captcha wrong. other: O Captcha está incorreto.
disallow_follow: disallow_follow:
other: You are not allowed to follow. other: Você não possui autorização suficiente para seguir.
disallow_vote: disallow_vote:
other: You are not allowed to vote. other: Você não possui permissão para votar.
disallow_vote_your_self: 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: not_found:
other: Object not found. other: Objeto não encontrado.
verification_failed: verification_failed:
other: Verification failed. other: A verificação falhou.
email_or_password_incorrect: email_or_password_incorrect:
other: Email and password do not match. other: O e-mail e a senha não correspondem.
old_password_verification_failed: old_password_verification_failed:
other: The old password verification failed other: Falha na verificação de senha antiga
new_password_same_as_previous_setting: new_password_same_as_previous_setting:
other: The new password is the same as the previous one. other: A nova senha é a mesma que a anterior.
question: question:
not_found: not_found:
other: Question not found. other: Pergunta não encontrada.
cannot_deleted: cannot_deleted:
other: No permission to delete. other: Sem permissão para remover.
cannot_close: cannot_close:
other: No permission to close. other: Sem permissão para fechar.
cannot_update: cannot_update:
other: No permission to update. other: Sem permissão para atualizar.
rank: rank:
fail_to_meet_the_condition: fail_to_meet_the_condition:
other: Rank fail to meet the condition. other: O nível não consegue satisfazer a condição.
report: report:
handle_failed: handle_failed:
other: Report handle failed. other: Falha ao manusear relatório.
not_found: not_found:
other: Report not found. other: Relatório não encontrado.
tag: tag:
not_found: not_found:
other: Tag not found. other: Marcador não encontrado.
recommend_tag_not_found: recommend_tag_not_found:
other: Recommend Tag is not exist. other: O marcador recomendado não existe.
recommend_tag_enter: recommend_tag_enter:
other: Please enter at least one required tag. other: Por favor, insira pelo menos um marcador obrigatório.
not_contain_synonym_tags: not_contain_synonym_tags:
other: Should not contain synonym tags. other: Não deve conter marcadores sinónimos.
cannot_update: cannot_update:
other: No permission to update. other: Sem permissão para atualizar.
is_used_cannot_delete:
other: Não é possível excluir um marcador em uso
cannot_set_synonym_as_itself: 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 a si mesmo.
smtp: smtp:
config_from_name_cannot_be_email: config_from_name_cannot_be_email:
other: The From Name cannot be a email address. other: Nome do remetente não pode ser um endereço de e-mail.
theme: theme:
not_found: not_found:
other: Theme not found. other: Tema não encontrado.
revision: revision:
review_underway: review_underway:
other: Can't edit currently, there is a version in the review queue. other: Não é possível neste momento, há uma versão na fila de análise.
no_permission: no_permission:
other: No permission to Revision. other: Sem permissão para realizar Revisão.
user: user:
email_or_password_wrong: email_or_password_wrong:
other: other:
other: Email and password do not match. other: O e-mail e a senha não conferem.
not_found: not_found:
other: User not found. other: Usuário não encontrado.
suspended: suspended:
other: User has been suspended. other: O usuário foi suspenso.
username_invalid: username_invalid:
other: Username is invalid. other: Nome de usuário inválido.
username_duplicate: username_duplicate:
other: Username is already in use. other: O nome de usuário já em uso.
set_avatar: set_avatar:
other: Avatar set failed. other: Configuração de avatar falhou.
cannot_update_your_role: cannot_update_your_role:
other: You cannot modify your role. other: Você não pode modificar a sua função.
not_allowed_registration: not_allowed_registration:
other: Currently the site is not open for registration other: O site não está aberto para novos registros
config: config:
read_config_failed: read_config_failed:
other: Read config failed other: Falha ao ler configuração
database: database:
connection_failed: connection_failed:
other: Database connection failed other: Falha ao conectar-se ao banco de dados
create_table_failed: create_table_failed:
other: Create table failed other: Falha ao criar tabela
install: install:
create_config_failed: create_config_failed:
other: Can't create the config.yaml file. other: Não foi possível criar o arquivo de configuração.
upload: upload:
unsupported_file_format: unsupported_file_format:
other: Unsupported file format. other: Formato de arquivo não suportado.
report: report:
spam: spam:
name: name:
other: spam other: spam
desc: desc:
other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. other: Essa postagem é um anúncio, ou vandalismo. Não é útil ou relevante para o tópico atual.
rude: rude:
name: name:
other: rude or abusive other: rude ou abusivo
desc: desc:
other: A reasonable person would find this content inappropriate for respectful discourse. other: A reasonable person would find this content inappropriate for respectful discourse.
duplicate: duplicate:
@ -262,73 +264,73 @@ ui:
settings: Settings settings: Settings
notifications: Notifications notifications: Notifications
login: Log In login: Log In
sign_up: Sign Up sign_up: Registar-se
account_recovery: Account Recovery account_recovery: Recuperação de conta
account_activation: Account Activation account_activation: Ativação de conta
confirm_email: Confirm Email confirm_email: Confirmar E-mail
account_suspended: Account Suspended account_suspended: Conta suspensa
admin: Admin admin: Administrador
change_email: Modify Email change_email: Modificar e-mail
install: Answer Installation install: Instalação do Answer
upgrade: Answer Upgrade upgrade: Atualização do Answer
maintenance: Website Maintenance maintenance: Manutenção do website
users: Users users: Usuários
notifications: notifications:
title: Notifications title: Notificações
inbox: Inbox inbox: Caixa de entrada
achievement: Achievements achievement: Conquistas
all_read: Mark all as read all_read: Marcar todos como lida
show_more: Show more show_more: Mostrar mais
suspended: suspended:
title: Your Account has been Suspended title: A sua conta foi suspensa
until_time: "Your account was suspended until {{ time }}." until_time: "Sua conta está suspensa até {{ time }}."
forever: This user was suspended forever. forever: Este usuário foi suspenso permanentemente.
end: You don't meet a community guideline. end: Você não atende a uma diretriz da comunidade.
editor: editor:
blockquote: blockquote:
text: Blockquote text: Bloco de citação
bold: bold:
text: Strong text: Negrito
chart: chart:
text: Chart text: Gráfico
flow_chart: Flow chart flow_chart: Gráfico de fluxo
sequence_diagram: Sequence diagram sequence_diagram: Diagrama de sequência
class_diagram: Class diagram class_diagram: Diagrama de classe
state_diagram: State diagram state_diagram: Diagrama de estado
entity_relationship_diagram: Entity relationship diagram entity_relationship_diagram: Diagrama de relacionamento de entidade
user_defined_diagram: User defined diagram user_defined_diagram: Diagrama definido pelo usuário
gantt_chart: Gantt chart gantt_chart: Gráfico de Gantt
pie_chart: Pie chart pie_chart: Gráfico de pizza
code: code:
text: Code Sample text: Exemplo de código
add_code: Add code sample add_code: Adicionar exemplo de código
form: form:
fields: fields:
code: code:
label: Code label: Código
msg: msg:
empty: Code cannot be empty. empty: Código não pode ser vazio.
language: language:
label: Language (optional) label: Idioma
placeholder: Automatic detection placeholder: Deteção automática
btn_cancel: Cancel btn_cancel: Cancelar
btn_confirm: Add btn_confirm: Adicionar
formula: formula:
text: Formula text: Fórmula
options: options:
inline: Inline formula inline: Fórmula na linha
block: Block formula block: Bloco de fórmula
heading: heading:
text: Heading text: Cabeçalho
options: options:
h1: Heading 1 h1: Cabeçalho 1
h2: Heading 2 h2: Cabeçalho 2
h3: Heading 3 h3: Cabeçalho 3
h4: Heading 4 h4: Cabeçalho 4
h5: Heading 5 h5: Cabeçalho 5
h6: Heading 6 h6: Cabeçalho 6
help: help:
text: Help text: Ajuda
hr: hr:
text: Horizontal Rule text: Horizontal Rule
image: image:
@ -345,7 +347,7 @@ ui:
only_image: Only image files are allowed. only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB. max_size: File size cannot exceed 4MB.
desc: desc:
label: Description (optional) label: Description
tab_url: Image URL tab_url: Image URL
form_url: form_url:
fields: fields:
@ -354,7 +356,7 @@ ui:
msg: msg:
empty: Image URL cannot be empty. empty: Image URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
uploading: Uploading uploading: Uploading
@ -374,7 +376,7 @@ ui:
msg: msg:
empty: URL cannot be empty. empty: URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
ordered_list: ordered_list:
@ -422,7 +424,7 @@ ui:
range: URL slug up to 35 characters. range: URL slug up to 35 characters.
character: URL slug contains unallowed character set. character: URL slug contains unallowed character set.
desc: desc:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_submit: Submit btn_submit: Submit
tag_info: tag_info:
@ -439,9 +441,11 @@ ui:
synonyms_text: The following tags will be remapped to synonyms_text: The following tags will be remapped to
delete: delete:
title: Delete this tag title: Delete this tag
content: >- tip_with_posts: >-
<p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: Are you sure you wish to delete? tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Close close: Close
edit_tag: edit_tag:
title: Edit Tag title: Edit Tag
@ -677,13 +681,13 @@ ui:
default: System default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me
website: website:
label: Website (optional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Website incorrect format msg: Website incorrect format
location: location:
label: Location (optional) label: Location
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1313,6 +1309,7 @@ ui:
label: Login required label: Login required
text: Only logged in users can access this community. text: Only logged in users can access this community.
form: form:
optional: (optional)
empty: cannot be empty empty: cannot be empty
invalid: is invalid invalid: is invalid
btn_submit: Save btn_submit: Save
@ -1362,5 +1359,7 @@ ui:
staffs: Our community staff staffs: Our community staff
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

View File

@ -107,6 +107,8 @@ backend:
other: Не должно содержать теги синонимы. other: Не должно содержать теги синонимы.
cannot_update: cannot_update:
other: Нет прав для обновления. other: Нет прав для обновления.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: Вы не можете установить синоним текущего тега. other: Вы не можете установить синоним текущего тега.
smtp: smtp:
@ -309,7 +311,7 @@ ui:
msg: msg:
empty: Код не может быть пустым. empty: Код не может быть пустым.
language: language:
label: Language (optional) label: Language
placeholder: Automatic detection placeholder: Automatic detection
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
@ -345,7 +347,7 @@ ui:
only_image: Only image files are allowed. only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB. max_size: File size cannot exceed 4MB.
desc: desc:
label: Description (optional) label: Description
tab_url: Image URL tab_url: Image URL
form_url: form_url:
fields: fields:
@ -354,7 +356,7 @@ ui:
msg: msg:
empty: Image URL cannot be empty. empty: Image URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
uploading: Uploading uploading: Uploading
@ -374,7 +376,7 @@ ui:
msg: msg:
empty: URL cannot be empty. empty: URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
ordered_list: ordered_list:
@ -422,7 +424,7 @@ ui:
range: URL slug до 35 символов. range: URL slug до 35 символов.
character: URL slug содержит недопустимый набор символов. character: URL slug содержит недопустимый набор символов.
desc: desc:
label: Описание (опционально) label: Description
btn_cancel: Отмена btn_cancel: Отмена
btn_submit: Отправить btn_submit: Отправить
tag_info: tag_info:
@ -439,9 +441,11 @@ ui:
synonyms_text: Следующие теги будут переназначены на synonyms_text: Следующие теги будут переназначены на
delete: delete:
title: Удалить этот тег title: Удалить этот тег
content: >- tip_with_posts: >-
<p>Мы не разрешаем удалять тег с сообщениями.</p><p>Пожалуйста, сначала удалите этот тег из сообщений</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: Вы действительно хотите удалить? tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Закрыть close: Закрыть
edit_tag: edit_tag:
title: Изменить тег title: Изменить тег
@ -677,13 +681,13 @@ ui:
default: System default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me
website: website:
label: Website (optional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Website incorrect format msg: Website incorrect format
location: location:
label: Location (optional) label: Location
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1313,6 +1309,7 @@ ui:
label: Login required label: Login required
text: Only logged in users can access this community. text: Only logged in users can access this community.
form: form:
optional: (optional)
empty: cannot be empty empty: cannot be empty
invalid: is invalid invalid: is invalid
btn_submit: Save btn_submit: Save
@ -1362,5 +1359,7 @@ ui:
staffs: Our community staff staffs: Our community staff
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

View File

@ -2,23 +2,23 @@
backend: backend:
base: base:
success: success:
other: Success. other: Başarılı.
unknown: unknown:
other: Unknown error. other: Bilinmeyen hata.
request_format_error: request_format_error:
other: Request format is not valid. other: Request format is not valid.
unauthorized_error: unauthorized_error:
other: Unauthorized. other: İzin yok.
database_error: database_error:
other: Data server error. other: Veri sunucu hatası.
role: role:
name: name:
user: user:
other: User other: Kullanıcı
admin: admin:
other: Admin other: Yönetici
moderator: moderator:
other: Moderator other: Moderatör
description: description:
user: user:
other: Default with no special access. other: Default with no special access.
@ -27,131 +27,133 @@ backend:
moderator: moderator:
other: Has access to all posts except admin settings. other: Has access to all posts except admin settings.
email: email:
other: Email other: E-Posta
password: password:
other: Password other: Parola
email_or_password_wrong_error: email_or_password_wrong_error:
other: Email and password do not match. other: E-Posta ve parola eşleşmiyor.
error: error:
admin: admin:
email_or_password_wrong: email_or_password_wrong:
other: Email and password do not match. other: E-Posta ve parola eşleşmiyor.
answer: answer:
not_found: not_found:
other: Answer do not found. other: Cevap bulunamadı.
cannot_deleted: cannot_deleted:
other: No permission to delete. other: Silme izni yok.
cannot_update: cannot_update:
other: No permission to update. other: Düzenleme izni yok.
comment: comment:
edit_without_permission: edit_without_permission:
other: Comment are not allowed to edit. other: Yorum düzenleme izni yok.
not_found: not_found:
other: Comment not found. other: Yorum bulunamadı.
cannot_edit_after_deadline: cannot_edit_after_deadline:
other: The comment time has been too long to modify. other: The comment time has been too long to modify.
email: email:
duplicate: duplicate:
other: Email already exists. other: Bu e-posta adresi kullanılmaktadır.
need_to_be_verified: need_to_be_verified:
other: Email should be verified. other: E-Posta doğrulanmalı.
verify_url_expired: verify_url_expired:
other: Email verified URL has expired, please resend the email. other: Email verified URL has expired, please resend the email.
lang: lang:
not_found: not_found:
other: Language file not found. other: Dil dosyası bulunamadı.
object: object:
captcha_verification_failed: captcha_verification_failed:
other: Captcha wrong. other: Captcha yanlış.
disallow_follow: disallow_follow:
other: You are not allowed to follow. other: Takip etme izniniz yok.
disallow_vote: disallow_vote:
other: You are not allowed to vote. other: Oy verme yetkiniz yok.
disallow_vote_your_self: disallow_vote_your_self:
other: You can't vote for your own post. other: Kendi mesajınız için oy kullanamazsınız.
not_found: not_found:
other: Object not found. other: Nesne bulunamadı.
verification_failed: verification_failed:
other: Verification failed. other: Doğrulama başarısız.
email_or_password_incorrect: email_or_password_incorrect:
other: Email and password do not match. other: E-Posta ve parola eşleşmiyor.
old_password_verification_failed: old_password_verification_failed:
other: The old password verification failed other: Eski parola doğrulaması başarısız
new_password_same_as_previous_setting: new_password_same_as_previous_setting:
other: The new password is the same as the previous one. other: Yeni parola bir önceki parolanızın aynısı.
question: question:
not_found: not_found:
other: Question not found. other: Soru bulunamadı.
cannot_deleted: cannot_deleted:
other: No permission to delete. other: Silme izni yok.
cannot_close: cannot_close:
other: No permission to close. other: Kapatma izni yok.
cannot_update: cannot_update:
other: No permission to update. other: Düzenleme izni yok.
rank: rank:
fail_to_meet_the_condition: fail_to_meet_the_condition:
other: Rank fail to meet the condition. other: Rank fail to meet the condition.
report: report:
handle_failed: handle_failed:
other: Report handle failed. other: Rapor işlenemedi.
not_found: not_found:
other: Report not found. other: Rapor bulunamadı.
tag: tag:
not_found: not_found:
other: Tag not found. other: Etiket bulunamadı.
recommend_tag_not_found: recommend_tag_not_found:
other: Recommend Tag is not exist. other: Önerilen Etiket yok.
recommend_tag_enter: recommend_tag_enter:
other: Please enter at least one required tag. other: Lütfen en az bir adet gerekli etiket giriniz.
not_contain_synonym_tags: not_contain_synonym_tags:
other: Should not contain synonym tags. other: Should not contain synonym tags.
cannot_update: cannot_update:
other: No permission to update. other: Düzenleme izni yok.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: You cannot set the synonym of the current tag as itself. other: You cannot set the synonym of the current tag as itself.
smtp: smtp:
config_from_name_cannot_be_email: config_from_name_cannot_be_email:
other: The From Name cannot be a email address. other: Gönderen adı bir e-posta adresi olamaz.
theme: theme:
not_found: not_found:
other: Theme not found. other: Tema bulunamadı.
revision: revision:
review_underway: review_underway:
other: Can't edit currently, there is a version in the review queue. other: Can't edit currently, there is a version in the review queue.
no_permission: no_permission:
other: No permission to Revision. other: Revizyon izni yok.
user: user:
email_or_password_wrong: email_or_password_wrong:
other: other:
other: Email and password do not match. other: E-Posta ve parola eşleşmiyor.
not_found: not_found:
other: User not found. other: Kullanıcı bulunamadı.
suspended: suspended:
other: User has been suspended. other: Kullanıcı askıya alındı.
username_invalid: username_invalid:
other: Username is invalid. other: Kullanıcı adı geçersiz.
username_duplicate: username_duplicate:
other: Username is already in use. other: Kullanıcı adı zaten kullanımda.
set_avatar: set_avatar:
other: Avatar set failed. other: Avatar ayarlama başarısız.
cannot_update_your_role: cannot_update_your_role:
other: You cannot modify your role. other: Kendi rolünüzü değiştiremezsiniz.
not_allowed_registration: not_allowed_registration:
other: Currently the site is not open for registration other: Site şu anda kayıt olmaya açık değil
config: config:
read_config_failed: read_config_failed:
other: Read config failed other: Read config failed
database: database:
connection_failed: connection_failed:
other: Database connection failed other: Veri tabanı bağlantısı başarısız
create_table_failed: create_table_failed:
other: Create table failed other: Tablo oluşturulamadı
install: install:
create_config_failed: create_config_failed:
other: Can't create the config.yaml file. other: Config.yaml dosyası oluşturulamadı.
upload: upload:
unsupported_file_format: unsupported_file_format:
other: Unsupported file format. other: Desteklenmeyen dosya formatı.
report: report:
spam: spam:
name: name:
@ -170,7 +172,7 @@ backend:
other: This question has been asked before and already has an answer. other: This question has been asked before and already has an answer.
not_answer: not_answer:
name: name:
other: not an answer other: cevap değil
desc: 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: 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.
not_need: not_need:
@ -229,15 +231,15 @@ backend:
reply_to_you: reply_to_you:
other: replied to you other: replied to you
mention_you: mention_you:
other: mentioned you other: sizden bahsetti
your_question_is_closed: your_question_is_closed:
other: Your question has been closed other: Sorunuz kapatıldı
your_question_was_deleted: your_question_was_deleted:
other: Your question has been deleted other: Sorunuz silindi
your_answer_was_deleted: your_answer_was_deleted:
other: Your answer has been deleted other: Cevabınız silindi
your_comment_was_deleted: your_comment_was_deleted:
other: Your comment has been deleted other: Yorumunuz silindi
#The following fields are used for interface presentation(Front-end) #The following fields are used for interface presentation(Front-end)
ui: ui:
how_to_format: how_to_format:
@ -245,53 +247,53 @@ ui:
desc: >- desc: >-
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">put returns between paragraphs</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by placing <code>&gt;</code> at start of line</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">create code fences with backticks <code>`</code></p><pre class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul> <ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">put returns between paragraphs</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by placing <code>&gt;</code> at start of line</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">create code fences with backticks <code>`</code></p><pre class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul>
pagination: pagination:
prev: Prev prev: Önceki
next: Next next: Sonraki
page_title: page_title:
question: Question question: Soru
questions: Questions questions: Sorular
tag: Tag tag: Etiket
tags: Tags tags: Etiketler
tag_wiki: tag wiki tag_wiki: tag wiki
edit_tag: Edit Tag edit_tag: Etiketi Düzenle
ask_a_question: Add Question ask_a_question: Soru Ekle
edit_question: Edit Question edit_question: Soruyu Düzenle
edit_answer: Edit Answer edit_answer: Cevabı Düzenle
search: Search search: Ara
posts_containing: Posts containing posts_containing: Posts containing
settings: Settings settings: Ayarlar
notifications: Notifications notifications: Bildirimler
login: Log In login: Oturum Aç
sign_up: Sign Up sign_up: Üye Ol
account_recovery: Account Recovery account_recovery: Hesap Kurtarma
account_activation: Account Activation account_activation: Hesap Aktivasyonu
confirm_email: Confirm Email confirm_email: E-Posta Adresinizi Onaylayın
account_suspended: Account Suspended account_suspended: Hesap Askıya Alındı
admin: Admin admin: Yönetici
change_email: Modify Email change_email: E-Postayı değiştir
install: Answer Installation install: Answer Installation
upgrade: Answer Upgrade upgrade: Answer Upgrade
maintenance: Website Maintenance maintenance: Website Bakımı
users: Users users: Kullanıcı
notifications: notifications:
title: Notifications title: Bildirimler
inbox: Inbox inbox: Gelen Kutusu
achievement: Achievements achievement: Başarılar
all_read: Mark all as read all_read: Tümünü okundu olarak işaretle
show_more: Show more show_more: Daha fazla göster
suspended: suspended:
title: Your Account has been Suspended title: Hesabınız Askıya Alındı
until_time: "Your account was suspended until {{ time }}." until_time: "Your account was suspended until {{ time }}."
forever: This user was suspended forever. forever: This user was suspended forever.
end: You don't meet a community guideline. end: You don't meet a community guideline.
editor: editor:
blockquote: blockquote:
text: Blockquote text: Alıntı
bold: bold:
text: Strong text: Strong
chart: chart:
text: Chart text: Chart
flow_chart: Flow chart flow_chart: Akış Şeması
sequence_diagram: Sequence diagram sequence_diagram: Sequence diagram
class_diagram: Class diagram class_diagram: Class diagram
state_diagram: State diagram state_diagram: State diagram
@ -309,7 +311,7 @@ ui:
msg: msg:
empty: Code cannot be empty. empty: Code cannot be empty.
language: language:
label: Language (optional) label: Language
placeholder: Automatic detection placeholder: Automatic detection
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
@ -345,7 +347,7 @@ ui:
only_image: Only image files are allowed. only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB. max_size: File size cannot exceed 4MB.
desc: desc:
label: Description (optional) label: Description
tab_url: Image URL tab_url: Image URL
form_url: form_url:
fields: fields:
@ -354,10 +356,10 @@ ui:
msg: msg:
empty: Image URL cannot be empty. empty: Image URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: İptal
btn_confirm: Add btn_confirm: Ekle
uploading: Uploading uploading: Gönderiliyor
indent: indent:
text: Indent text: Indent
outdent: outdent:
@ -365,54 +367,54 @@ ui:
italic: italic:
text: Emphasis text: Emphasis
link: link:
text: Hyperlink text: Bağlantı
add_link: Add hyperlink add_link: Bağlantı ekle
form: form:
fields: fields:
url: url:
label: URL label: URL
msg: msg:
empty: URL cannot be empty. empty: URL boş olamaz.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: İptal Et
btn_confirm: Add btn_confirm: Ekle
ordered_list: ordered_list:
text: Numbered List text: Numaralı liste
unordered_list: unordered_list:
text: Bulleted List text: Bulleted List
table: table:
text: Table text: Tablo
heading: Heading heading: Başlık
cell: Cell cell: Hücre
close_modal: close_modal:
title: I am closing this post as... title: I am closing this post as...
btn_cancel: Cancel btn_cancel: İptal Et
btn_submit: Submit btn_submit: Gönder
remark: remark:
empty: Cannot be empty. empty: Boş olamaz.
msg: msg:
empty: Please select a reason. empty: Lütfen bir sebep seçin.
report_modal: report_modal:
flag_title: I am flagging to report this post as... flag_title: I am flagging to report this post as...
close_title: I am closing this post as... close_title: I am closing this post as...
review_question_title: Review question review_question_title: Review question
review_answer_title: Review answer review_answer_title: Review answer
review_comment_title: Review comment review_comment_title: Review comment
btn_cancel: Cancel btn_cancel: İptal Et
btn_submit: Submit btn_submit: Gönder
remark: remark:
empty: Cannot be empty. empty: Boş olamaz.
msg: msg:
empty: Please select a reason. empty: Lütfen bir sebep seçin.
tag_modal: tag_modal:
title: Create new tag title: Yeni etiket oluştur
form: form:
fields: fields:
display_name: display_name:
label: Display Name label: Görüntülenen Ad
msg: msg:
empty: Display name cannot be empty. empty: Görünen ad boş olamaz.
range: Display name up to 35 characters. range: Display name up to 35 characters.
slug_name: slug_name:
label: URL Slug label: URL Slug
@ -422,15 +424,15 @@ ui:
range: URL slug up to 35 characters. range: URL slug up to 35 characters.
character: URL slug contains unallowed character set. character: URL slug contains unallowed character set.
desc: desc:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: İptal Et
btn_submit: Submit btn_submit: Gönder
tag_info: tag_info:
created_at: Created created_at: Oluşturuldu
edited_at: Edited edited_at: Düzenlendi
history: History history: Geçmiş
synonyms: synonyms:
title: Synonyms title: Eş anlamlılar
text: The following tags will be remapped to text: The following tags will be remapped to
empty: No synonyms found. empty: No synonyms found.
btn_add: Add a synonym btn_add: Add a synonym
@ -438,30 +440,32 @@ ui:
btn_save: Save btn_save: Save
synonyms_text: The following tags will be remapped to synonyms_text: The following tags will be remapped to
delete: delete:
title: Delete this tag title: Bu etiketi sil
content: >- tip_with_posts: >-
<p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: Are you sure you wish to delete? tip_with_synonyms: >-
close: Close <p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Kapat
edit_tag: edit_tag:
title: Edit Tag title: Etiketi Düzenle
default_reason: Edit tag default_reason: Etiketi düzenle
form: form:
fields: fields:
revision: revision:
label: Revision label: Revizyon
display_name: display_name:
label: Display Name label: Görüntülenen Ad
slug_name: slug_name:
label: URL Slug label: URL Slug
info: 'Must use the character set "a-z", "0-9", "+ # - ."' info: 'Must use the character set "a-z", "0-9", "+ # - ."'
desc: desc:
label: Description label: ıklama
edit_summary: edit_summary:
label: Edit Summary label: Özeti Düzenle
placeholder: >- placeholder: >-
Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)
btn_save_edits: Save edits btn_save_edits: Düzenlemeleri kaydet
btn_cancel: Cancel btn_cancel: Cancel
dates: dates:
long_date: MMM D long_date: MMM D
@ -677,13 +681,13 @@ ui:
default: System default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me
website: website:
label: Website (optional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Website incorrect format msg: Website incorrect format
location: location:
label: Location (optional) label: Location
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1313,6 +1309,7 @@ ui:
label: Login required label: Login required
text: Only logged in users can access this community. text: Only logged in users can access this community.
form: form:
optional: (optional)
empty: cannot be empty empty: cannot be empty
invalid: is invalid invalid: is invalid
btn_submit: Save btn_submit: Save
@ -1362,5 +1359,7 @@ ui:
staffs: Our community staff staffs: Our community staff
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

View File

@ -107,6 +107,8 @@ backend:
other: Should not contain synonym tags. other: Should not contain synonym tags.
cannot_update: cannot_update:
other: No permission to update. other: No permission to update.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: You cannot set the synonym of the current tag as itself. other: You cannot set the synonym of the current tag as itself.
smtp: smtp:
@ -309,7 +311,7 @@ ui:
msg: msg:
empty: Code cannot be empty. empty: Code cannot be empty.
language: language:
label: Language (optional) label: Language
placeholder: Automatic detection placeholder: Automatic detection
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
@ -345,7 +347,7 @@ ui:
only_image: Only image files are allowed. only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB. max_size: File size cannot exceed 4MB.
desc: desc:
label: Description (optional) label: Description
tab_url: Image URL tab_url: Image URL
form_url: form_url:
fields: fields:
@ -354,7 +356,7 @@ ui:
msg: msg:
empty: Image URL cannot be empty. empty: Image URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
uploading: Uploading uploading: Uploading
@ -374,7 +376,7 @@ ui:
msg: msg:
empty: URL cannot be empty. empty: URL cannot be empty.
name: name:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_confirm: Add btn_confirm: Add
ordered_list: ordered_list:
@ -422,7 +424,7 @@ ui:
range: URL slug up to 35 characters. range: URL slug up to 35 characters.
character: URL slug contains unallowed character set. character: URL slug contains unallowed character set.
desc: desc:
label: Description (optional) label: Description
btn_cancel: Cancel btn_cancel: Cancel
btn_submit: Submit btn_submit: Submit
tag_info: tag_info:
@ -439,9 +441,11 @@ ui:
synonyms_text: The following tags will be remapped to synonyms_text: The following tags will be remapped to
delete: delete:
title: Delete this tag title: Delete this tag
content: >- tip_with_posts: >-
<p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p> <p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
content2: Are you sure you wish to delete? tip_with_synonyms: >-
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
tip: Are you sure you wish to delete?
close: Close close: Close
edit_tag: edit_tag:
title: Edit Tag title: Edit Tag
@ -677,13 +681,13 @@ ui:
default: System default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me
website: website:
label: Website (optional) label: Website
placeholder: "https://example.com" placeholder: "https://example.com"
msg: Website incorrect format msg: Website incorrect format
location: location:
label: Location (optional) label: Location
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications heading: Notifications
@ -1163,11 +1167,11 @@ ui:
validate: Please enter a valid URL. validate: Please enter a valid URL.
text: The address of your site. text: The address of your site.
short_desc: short_desc:
label: Short Site Description (optional) label: Short Site Description
msg: Short site description cannot be empty. msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage." text: "Short description, as used in the title tag on homepage."
desc: desc:
label: Site Description (optional) label: Site Description
msg: Site description cannot be empty. msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag." text: "Describe this site in one sentence, as used in the meta description tag."
contact_email: contact_email:
@ -1177,14 +1181,6 @@ ui:
text: Email address of key contact responsible for this site. text: Email address of key contact responsible for this site.
interface: interface:
page_title: Interface page_title: Interface
logo:
label: Logo (optional)
msg: Site logo cannot be empty.
text: You can upload your image or <1>reset</1> it to the site title text.
theme:
label: Theme
msg: Theme cannot be empty.
text: Select an existing theme.
language: language:
label: Interface Language label: Interface Language
msg: Interface language cannot be empty. msg: Interface language cannot be empty.
@ -1236,18 +1232,18 @@ ui:
branding: branding:
page_title: Branding page_title: Branding
logo: logo:
label: Logo (optional) label: Logo
msg: Logo cannot be empty. 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. 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.
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: Mobile Logo
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. 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.
square_icon: square_icon:
label: Square Icon (optional) label: Square Icon
msg: Square icon cannot be empty. msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon: favicon:
label: Favicon (optional) label: Favicon
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. 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.
legal: legal:
page_title: Legal page_title: Legal
@ -1313,6 +1309,7 @@ ui:
label: Login required label: Login required
text: Only logged in users can access this community. text: Only logged in users can access this community.
form: form:
optional: (optional)
empty: cannot be empty empty: cannot be empty
invalid: is invalid invalid: is invalid
btn_submit: Save btn_submit: Save
@ -1362,5 +1359,7 @@ ui:
staffs: Our community staff staffs: Our community staff
reputation: reputation reputation: reputation
votes: votes votes: votes
prompt:
leave_page: "Are you sure you want to leave the page?"
changes_not_save: "Your changes may not be saved."

View File

@ -1,4 +1,5 @@
#The following fields are used for back-end #The following fields are used for back-end
backend: backend:
base: base:
success: success:
@ -49,7 +50,7 @@ backend:
not_found: not_found:
other: 评论未找到。 other: 评论未找到。
cannot_edit_after_deadline: cannot_edit_after_deadline:
other: The comment time has been too long to modify. other: 评论时间太长,无法修改。
email: email:
duplicate: duplicate:
other: 邮箱已经存在。 other: 邮箱已经存在。
@ -80,6 +81,8 @@ backend:
new_password_same_as_previous_setting: new_password_same_as_previous_setting:
other: 新密码与之前的设置相同 other: 新密码与之前的设置相同
question: question:
already_deleted:
other: 该内容已被删除
not_found: not_found:
other: 问题未找到 other: 问题未找到
cannot_deleted: cannot_deleted:
@ -108,7 +111,7 @@ backend:
cannot_update: cannot_update:
other: 没有更新标签权限。 other: 没有更新标签权限。
is_used_cannot_delete: is_used_cannot_delete:
other: 您不能删除正在使用的标签 other: 您不能删除正在使用的标签
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: 您不能将当前标签的同义词设置为本身。 other: 您不能将当前标签的同义词设置为本身。
smtp: smtp:
@ -150,10 +153,10 @@ backend:
other: 创建表失败 other: 创建表失败
install: install:
create_config_failed: create_config_failed:
other: Can't create the config.yaml file. other: 无法创建 config.yaml 文件。
upload: upload:
unsupported_file_format: unsupported_file_format:
other: Unsupported file format. other: 不支持的文件格式。
report: report:
spam: spam:
name: name:
@ -311,7 +314,7 @@ ui:
msg: msg:
empty: 代码块不能为空 empty: 代码块不能为空
language: language:
label: 语言 (可选) label: 语言
placeholder: 自动识别 placeholder: 自动识别
btn_cancel: 取消 btn_cancel: 取消
btn_confirm: 添加 btn_confirm: 添加
@ -347,7 +350,7 @@ ui:
only_image: 只能上传图片文件。 only_image: 只能上传图片文件。
max_size: 图片文件大小不能超过 4 MB。 max_size: 图片文件大小不能超过 4 MB。
desc: desc:
label: 描述(可选) label: 描述
tab_url: 网络图片 tab_url: 网络图片
form_url: form_url:
fields: fields:
@ -356,7 +359,7 @@ ui:
msg: msg:
empty: 图片地址不能为空 empty: 图片地址不能为空
name: name:
label: 图片描述(可选) label: 描述
btn_cancel: 取消 btn_cancel: 取消
btn_confirm: 添加 btn_confirm: 添加
uploading: 上传中... uploading: 上传中...
@ -376,7 +379,7 @@ ui:
msg: msg:
empty: 链接不能为空。 empty: 链接不能为空。
name: name:
label: 链接描述(可选) label: 描述
btn_cancel: 取消 btn_cancel: 取消
btn_confirm: 添加 btn_confirm: 添加
ordered_list: ordered_list:
@ -424,7 +427,7 @@ ui:
range: URL 固定链接不能超过 35 个字符。 range: URL 固定链接不能超过 35 个字符。
character: URL 固定链接包含非法字符。 character: URL 固定链接包含非法字符。
desc: desc:
label: 描述(可选) label: 描述
btn_cancel: 取消 btn_cancel: 取消
btn_submit: 提交 btn_submit: 提交
tag_info: tag_info:
@ -441,9 +444,11 @@ ui:
synonyms_text: 以下标签等同于 synonyms_text: 以下标签等同于
delete: delete:
title: 删除标签 title: 删除标签
content: >- tip_with_posts: >-
<p>不允许删除有关联问题的标签。</p><p>请先从关联的问题中删除此标签的引用。</p> <p>我们不允许 <strong>删除带有同义词的标签</strong>。</p> <p>请先从此标签中删除同义词。</p>
content2: 确定要删除吗? tip_with_synonyms: >-
<p>我们不允许 <strong>删除带有同义词的标签</strong>。</p> <p>请先从此标签中删除同义词。</p>
tip: 确定要删除吗?
close: 关闭 close: 关闭
edit_tag: edit_tag:
title: 编辑标签 title: 编辑标签
@ -484,7 +489,7 @@ ui:
btn_flag: 举报 btn_flag: 举报
btn_save_edits: 保存 btn_save_edits: 保存
btn_cancel: 取消 btn_cancel: 取消
show_more: Show more comments show_more: 显示更多评论
tip_question: >- tip_question: >-
使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。
tip_answer: >- tip_answer: >-
@ -499,7 +504,7 @@ ui:
answer: answer:
label: 回答内容 label: 回答内容
feedback: feedback:
characters: content must be at least 6 characters in length. characters: 内容长度至少 6 个字符
edit_summary: edit_summary:
label: 编辑概要 label: 编辑概要
placeholder: >- placeholder: >-
@ -624,7 +629,7 @@ ui:
msg: msg:
empty: 邮箱不能为空 empty: 邮箱不能为空
change_email: change_email:
page_title: Welcome to {{site_name}} page_title: 欢迎来到 {{site_name}}
btn_cancel: 取消 btn_cancel: 取消
btn_update: 更新电子邮件地址 btn_update: 更新电子邮件地址
send_success: >- send_success: >-
@ -662,12 +667,12 @@ ui:
display_name: display_name:
label: 昵称 label: 昵称
msg: 昵称不能为空。 msg: 昵称不能为空。
msg_range: Display name up to 30 characters. msg_range: 显示名称不能超过 30 个字符。
username: username:
label: 用户名 label: 用户名
caption: 用户之间可以通过 "@用户名" 进行交互。 caption: 用户之间可以通过 "@用户名" 进行交互。
msg: 用户名不能为空 msg: 用户名不能为空
msg_range: Username up to 30 characters. msg_range: 用户名不能超过 30 个字符。
character: '用户名只能由 "a-z", "0-9", " - . _" 组成' character: '用户名只能由 "a-z", "0-9", " - . _" 组成'
avatar: avatar:
label: 头像 label: 头像
@ -679,13 +684,13 @@ ui:
default: 系统 default: 系统
msg: 请上传头像 msg: 请上传头像
bio: bio:
label: 关于我 (可选) label: 关于我
website: website:
label: 网站 (可选) label: 网站
placeholder: "https://example.com" placeholder: "https://example.com"
msg: 格式不正确 msg: 格式不正确
location: location:
label: 位置 (可选) label: 位置
placeholder: "城市, 国家" placeholder: "城市, 国家"
notification: notification:
heading: 通知 heading: 通知
@ -753,7 +758,7 @@ ui:
confirm_info: >- confirm_info: >-
<p>您确定要提交一个新的回答吗?</p><p>您可以直接编辑和改善您之前的回答的。</p> <p>您确定要提交一个新的回答吗?</p><p>您可以直接编辑和改善您之前的回答的。</p>
empty: 回答内容不能为空。 empty: 回答内容不能为空。
characters: content must be at least 6 characters in length. characters: 内容长度至少 6 个字符
reopen: reopen:
title: 重新打开这个帖子 title: 重新打开这个帖子
content: 确定要重新打开吗? content: 确定要重新打开吗?
@ -814,7 +819,7 @@ ui:
modal_confirm: modal_confirm:
title: 发生错误... title: 发生错误...
account_result: account_result:
page_title: Welcome to {{site_name}} page_title: 欢迎来到 {{site_name}}
success: 你的账号已通过验证,即将返回首页。 success: 你的账号已通过验证,即将返回首页。
link: 返回首页 link: 返回首页
invalid: >- invalid: >-
@ -825,7 +830,7 @@ ui:
unsubscribe: unsubscribe:
page_title: 退订 page_title: 退订
success_title: 取消订阅成功 success_title: 取消订阅成功
success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. success_desc: 您已成功地从此订阅者列表中移除,并且将不会再收到我们的任何电子邮件。
link: 更改设置 link: 更改设置
question: question:
following_tags: 已关注的标签 following_tags: 已关注的标签
@ -887,7 +892,7 @@ ui:
title: Answer title: Answer
next: 下一步 next: 下一步
done: 完成 done: 完成
config_yaml_error: Can't create the config.yaml file. config_yaml_error: 无法创建 config.yaml 文件。
lang: lang:
label: 请选择一种语言 label: 请选择一种语言
db_type: db_type:
@ -917,7 +922,7 @@ ui:
label: 已创建 config.yaml 文件。 label: 已创建 config.yaml 文件。
desc: >- desc: >-
您可以手动在 <1>/var/wwww/xxx/</1> 目录中创建<1>config.yaml</1> 文件并粘贴以下文本。 您可以手动在 <1>/var/wwww/xxx/</1> 目录中创建<1>config.yaml</1> 文件并粘贴以下文本。
info: After you've done that, click "Next" button. info: 完成后,点击“下一步”按钮。
site_information: 站点信息 site_information: 站点信息
admin_account: 管理员账户 admin_account: 管理员账户
site_name: site_name:
@ -962,12 +967,12 @@ ui:
您似乎已经安装过了。要重新安装,请先清除旧的数据库表。 您似乎已经安装过了。要重新安装,请先清除旧的数据库表。
db_failed: 数据连接异常! db_failed: 数据连接异常!
db_failed_desc: >- db_failed_desc: >-
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your hosts database server is down. 这或者意味着数据库信息在 <1>config.yaml</1> 文件不正确,或者无法与数据库服务器建立联系。这可能意味着您的主机数据库服务器已关闭。
counts: counts:
views: views views: 次浏览
votes: votes votes: 个点赞
answers: answers answers: 个回答
accepted: Accepted accepted: 已被采纳
page_404: page_404:
desc: "很抱歉,此页面不存在。" desc: "很抱歉,此页面不存在。"
back_home: 回到主页 back_home: 回到主页
@ -975,7 +980,7 @@ ui:
desc: 服务器遇到了一个错误,无法完成你的请求。 desc: 服务器遇到了一个错误,无法完成你的请求。
back_home: 回到主页 back_home: 回到主页
page_maintenance: page_maintenance:
desc: "We are under maintenance, we'll be back soon." desc: "我们正在进行维护,我们将很快回来。"
nav_menus: nav_menus:
dashboard: 后台管理 dashboard: 后台管理
contents: 内容管理 contents: 内容管理
@ -1116,7 +1121,7 @@ ui:
password: password:
label: 密码 label: 密码
text: 用户将被注销,需要再次登录。 text: 用户将被注销,需要再次登录。
msg: Password must be at 8-32 characters in length. msg: 密码的长度必须是8-32个字符。
btn_cancel: 取消 btn_cancel: 取消
btn_submit: 提交 btn_submit: 提交
user_modal: user_modal:
@ -1125,13 +1130,13 @@ ui:
fields: fields:
display_name: display_name:
label: 昵称 label: 昵称
msg: Display Name must be at 3-30 characters in length. msg: 显示名称长度必须为 3-30 个字符
email: email:
label: 邮箱 label: 邮箱
msg: 电子邮箱无效。 msg: 电子邮箱无效。
password: password:
label: 密码 label: 密码
msg: Password must be at 8-32 characters in length. msg: 密码的长度必须是8-32个字符。
btn_cancel: 取消 btn_cancel: 取消
btn_submit: 提交 btn_submit: 提交
questions: questions:
@ -1172,11 +1177,11 @@ ui:
validate: 请输入一个有效的 URL。 validate: 请输入一个有效的 URL。
text: 此网站的地址。 text: 此网站的地址。
short_desc: short_desc:
label: 简短站描述(可选) label: 简短描述
msg: 简短网站描述不能为空。 msg: 简短网站描述不能为空。
text: "简短的标语作为网站主页的标题Html 的 title 标签)。" text: "简短的标语作为网站主页的标题Html 的 title 标签)。"
desc: desc:
label: 站描述 (可选) label: 描述
msg: 网站描述不能为空。 msg: 网站描述不能为空。
text: "使用一句话描述本站作为网站的描述Html 的 meta 标签)。" text: "使用一句话描述本站作为网站的描述Html 的 meta 标签)。"
contact_email: contact_email:
@ -1186,14 +1191,6 @@ ui:
text: 负责本网站的主要联系人的电子邮件地址。 text: 负责本网站的主要联系人的电子邮件地址。
interface: interface:
page_title: 界面 page_title: 界面
logo:
label: Logo (可选)
msg: 不能为空
text: 可以上传图片,或者<1>重置</1>为站点标题。
theme:
label: 主题
msg: 不能为空
text: 选择一个主题
language: language:
label: 界面语言 label: 界面语言
msg: 不能为空 msg: 不能为空
@ -1245,19 +1242,19 @@ ui:
branding: branding:
page_title: 品牌 page_title: 品牌
logo: logo:
label: Logo (可选) label: 网站标志(Logo)
msg: 图标不能为空。 msg: 图标不能为空。
text: 在你的网站左上方的Logo图标。使用一个高度为56长宽比大于3:1的宽长方形图像。如果留空将显示网站标题文本。 text: 在你的网站左上方的Logo图标。使用一个高度为56长宽比大于3:1的宽长方形图像。如果留空将显示网站标题文本。
mobile_logo: mobile_logo:
label: 移动端图标(可选) label: 移动端 Logo
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. text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空将使用 "Logo"设置中的图像。
square_icon: square_icon:
label: 方形图标 (可选) label: 方形图标
msg: 方形图标不能为空。 msg: 方形图标不能为空。
text: 用作元数据图标的基础的图像。最好是大于512x512。 text: 用作元数据图标的基础的图像。最好是大于512x512。
favicon: favicon:
label: 收藏夹图标(可选) label: 收藏夹图标
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. text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空将使用“方形图标”。
legal: legal:
page_title: 法律条款 page_title: 法律条款
terms_of_service: terms_of_service:
@ -1339,6 +1336,7 @@ ui:
activate: 启用 activate: 启用
settings: 设置 settings: 设置
form: form:
optional: (选填)
empty: 不能为空 empty: 不能为空
invalid: 是无效的 invalid: 是无效的
btn_submit: 保存 btn_submit: 保存
@ -1388,5 +1386,7 @@ ui:
staffs: 我们的社区工作人员 staffs: 我们的社区工作人员
reputation: 声望值 reputation: 声望值
votes: 投票 votes: 投票
prompt:
leave_page: "确定要离开此页面?"
changes_not_save: "您的更改尚未保存"

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ const (
QuestionCannotDeleted = "error.question.cannot_deleted" QuestionCannotDeleted = "error.question.cannot_deleted"
QuestionCannotClose = "error.question.cannot_close" QuestionCannotClose = "error.question.cannot_close"
QuestionCannotUpdate = "error.question.cannot_update" QuestionCannotUpdate = "error.question.cannot_update"
QuestionAlreadyDeleted = "error.question.already_deleted"
AnswerNotFound = "error.answer.not_found" AnswerNotFound = "error.answer.not_found"
AnswerCannotDeleted = "error.answer.cannot_deleted" AnswerCannotDeleted = "error.answer.cannot_deleted"
AnswerCannotUpdate = "error.answer.cannot_update" AnswerCannotUpdate = "error.answer.cannot_update"
@ -64,4 +65,6 @@ const (
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself" TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
NotAllowedRegistration = "error.user.not_allowed_registration" NotAllowedRegistration = "error.user.not_allowed_registration"
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email" SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
) )

View File

@ -20,6 +20,8 @@ var GlobalTrans i18n.Translator
type LangOption struct { type LangOption struct {
Label string `json:"label"` Label string `json:"label"`
Value string `json:"value"` 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. // DefaultLangOption default language option. If user config the language is default, the language option is admin choose.
@ -47,6 +49,7 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" { if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" {
continue continue
} }
log.Debugf("try to read file: %s", file.Name())
buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name())) buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name()))
if err != nil { if err != nil {
return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err) return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err)
@ -96,6 +99,11 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
return nil, fmt.Errorf("i18n file parsing failed: %s", err) return nil, fmt.Errorf("i18n file parsing failed: %s", err)
} }
LanguageOptions = s.LangOption LanguageOptions = s.LangOption
for _, option := range LanguageOptions {
if option.Progress != 100 {
option.Label = fmt.Sprintf("%s (%d%%)", option.Label, option.Progress)
}
}
return GlobalTrans, err return GlobalTrans, err
} }

View File

@ -112,6 +112,18 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(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) can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentEdit, req.CommentID)
if err != nil { if err != nil {
handler.HandleResponse(ctx, err, nil) handler.HandleResponse(ctx, err, nil)
@ -122,8 +134,8 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
return return
} }
err = cc.commentService.UpdateComment(ctx, req) resp, err := cc.commentService.UpdateComment(ctx, req)
handler.HandleResponse(ctx, err, nil) handler.HandleResponse(ctx, err, resp)
} }
// GetCommentWithPage get comment page // GetCommentWithPage get comment page

View File

@ -41,7 +41,6 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) {
// @Tags Lang // @Tags Lang
// @Produce json // @Produce json
// @Success 200 {object} handler.RespBody{} // @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/language/options [get]
// @Router /answer/admin/api/language/options [get] // @Router /answer/admin/api/language/options [get]
func (u *LangController) GetAdminLangOptions(ctx *gin.Context) { func (u *LangController) GetAdminLangOptions(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, translator.LanguageOptions) handler.HandleResponse(ctx, nil, translator.LanguageOptions)

View File

@ -404,22 +404,17 @@ func (tc *TemplateController) UserInfo(ctx *gin.Context) {
req := &schema.GetOtherUserInfoByUsernameReq{} req := &schema.GetOtherUserInfoByUsernameReq{}
req.Username = username req.Username = username
userinfo, err := tc.templateRenderController.UserInfo(ctx, req) userinfo, err := tc.templateRenderController.UserInfo(ctx, req)
if err != nil { if err != nil {
tc.Page404(ctx) tc.Page404(ctx)
return return
} }
if !userinfo.Has {
tc.Page404(ctx)
return
}
siteInfo := tc.SiteInfo(ctx) siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username) siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username)
siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name) siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{ tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{
"userinfo": userinfo, "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 { if !ok {
data["path"] = "" data["path"] = ""
} }
ctx.Header("X-Frame-Options", "DENY")
ctx.HTML(code, tpl, data) ctx.HTML(code, tpl, data)
} }

View File

@ -5,6 +5,6 @@ import (
"golang.org/x/net/context" "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) return q.userService.GetOtherUserInfoByUsername(ctx, req.Username)
} }

View File

@ -157,7 +157,7 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
return return
} }
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP()) _, _ = 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) handler.HandleResponse(ctx, err, nil)
} }
@ -203,6 +203,7 @@ func (uc *UserController) UserLogout(ctx *gin.Context) {
return return
} }
_ = uc.authService.RemoveUserCacheInfo(ctx, accessToken) _ = uc.authService.RemoveUserCacheInfo(ctx, accessToken)
_ = uc.authService.RemoveAdminUserCacheInfo(ctx, accessToken)
handler.HandleResponse(ctx, nil, nil) handler.HandleResponse(ctx, nil, nil)
} }

View File

@ -34,6 +34,8 @@ func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) {
return return
} }
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
err := uc.userService.UpdateUserStatus(ctx, req) err := uc.userService.UpdateUserStatus(ctx, req)
handler.HandleResponse(ctx, err, nil) handler.HandleResponse(ctx, err, nil)
} }

View File

@ -35,6 +35,8 @@ type Answer struct {
type AnswerSearch struct { type AnswerSearch struct {
Answer Answer
IncludeDeleted bool `json:"include_deleted"`
LoginUserID string `json:"login_user_id"`
Order string `json:"order_by"` // default or updated Order string `json:"order_by"` // default or updated
Page int `json:"page" form:"page"` // Query number of pages Page int `json:"page" form:"page"` // Query number of pages
PageSize int `json:"page_size" form:"page_size"` // Search page size PageSize int `json:"page_size" form:"page_size"` // Search page size

View File

@ -78,7 +78,7 @@ type InitEnvironmentResp struct {
// InitBaseInfoReq init base info request // InitBaseInfoReq init base info request
type InitBaseInfoReq struct { type InitBaseInfoReq struct {
Language string `validate:"required,gt=0,lte=30" json:"lang"` 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"` SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"`
ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"` ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"`
AdminName string `validate:"required,gt=3,lte=30" json:"name"` AdminName string `validate:"required,gt=3,lte=30" json:"name"`

View File

@ -206,7 +206,9 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
default: default:
session = session.OrderBy("adopted desc,vote_count desc,created_at asc") 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) session = session.Limit(search.PageSize, offset)
count, err = session.FindAndCount(&rows) count, err = session.FindAndCount(&rows)

View File

@ -68,3 +68,11 @@ func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha stri
} }
return captcha, nil 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
}

View File

@ -106,6 +106,7 @@ func (a *UIRouter) Register(r *gin.Engine) {
default: default:
filePath = UIIndexFilePath filePath = UIIndexFilePath
c.Header("content-type", "text/html;charset=utf-8") c.Header("content-type", "text/html;charset=utf-8")
c.Header("X-Frame-Options", "DENY")
} }
file, err := ui.Build.ReadFile(filePath) file, err := ui.Build.ReadFile(filePath)
if err != nil { if err != nil {

View File

@ -83,6 +83,7 @@ type AnswerInfo struct {
VoteStatus string `json:"vote_status"` VoteStatus string `json:"vote_status"`
VoteCount int `json:"vote_count"` VoteCount int `json:"vote_count"`
QuestionInfo *QuestionInfo `json:"question_info,omitempty"` QuestionInfo *QuestionInfo `json:"question_info,omitempty"`
Status int `json:"status"`
// MemberActions // MemberActions
MemberActions []*PermissionMemberAction `json:"member_actions"` MemberActions []*PermissionMemberAction `json:"member_actions"`

View File

@ -2,10 +2,9 @@ package schema
// UpdateUserStatusReq update user request // UpdateUserStatusReq update user request
type UpdateUserStatusReq struct { type UpdateUserStatusReq struct {
// user id
UserID string `validate:"required" json:"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"` Status string `validate:"required,oneof=normal suspended deleted inactive" json:"status" enums:"normal,suspended,deleted,inactive"`
LoginUserID string `json:"-"`
} }
const ( const (

View File

@ -53,6 +53,12 @@ type UpdateCommentReq struct {
// user id // user id
UserID string `json:"-"` UserID string `json:"-"`
IsAdmin bool `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) { func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) {

View File

@ -176,11 +176,20 @@ type AdminQuestionInfo struct {
UserInfo *UserBasicInfo `json:"user_info"` UserInfo *UserBasicInfo `json:"user_info"`
} }
type OperationLevel string
const (
OperationLevelInfo OperationLevel = "info"
OperationLevelDanger OperationLevel = "danger"
OperationLevelWarning OperationLevel = "warning"
)
type Operation struct { type Operation struct {
OperationType string `json:"operation_type"` Type string `json:"type"`
OperationDescription string `json:"operation_description"` Description string `json:"description"`
OperationMsg string `json:"operation_msg"` Msg string `json:"msg"`
OperationTime int64 `json:"operation_time"` Time int64 `json:"time"`
Level OperationLevel `json:"level"`
} }
type GetCloseTypeResp struct { type GetCloseTypeResp struct {

View File

@ -90,7 +90,7 @@ type GetTagResp struct {
} }
func (tr *GetTagResp) GetExcerpt() { func (tr *GetTagResp) GetExcerpt() {
excerpt := strings.TrimSpace(tr.OriginalText) excerpt := strings.TrimSpace(tr.ParsedText)
idx := strings.Index(excerpt, "\n") idx := strings.Index(excerpt, "\n")
if idx >= 0 { if idx >= 0 {
excerpt = excerpt[0:idx] excerpt = excerpt[0:idx]

View File

@ -316,7 +316,7 @@ func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, er
return errFields, errors.BadRequest(reason.UsernameInvalid) return errFields, errors.BadRequest(reason.UsernameInvalid)
} }
} }
req.BioHTML = converter.Markdown2HTML(req.Bio) req.BioHTML = converter.Markdown2BasicHTML(req.Bio)
return nil, nil return nil, nil
} }
@ -393,7 +393,6 @@ type GetOtherUserInfoByUsernameReq struct {
type GetOtherUserInfoResp struct { type GetOtherUserInfoResp struct {
Info *GetOtherUserInfoByUsernameResp `json:"info"` Info *GetOtherUserInfoByUsernameResp `json:"info"`
Has bool `json:"has"`
} }
type UserChangeEmailSendCodeReq struct { type UserChangeEmailSendCodeReq struct {

View File

@ -16,6 +16,7 @@ import (
type CaptchaRepo interface { type CaptchaRepo interface {
SetCaptcha(ctx context.Context, key, captcha string) (err error) SetCaptcha(ctx context.Context, key, captcha string) (err error)
GetCaptcha(ctx context.Context, key string) (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) SetActionType(ctx context.Context, ip, actionType string, amount int) (err error)
GetActionType(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) 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) { func (cs *CaptchaService) VerifyCaptcha(ctx context.Context, key, captcha string) (isCorrect bool, err error) {
realCaptcha, err := cs.captchaRepo.GetCaptcha(ctx, key) realCaptcha, err := cs.captchaRepo.GetCaptcha(ctx, key)
if err != nil { 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 false, nil
} }
return strings.TrimSpace(captcha) == realCaptcha, nil return strings.TrimSpace(captcha) == realCaptcha, nil

View File

@ -73,6 +73,7 @@ func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *sc
} }
info.UserID = data.UserID info.UserID = data.UserID
info.UpdateUserID = data.LastEditUserID info.UpdateUserID = data.LastEditUserID
info.Status = data.Status
return &info return &info
} }

View File

@ -164,7 +164,7 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
if err != nil { if err != nil {
log.Error("UpdateLastAnswer error", err.Error()) log.Error("UpdateLastAnswer error", err.Error())
} }
err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID) err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID)
if err != nil { if err != nil {
return insertData.ID, err return insertData.ID, err
} }
@ -232,6 +232,11 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq
return "", nil return "", nil
} }
if answerInfo.Status == entity.AnswerStatusDeleted {
err = errors.BadRequest(reason.AnswerCannotUpdate)
return "", err
}
//If the content is the same, ignore it //If the content is the same, ignore it
if answerInfo.OriginalText == req.Content { if answerInfo.OriginalText == req.Content {
return "", nil 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 { if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}); err != nil {
return "", err return "", err
} }
err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID) err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID)
if err != nil { if err != nil {
return insertData.ID, err return insertData.ID, err
} }
@ -473,6 +478,8 @@ func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListR
dbSearch.Page = req.Page dbSearch.Page = req.Page
dbSearch.PageSize = req.PageSize dbSearch.PageSize = req.PageSize
dbSearch.Order = req.Order dbSearch.Order = req.Order
dbSearch.IncludeDeleted = req.CanDelete
dbSearch.LoginUserID = req.UserID
answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch) answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch)
if err != nil { if err != nil {
return list, count, err return list, count, err

View File

@ -209,24 +209,40 @@ func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveC
} }
// UpdateComment update comment // 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) old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
if err != nil { if err != nil {
return return
} }
if !exist { 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. // user can edit the comment that was posted by himself before deadline.
if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) { 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{} comment := &entity.Comment{}
_ = copier.Copy(comment, req) _ = copier.Copy(comment, req)
comment.ID = req.CommentID 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 // GetComment get comment one

View File

@ -89,6 +89,7 @@ func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.Das
} }
startTime := time.Now().Unix() - schema.AppStartTime.Unix() startTime := time.Now().Unix() - schema.AppStartTime.Unix()
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
dashboardInfo.VersionInfo.Version = constant.Version
return dashboardInfo, nil return dashboardInfo, nil
} }

View File

@ -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) 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) 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{} questioninfo := &entity.Question{}
now := time.Now() now := time.Now()
questioninfo.ID = questionID questioninfo.ID = questionID
questioninfo.PostUpdateTime = now questioninfo.PostUpdateTime = now
return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"}) 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 := &entity.Question{}
questioninfo.ID = questionID questioninfo.ID = questionID
questioninfo.PostUpdateTime = setTime questioninfo.PostUpdateTime = setTime
@ -148,7 +148,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
return showinfo, err return showinfo, err
} }
if !has { if !has {
return showinfo, errors.BadRequest(reason.QuestionNotFound) return showinfo, errors.NotFound(reason.QuestionNotFound)
} }
showinfo = qs.ShowFormat(ctx, dbinfo) 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()) log.Error("json.Unmarshal QuestionCloseJson error", err.Error())
} else { } else {
operation := &schema.Operation{} operation := &schema.Operation{}
operation.OperationType = closeinfo.Name operation.Type = closeinfo.Name
operation.OperationDescription = closeinfo.Description operation.Description = closeinfo.Description
operation.OperationMsg = closemsg.CloseMsg operation.Msg = closemsg.CloseMsg
operation.OperationTime = metainfo.CreatedAt.Unix() operation.Time = metainfo.CreatedAt.Unix()
operation.Level = schema.OperationLevelInfo
showinfo.Operation = operation showinfo.Operation = operation
} }

View File

@ -470,6 +470,10 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
if !has { if !has {
return return
} }
if dbinfo.Status == entity.QuestionStatusDeleted {
err = errors.BadRequest(reason.QuestionCannotUpdate)
return nil, err
}
now := time.Now() now := time.Now()
question := &entity.Question{} question := &entity.Question{}
@ -614,12 +618,23 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
if err != nil { if err != nil {
return 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 { if question.Status != entity.QuestionStatusClosed {
per.CanReopen = false per.CanReopen = false
} }
if question.Status == entity.QuestionStatusClosed { if question.Status == entity.QuestionStatusClosed {
per.CanClose = false 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.Description = htmltext.FetchExcerpt(question.HTML, "...", 240)
question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID, question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID,
per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen) 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, func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string,
per schema.QuestionPermission) ( per schema.QuestionPermission) (
resp *schema.QuestionInfo, err error) { resp *schema.QuestionInfo, err error) {
err = qs.questioncommon.UpdataPv(ctx, questionID) err = qs.questioncommon.UpdatePv(ctx, questionID)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }

View File

@ -191,7 +191,7 @@ func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem
if saveerr != nil { if saveerr != nil {
return saveerr return saveerr
} }
saveerr = rs.questionCommon.UpdataPostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime) saveerr = rs.questionCommon.UpdatePostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime)
if saveerr != nil { if saveerr != nil {
return saveerr return saveerr
} }

View File

@ -102,7 +102,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
return nil, err return nil, err
} }
if !exist { if !exist {
return nil, errors.BadRequest(reason.TagNotFound) return nil, errors.NotFound(reason.TagNotFound)
} }
resp = &schema.GetTagResp{} resp = &schema.GetTagResp{}
@ -113,7 +113,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
return nil, err return nil, err
} }
if !exist { if !exist {
return nil, errors.BadRequest(reason.TagNotFound) return nil, errors.NotFound(reason.TagNotFound)
} }
resp.MainTagSlugName = tagInfo.SlugName resp.MainTagSlugName = tagInfo.SlugName
} }

View File

@ -61,6 +61,10 @@ func NewUserAdminService(
// UpdateUserStatus update user // UpdateUserStatus update user
func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) { 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) userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
if err != nil { if err != nil {
return return
@ -153,6 +157,10 @@ func (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq)
// UpdateUserPassword update user password // UpdateUserPassword update user password
func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) { 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) userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
if err != nil { if err != nil {
return err return err

View File

@ -92,19 +92,17 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
} }
func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) ( 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) userInfo, exist, err := us.userRepo.GetByUsername(ctx, username)
if err != nil { if err != nil {
return nil, err return nil, err
} }
resp = &schema.GetOtherUserInfoResp{}
if !exist { if !exist {
return resp, nil return nil, errors.NotFound(reason.UserNotFound)
} }
resp.Has = true resp = &schema.GetOtherUserInfoByUsernameResp{}
resp.Info = &schema.GetOtherUserInfoByUsernameResp{} resp.GetFromUserEntity(userInfo)
resp.Info.GetFromUserEntity(userInfo)
return resp, nil return resp, nil
} }
@ -155,13 +153,13 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
} }
// RetrievePassWord . // 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) userInfo, has, err := us.userRepo.GetByEmail(ctx, req.Email)
if err != nil { if err != nil {
return "", err return err
} }
if !has { if !has {
return "", errors.BadRequest(reason.UserNotFound) return nil
} }
// send email // send email
@ -173,10 +171,10 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code) verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code)
title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL) title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL)
if err != nil { if err != nil {
return "", err return err
} }
go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString()) go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
return code, nil return nil
} }
// UseRePassword // UseRePassword

View File

@ -35,6 +35,17 @@ func Markdown2HTML(source string) string {
return buf.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 { type DangerousHTMLFilterExtension struct {
} }

View File

@ -6,6 +6,21 @@ export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';
export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';
export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_'; 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 = { export const ADMIN_LIST_STATUS = {
// normal; // normal;

View File

@ -5,7 +5,6 @@ import { Link } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { unionBy } from 'lodash'; import { unionBy } from 'lodash';
import { marked } from 'marked';
import * as Types from '@/common/interface'; import * as Types from '@/common/interface';
import { Modal } from '@/components'; import { Modal } from '@/components';
@ -108,15 +107,11 @@ const Comment = ({ objectId, mode, commentId }) => {
const users = matchedUsers(item.value); const users = matchedUsers(item.value);
const userNames = unionBy(users.map((user) => user.userName)); const userNames = unionBy(users.map((user) => user.userName));
const commentMarkDown = parseUserInfo(item.value); const commentMarkDown = parseUserInfo(item.value);
const html = marked.parse(commentMarkDown);
// if (!commentMarkDown || !html) {
// return;
// }
const params = { const params = {
object_id: objectId, object_id: objectId,
original_text: commentMarkDown, original_text: commentMarkDown,
mention_username_list: userNames, mention_username_list: userNames,
parsed_text: html,
...(item.type === 'reply' ...(item.type === 'reply'
? { ? {
reply_comment_id: item.comment_id, reply_comment_id: item.comment_id,
@ -128,13 +123,13 @@ const Comment = ({ objectId, mode, commentId }) => {
return updateComment({ return updateComment({
...params, ...params,
comment_id: item.comment_id, comment_id: item.comment_id,
}).then(() => { }).then((res) => {
setComments( setComments(
comments.map((comment) => { comments.map((comment) => {
if (comment.comment_id === item.comment_id) { if (comment.comment_id === item.comment_id) {
comment.showEdit = false; comment.showEdit = false;
comment.parsed_text = html; comment.parsed_text = res.parsed_text;
comment.original_text = item.value; comment.original_text = res.original_text;
} }
return comment; return comment;
}), }),

View File

@ -69,7 +69,10 @@ const Index: FC<Props> = ({
{objectType !== 'answer' && opts?.showTitle && ( {objectType !== 'answer' && opts?.showTitle && (
<h5 <h5
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: diffText(newData.title, oldData?.title), __html: diffText(
newData.title?.replace(/</gi, '&lt;'),
oldData?.title?.replace(/</gi, '&lt;'),
),
}} }}
className="mb-3" className="mb-3"
/> />

View File

@ -114,19 +114,8 @@ export function htmlRender(el: HTMLElement | null) {
}, },
); );
el.querySelectorAll('table').forEach((table) => { // remove change table style to htmlToReact function
if ( /**
(table.parentNode as HTMLDivElement)?.classList.contains( * @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.
'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);
});
} }

View File

@ -71,6 +71,10 @@ const Header: FC = () => {
}; };
const onLoginClick = (evt) => { const onLoginClick = (evt) => {
evt.preventDefault(); evt.preventDefault();
if (location.pathname === '/users/login') {
window.location.reload();
return;
}
floppyNavigation.navigateToLogin((loginPath) => { floppyNavigation.navigateToLogin((loginPath) => {
navigate(loginPath, { replace: true }); navigate(loginPath, { replace: true });
}); });

View File

@ -238,7 +238,6 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
const errors = requiredValidator(); const errors = requiredValidator();
if (errors.length > 0) { if (errors.length > 0) {
formData = errors.reduce((acc, cur) => { formData = errors.reduce((acc, cur) => {
console.log('schema.properties[cur]', cur);
acc[cur] = { acc[cur] = {
...formData[cur], ...formData[cur],
isInvalid: true, isInvalid: true,

View File

@ -10,6 +10,7 @@ import useChangePasswordModal from './useChangePasswordModal';
import usePageTags from './usePageTags'; import usePageTags from './usePageTags';
import useLoginRedirect from './useLoginRedirect'; import useLoginRedirect from './useLoginRedirect';
import usePromptWithUnload from './usePrompt'; import usePromptWithUnload from './usePrompt';
import useImgViewer from './useImgViewer';
export { export {
useTagModal, useTagModal,
@ -24,4 +25,5 @@ export {
usePageTags, usePageTags,
useLoginRedirect, useLoginRedirect,
usePromptWithUnload, usePromptWithUnload,
useImgViewer,
}; };

View File

@ -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<HTMLElement>) => {
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(
<Modal
show={visible}
fullscreen
centered
scrollable
contentClassName="bg-transparent"
onHide={onClose}>
<Modal.Body onClick={onClose} className="p-0 d-flex">
<img
className="cursor-zoom-out img-fluid m-auto"
src={imgSrc}
alt={imgSrc}
/>
</Modal.Body>
</Modal>,
);
});
useEffect(() => {
onClose();
}, [location]);
return {
onClose,
checkClickForImgView,
};
};
export default useImgViewer;

View File

@ -6,7 +6,7 @@ import {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
// https://gist.github.com/chaance/2f3c14ec2351a175024f62fd6ba64aa6 // 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 { interface PromptProps {
when: boolean; when: boolean;
beforeUnload?: boolean; beforeUnload?: boolean;

View File

@ -47,7 +47,11 @@ const useReportModal = (callback?: () => void) => {
setShow(true); setShow(true);
}); });
}; };
const asyncCallback = () => {
setTimeout(() => {
callback?.();
});
};
const handleRadio = (val) => { const handleRadio = (val) => {
setInvalidState(false); setInvalidState(false);
setContent({ setContent({
@ -93,8 +97,8 @@ const useReportModal = (callback?: () => void) => {
close_type: reportType.type, close_type: reportType.type,
close_msg: content.value, close_msg: content.value,
}).then(() => { }).then(() => {
callback?.();
onClose(); onClose();
asyncCallback();
}); });
return; return;
} }
@ -109,8 +113,8 @@ const useReportModal = (callback?: () => void) => {
msg: t('flag_success', { keyPrefix: 'toast' }), msg: t('flag_success', { keyPrefix: 'toast' }),
variant: 'warning', variant: 'warning',
}); });
callback?.();
onClose(); onClose();
asyncCallback();
}); });
} }
@ -121,8 +125,8 @@ const useReportModal = (callback?: () => void) => {
flagged_type: reportType.type, flagged_type: reportType.type,
id: params.id, id: params.id,
}).then(() => { }).then(() => {
callback?.();
onClose(); onClose();
asyncCallback();
}); });
} }
}; };

View File

@ -120,6 +120,14 @@ a {
cursor: pointer; cursor: pointer;
} }
.cursor-zoom-out {
cursor: zoom-out !important;
}
img:not(a img, img.broken) {
cursor: zoom-in;
}
.resize-none { .resize-none {
resize: none; resize: none;
} }

View File

@ -1,17 +1,30 @@
import { useEffect } from 'react';
import { Container, Button } from 'react-bootstrap'; import { Container, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const Index = () => { const Index = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_404' }); 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 ( return (
<Container className="d-flex flex-column justify-content-center align-items-center page-wrap"> <Container
className="d-flex flex-column justify-content-center align-items-center"
style={{ flex: 1 }}>
<div <div
className="mb-4 text-secondary" className="mb-4 text-secondary"
style={{ fontSize: '120px', lineHeight: 1.2 }}> style={{ fontSize: '120px', lineHeight: 1.2 }}>
(=x=) (=x=)
</div> </div>
<div className="text-center mb-4">{t('desc')}</div> <h4 className="text-center">{t('http_error')}</h4>
<div className="text-center mb-3 fs-5">{t('desc')}</div>
<div className="text-center"> <div className="text-center">
<Button as={Link} to="/" variant="link"> <Button as={Link} to="/" variant="link">
{t('back_home')} {t('back_home')}

View File

@ -1,9 +1,19 @@
import { useEffect } from 'react';
import { Container, Button } from 'react-bootstrap'; import { Container, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const Index = () => { const Index = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_50X' }); const { t } = useTranslation('translation', { keyPrefix: 'page_50X' });
useEffect(() => {
// auto height of container
const pageWrap = document.querySelector('.page-wrap');
pageWrap.style.display = 'contents';
return () => {
pageWrap.style.display = 'block';
};
}, []);
return ( return (
<Container className="d-flex flex-column justify-content-center align-items-center page-wrap"> <Container className="d-flex flex-column justify-content-center align-items-center page-wrap">
<div <div
@ -11,7 +21,9 @@ const Index = () => {
style={{ fontSize: '120px', lineHeight: 1.2 }}> style={{ fontSize: '120px', lineHeight: 1.2 }}>
(=T^T=) (=T^T=)
</div> </div>
<div className="text-center mb-3">{t('desc')}</div>
<h4 className="text-center">{t('http_error')}</h4>
<div className="text-center mb-3 fs-5">{t('desc')}</div>
<div className="text-center"> <div className="text-center">
<Button as={Link} to="/" variant="link"> <Button as={Link} to="/" variant="link">
{t('back_home')} {t('back_home')}

View File

@ -3,6 +3,7 @@ import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { FormDataType } from '@/common/interface'; import type { FormDataType } from '@/common/interface';
import Pattern from '@/common/pattern';
import Progress from '../Progress'; import Progress from '../Progress';
interface Props { interface Props {
@ -54,8 +55,7 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
}; };
} }
const mailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/; if (contact_email.value && !Pattern.email.test(contact_email.value)) {
if (contact_email.value && !contact_email.value.match(mailReg)) {
bol = false; bol = false;
data.contact_email = { data.contact_email = {
value: contact_email.value, value: contact_email.value,
@ -98,7 +98,7 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
}; };
} }
if (email.value && !email.value.match(mailReg)) { if (email.value && !Pattern.email.test(email.value)) {
bol = false; bol = false;
data.email = { data.email = {
value: email.value, value: email.value,

View File

@ -1,10 +1,10 @@
import { FC, memo } from 'react'; import { FC, memo, useEffect } from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet, useLocation } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import { SWRConfig } from 'swr'; import { SWRConfig } from 'swr';
import { toastStore, loginToContinueStore } from '@/stores'; import { toastStore, loginToContinueStore, notFoundStore } from '@/stores';
import { import {
Header, Header,
Footer, Footer,
@ -14,13 +14,23 @@ import {
PageTags, PageTags,
} from '@/components'; } from '@/components';
import { LoginToContinueModal } from '@/components/Modal'; import { LoginToContinueModal } from '@/components/Modal';
import { useImgViewer } from '@/hooks';
import Component404 from '@/pages/404';
const Layout: FC = () => { const Layout: FC = () => {
const location = useLocation();
const { msg: toastMsg, variant, clear: toastClear } = toastStore(); const { msg: toastMsg, variant, clear: toastClear } = toastStore();
const closeToast = () => { const closeToast = () => {
toastClear(); toastClear();
}; };
const { visible: show404, hide: notFoundHide } = notFoundStore();
const imgViewer = useImgViewer();
const { show: showLoginToContinueModal } = loginToContinueStore(); const { show: showLoginToContinueModal } = loginToContinueStore();
useEffect(() => {
notFoundHide();
}, [location]);
return ( return (
<HelmetProvider> <HelmetProvider>
<PageTags /> <PageTags />
@ -30,8 +40,11 @@ const Layout: FC = () => {
revalidateOnFocus: false, revalidateOnFocus: false,
}}> }}>
<Header /> <Header />
<div className="position-relative page-wrap"> {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
<Outlet /> <div
className="position-relative page-wrap"
onClick={imgViewer.checkClickForImgView}>
{show404 ? <Component404 /> : <Outlet />}
</div> </div>
<Toast msg={toastMsg} variant={variant} onClose={closeToast} /> <Toast msg={toastMsg} variant={variant} onClose={closeToast} />
<Footer /> <Footer />

View File

@ -10,6 +10,7 @@ import { isEqual } from 'lodash';
import { usePageTags, usePromptWithUnload } from '@/hooks'; import { usePageTags, usePromptWithUnload } from '@/hooks';
import { Editor, EditorRef, TagSelector } from '@/components'; import { Editor, EditorRef, TagSelector } from '@/components';
import type * as Type from '@/common/interface'; import type * as Type from '@/common/interface';
import { DRAFT_QUESTION_STORAGE_KEY } from '@/common/constants';
import { import {
saveQuestion, saveQuestion,
questionDetail, questionDetail,
@ -19,7 +20,7 @@ import {
useQueryQuestionByTitle, useQueryQuestionByTitle,
getTagsBySlugName, getTagsBySlugName,
} from '@/services'; } from '@/services';
import { handleFormError } from '@/utils'; import { handleFormError, SaveDraft, storageExpires } from '@/utils';
import { pathFactory } from '@/router/pathFactory'; import { pathFactory } from '@/router/pathFactory';
import SearchQuestion from './components/SearchQuestion'; import SearchQuestion from './components/SearchQuestion';
@ -32,6 +33,8 @@ interface FormDataItem {
edit_summary: Type.FormValue<string>; edit_summary: Type.FormValue<string>;
} }
const saveDraft = new SaveDraft({ type: 'question' });
const Ask = () => { const Ask = () => {
const initFormData = { const initFormData = {
title: { title: {
@ -66,6 +69,7 @@ const Ask = () => {
const [checked, setCheckState] = useState(false); const [checked, setCheckState] = useState(false);
const [contentChanged, setContentChanged] = useState(false); const [contentChanged, setContentChanged] = useState(false);
const [focusType, setForceType] = useState(''); const [focusType, setForceType] = useState('');
const [hasDraft, setHasDraft] = useState(false);
const resetForm = () => { const resetForm = () => {
setFormData(initFormData); setFormData(initFormData);
setCheckState(false); setCheckState(false);
@ -98,6 +102,34 @@ const Ask = () => {
isEdit ? '' : formData.title.value, isEdit ? '' : formData.title.value,
); );
const removeDraft = () => {
saveDraft.save.cancel();
saveDraft.remove();
setHasDraft(false);
};
useEffect(() => {
if (!qid) {
initQueryTags();
const draft = storageExpires.get(DRAFT_QUESTION_STORAGE_KEY);
if (draft) {
formData.title.value = draft.title;
formData.content.value = draft.content;
formData.tags.value = draft.tags;
formData.answer.value = draft.answer;
setCheckState(Boolean(draft.answer));
setHasDraft(true);
setFormData({ ...formData });
} else {
resetForm();
}
}
return () => {
resetForm();
};
}, [qid]);
useEffect(() => { useEffect(() => {
const { title, tags, content, answer } = formData; const { title, tags, content, answer } = formData;
const { title: editTitle, tags: editTags, content: editContent } = immData; const { title: editTitle, tags: editTags, content: editContent } = immData;
@ -118,11 +150,21 @@ const Ask = () => {
} }
return; return;
} }
// write // write
if (title.value || tags.value.length > 0 || content.value || answer.value) { if (title.value || tags.value.length > 0 || content.value || answer.value) {
// save draft
saveDraft.save({
params: {
title: title.value,
tags: tags.value,
content: content.value,
answer: answer.value,
},
callback: () => setHasDraft(true),
});
setContentChanged(true); setContentChanged(true);
} else { } else {
removeDraft();
setContentChanged(false); setContentChanged(false);
} }
}, [formData]); }, [formData]);
@ -131,12 +173,6 @@ const Ask = () => {
when: contentChanged, when: contentChanged,
}); });
useEffect(() => {
if (!isEdit) {
resetForm();
initQueryTags();
}
}, [isEdit]);
const { data: revisions = [] } = useQueryRevisions(qid); const { data: revisions = [] } = useQueryRevisions(qid);
useEffect(() => { useEffect(() => {
@ -191,6 +227,14 @@ const Ask = () => {
}, },
}); });
const deleteDraft = () => {
const res = window.confirm(t('discard_confirm', { keyPrefix: 'draft' }));
if (res) {
removeDraft();
resetForm();
}
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
setContentChanged(false); setContentChanged(false);
event.preventDefault(); event.preventDefault();
@ -248,6 +292,7 @@ const Ask = () => {
navigate(pathFactory.questionLanding(id)); navigate(pathFactory.questionLanding(id));
} }
} }
removeDraft();
} }
}; };
const backPage = () => { const backPage = () => {
@ -376,10 +421,17 @@ const Ask = () => {
<Button type="submit" className="me-2"> <Button type="submit" className="me-2">
{isEdit ? t('btn_save_edits') : t('btn_post_question')} {isEdit ? t('btn_save_edits') : t('btn_post_question')}
</Button> </Button>
{isEdit && (
<Button variant="link" onClick={backPage}> <Button variant="link" onClick={backPage}>
{t('cancel', { keyPrefix: 'btns' })} {t('cancel', { keyPrefix: 'btns' })}
</Button> </Button>
)}
{hasDraft && (
<Button variant="link" onClick={deleteDraft}>
{t('discard_draft', { keyPrefix: 'btns' })}
</Button>
)}
</div> </div>
)} )}
{!isEdit && ( {!isEdit && (
@ -411,7 +463,6 @@ const Ask = () => {
}} }}
/> />
<Form.Control <Form.Control
value={formData.answer.value}
type="text" type="text"
isInvalid={formData.answer.isInvalid} isInvalid={formData.answer.isInvalid}
hidden hidden
@ -424,10 +475,15 @@ const Ask = () => {
</> </>
)} )}
{checked && ( {checked && (
<Button type="submit" className="mt-3"> <div className="mt-3">
{t('post_question&answer')} <Button type="submit">{t('post_question&answer')}</Button>
{hasDraft && (
<Button variant="link" className="ms-2" onClick={deleteDraft}>
{t('discard_draft', { keyPrefix: 'btns' })}
</Button> </Button>
)} )}
</div>
)}
</Form> </Form>
</Col> </Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">

View File

@ -10,38 +10,38 @@ interface Props {
const Index: FC<Props> = ({ data }) => { const Index: FC<Props> = ({ data }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Alert className="mb-4" variant="info"> <Alert className="mb-4" variant={data.level}>
{data.level === 'info' ? (
<div> <div>
{data.operation_msg.indexOf('http') > -1 ? ( {data.msg.indexOf('http') > -1 ? (
<p> <p>
{data.operation_description}{' '} {data.description}{' '}
<a href={data.operation_msg} style={{ color: '#055160' }}> <a href={data.msg} style={{ color: '#055160' }}>
<strong>{t('question_detail.show_exist')}</strong> <strong>{t('question_detail.show_exist')}</strong>
</a> </a>
</p> </p>
) : ( ) : (
<p> <p>{data.msg ? data.msg : data.description}</p>
{data.operation_msg
? data.operation_msg
: data.operation_description}
</p>
)} )}
<div className="fs-14"> <div className="fs-14">
{t('question_detail.closed_in')}{' '} {t('question_detail.closed_in')}{' '}
<time <time
dateTime={dayjs.unix(data.operation_time).tz().toISOString()} dateTime={dayjs.unix(data.time).tz().toISOString()}
title={dayjs title={dayjs
.unix(data.operation_time) .unix(data.time)
.tz() .tz()
.format(t('dates.long_date_with_time'))}> .format(t('dates.long_date_with_time'))}>
{dayjs {dayjs
.unix(data.operation_time) .unix(data.time)
.tz() .tz()
.format(t('dates.long_date_with_year'))} .format(t('dates.long_date_with_year'))}
</time> </time>
. .
</div> </div>
</div> </div>
) : (
data.msg
)}
</Alert> </Alert>
); );
}; };

View File

@ -1,5 +1,5 @@
import { memo, FC, useEffect, useRef } from 'react'; import { memo, FC, useEffect, useRef } from 'react';
import { Button } from 'react-bootstrap'; import { Button, Alert } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
@ -72,6 +72,11 @@ const Index: FC<Props> = ({
} }
return ( return (
<div id={data.id} ref={answerRef} className="answer-item py-4"> <div id={data.id} ref={answerRef} className="answer-item py-4">
{data.status === 10 && (
<Alert variant="danger" className="mb-4">
{t('post_deleted', { keyPrefix: 'messages' })}
</Alert>
)}
<article <article
dangerouslySetInnerHTML={{ __html: data?.html }} dangerouslySetInnerHTML={{ __html: data?.html }}
className="fmt text-break text-wrap" className="fmt text-break text-wrap"

View File

@ -1,4 +1,4 @@
import { memo, useState, FC } from 'react'; import { memo, useState, FC, useEffect } from 'react';
import { Form, Button } from 'react-bootstrap'; import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -9,7 +9,8 @@ import { usePromptWithUnload } from '@/hooks';
import { Editor, Modal, TextArea } from '@/components'; import { Editor, Modal, TextArea } from '@/components';
import { FormDataType } from '@/common/interface'; import { FormDataType } from '@/common/interface';
import { postAnswer } from '@/services'; import { postAnswer } from '@/services';
import { guard, handleFormError } from '@/utils'; import { guard, handleFormError, SaveDraft, storageExpires } from '@/utils';
import { DRAFT_ANSWER_STORAGE_KEY } from '@/common/constants';
interface Props { interface Props {
visible?: boolean; visible?: boolean;
@ -21,6 +22,8 @@ interface Props {
callback?: (obj) => void; callback?: (obj) => void;
} }
const saveDraft = new SaveDraft({ type: 'answer' });
const Index: FC<Props> = ({ visible = false, data, callback }) => { const Index: FC<Props> = ({ visible = false, data, callback }) => {
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
keyPrefix: 'question_detail.write_answer', keyPrefix: 'question_detail.write_answer',
@ -35,11 +38,51 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
const [showEditor, setShowEditor] = useState<boolean>(visible); const [showEditor, setShowEditor] = useState<boolean>(visible);
const [focusType, setFocusType] = useState(''); const [focusType, setFocusType] = useState('');
const [editorFocusState, setEditorFocusState] = useState(false); const [editorFocusState, setEditorFocusState] = useState(false);
const [hasDraft, setHasDraft] = useState(false);
usePromptWithUnload({ usePromptWithUnload({
when: Boolean(formData.content.value), when: Boolean(formData.content.value),
}); });
const removeDraft = () => {
// immediately remove debounced save
saveDraft.save.cancel();
saveDraft.remove();
setHasDraft(false);
};
useEffect(() => {
const draft = storageExpires.get(DRAFT_ANSWER_STORAGE_KEY);
if (draft?.questionId === data.qid && draft?.content) {
setFormData({
content: {
value: draft.content,
isInvalid: false,
errorMsg: '',
},
});
setShowEditor(true);
setHasDraft(true);
}
}, []);
useEffect(() => {
const draft = storageExpires.get(DRAFT_ANSWER_STORAGE_KEY);
const { content } = formData;
if (content.value) {
// save Draft
saveDraft.save({
questionId: data?.qid,
content: content.value,
});
setHasDraft(true);
} else if (draft?.questionId === data.qid && !content.value) {
removeDraft();
}
}, [formData.content.value]);
const checkValidated = (): boolean => { const checkValidated = (): boolean => {
let bol = true; let bol = true;
const { content } = formData; const { content } = formData;
@ -65,6 +108,24 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
return bol; return bol;
}; };
const resetForm = () => {
setFormData({
content: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
};
const deleteDraft = () => {
const res = window.confirm(t('discard_confirm', { keyPrefix: 'draft' }));
if (res) {
removeDraft();
resetForm();
}
};
const handleSubmit = () => { const handleSubmit = () => {
if (!guard.tryNormalLogged(true)) { if (!guard.tryNormalLogged(true)) {
return; return;
@ -86,6 +147,7 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
errorMsg: '', errorMsg: '',
}, },
}); });
removeDraft();
callback?.(res.info); callback?.(res.info);
}) })
.catch((ex) => { .catch((ex) => {
@ -128,7 +190,6 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
setShowEditor(true); setShowEditor(true);
setEditorFocusState(true); setEditorFocusState(true);
}; };
return ( return (
<Form noValidate className="mt-4"> <Form noValidate className="mt-4">
{(!data.answered || showEditor) && ( {(!data.answered || showEditor) && (
@ -187,6 +248,11 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
) : ( ) : (
<Button onClick={clickBtn}>{t('btn_name')}</Button> <Button onClick={clickBtn}>{t('btn_name')}</Button>
)} )}
{hasDraft && (
<Button variant="link" className="ms-2" onClick={deleteDraft}>
{t('discard_draft', { keyPrefix: 'btns' })}
</Button>
)}
</Form> </Form>
); );
}; };

View File

@ -56,6 +56,7 @@ const Index = () => {
const { setUsers } = usePageUsers(); const { setUsers } = usePageUsers();
const userInfo = loggedUserInfoStore((state) => state.user); const userInfo = loggedUserInfoStore((state) => state.user);
const isAuthor = userInfo?.username === question?.user_info?.username; const isAuthor = userInfo?.username === question?.user_info?.username;
const isAdmin = userInfo?.is_admin;
const isLogged = Boolean(userInfo?.access_token); const isLogged = Boolean(userInfo?.access_token);
const { state: locationState } = useLocation(); const { state: locationState } = useLocation();
@ -76,7 +77,22 @@ const Index = () => {
page_size: 999, page_size: 999,
}); });
if (res) { if (res) {
setAnswers(res); res.list = res.list?.filter((v) => {
// delete answers pnly show to author and admin and has searchparams aid
if (v.status === 10) {
if (
(v?.user_info.username === userInfo?.username || isAdmin) &&
aid === v.id
) {
return v;
}
return null;
}
return v;
});
setAnswers({ ...res, count: res.list.length });
if (page > 0 || order) { if (page > 0 || order) {
// scroll into view; // scroll into view;
const element = document.getElementById('answerHeader'); const element = document.getElementById('answerHeader');
@ -183,9 +199,7 @@ const Index = () => {
<Container className="pt-4 mt-2 mb-5 questionDetailPage"> <Container className="pt-4 mt-2 mb-5 questionDetailPage">
<Row className="justify-content-center"> <Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0"> <Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0">
{question?.operation?.operation_type && ( {question?.operation?.level && <Alert data={question.operation} />}
<Alert data={question.operation} />
)}
{isLoading ? ( {isLoading ? (
<ContentLoader /> <ContentLoader />
) : ( ) : (

View File

@ -18,7 +18,7 @@ const Index: FC<Props> = ({ visible, introduction, data }) => {
<h5 className="mb-3">{t('about_me')}</h5> <h5 className="mb-3">{t('about_me')}</h5>
{introduction ? ( {introduction ? (
<div <div
className="mb-4 text-break" className="mb-4 text-break fmt"
dangerouslySetInnerHTML={{ __html: introduction }} dangerouslySetInnerHTML={{ __html: introduction }}
/> />
) : ( ) : (

View File

@ -11,6 +11,7 @@ import {
usePersonalTop, usePersonalTop,
usePersonalListByTabName, usePersonalListByTabName,
} from '@/services'; } from '@/services';
import type { UserInfoRes } from '@/common/interface';
import { import {
UserInfo, UserInfo,
@ -47,8 +48,8 @@ const Personal: FC = () => {
tabName, tabName,
); );
let pageTitle = ''; let pageTitle = '';
if (userInfo) { if (userInfo?.username) {
pageTitle = `${userInfo.info.display_name} (${userInfo.info.username})`; pageTitle = `${userInfo?.display_name} (${userInfo?.username})`;
} }
const { count = 0, list = [] } = listData?.[tabName] || {}; const { count = 0, list = [] } = listData?.[tabName] || {};
usePageTags({ usePageTags({
@ -57,11 +58,11 @@ const Personal: FC = () => {
return ( return (
<Container className="pt-4 mt-2 mb-5"> <Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center"> <Row className="justify-content-center">
{userInfo?.info?.status !== 'normal' && userInfo?.info?.status_msg && ( {userInfo?.status !== 'normal' && userInfo?.status_msg && (
<Alert data={userInfo?.info.status_msg} /> <Alert data={userInfo?.status_msg} />
)} )}
<Col xxl={7} lg={8} sm={12}> <Col xxl={7} lg={8} sm={12}>
<UserInfo data={userInfo?.info} /> <UserInfo data={userInfo as UserInfoRes} />
</Col> </Col>
<Col <Col
xxl={3} xxl={3}
@ -88,11 +89,11 @@ const Personal: FC = () => {
<Col xxl={7} lg={8} sm={12}> <Col xxl={7} lg={8} sm={12}>
<Overview <Overview
visible={tabName === 'overview'} visible={tabName === 'overview'}
introduction={userInfo?.info?.bio_html} introduction={userInfo?.bio_html || ''}
data={topData} data={topData}
/> />
<ListHead <ListHead
count={tabName === 'reputation' ? userInfo?.info?.rank : count} count={tabName === 'reputation' ? Number(userInfo?.rank) : count}
sort={order} sort={order}
visible={tabName !== 'overview'} visible={tabName !== 'overview'}
tabName={tabName} tabName={tabName}
@ -120,17 +121,14 @@ const Personal: FC = () => {
</Col> </Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<h5 className="mb-3">{t('stats')}</h5> <h5 className="mb-3">{t('stats')}</h5>
{userInfo?.info && ( {userInfo?.created_at && (
<> <>
<div className="text-secondary"> <div className="text-secondary">
<FormatTime <FormatTime time={userInfo.created_at} preFix={t('joined')} />
time={userInfo.info.created_at}
preFix={t('joined')}
/>
</div> </div>
<div className="text-secondary"> <div className="text-secondary">
<FormatTime <FormatTime
time={userInfo.info.last_login_date} time={userInfo.last_login_date}
preFix={t('last_login')} preFix={t('last_login')}
/> />
</div> </div>

View File

@ -0,0 +1,8 @@
import Error50X from '@/pages/50X';
// import Page404 from '@/pages/404';
const Index = () => {
return <Error50X />;
};
export default Index;

View File

@ -2,9 +2,10 @@ import { Suspense, lazy } from 'react';
import { RouteObject } from 'react-router-dom'; import { RouteObject } from 'react-router-dom';
import Layout from '@/pages/Layout'; import Layout from '@/pages/Layout';
import ErrorBoundary from '@/pages/50X';
import baseRoutes, { RouteNode } from '@/router/routes'; import baseRoutes, { RouteNode } from './routes';
import RouteGuard from '@/router/RouteGuard'; import RouteGuard from './RouteGuard';
import RouteErrorBoundary from './RouteErrorBoundary';
const routes: RouteNode[] = []; const routes: RouteNode[] = [];
@ -18,7 +19,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {
) : ( ) : (
<Layout /> <Layout />
); );
rn.errorElement = <ErrorBoundary />; rn.errorElement = <RouteErrorBoundary />;
} else { } else {
/** /**
* cannot use a fully dynamic import statement * cannot use a fully dynamic import statement
@ -37,6 +38,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {
)} )}
</Suspense> </Suspense>
); );
rn.errorElement = <RouteErrorBoundary />;
} }
root.push(rn); root.push(rn);
const children = Array.isArray(rn.children) ? rn.children : null; const children = Array.isArray(rn.children) ? rn.children : null;

View File

@ -8,7 +8,10 @@ export const usePersonalInfoByName = (username: string) => {
const apiUrl = '/answer/api/v1/personal/user/info'; const apiUrl = '/answer/api/v1/personal/user/info';
const { data, error, mutate } = useSWR<Type.UserInfoRes, Error>( const { data, error, mutate } = useSWR<Type.UserInfoRes, Error>(
username ? `${apiUrl}?username=${username}` : null, username ? `${apiUrl}?username=${username}` : null,
request.instance.get, (url) =>
request.get(url, {
allow404: true,
}),
); );
return { return {
data, data,

View File

@ -47,7 +47,9 @@ export const useTagInfo = ({ id = '', name = '' }) => {
name = encodeURIComponent(name); name = encodeURIComponent(name);
apiUrl = `/answer/api/v1/tag?name=${name}`; apiUrl = `/answer/api/v1/tag?name=${name}`;
} }
const { data, error } = useSWR<Type.TagInfo>(apiUrl, request.instance.get); const { data, error } = useSWR<Type.TagInfo>(apiUrl, (url) =>
request.get(url, { allow404: true }),
);
return { return {
data, data,
isLoading: !data && !error, isLoading: !data && !error,

View File

@ -171,6 +171,7 @@ export const saveQuestion = (params: Type.QuestionParams) => {
export const questionDetail = (id: string) => { export const questionDetail = (id: string) => {
return request.get<Type.QuestionDetailRes>( return request.get<Type.QuestionDetailRes>(
`/answer/api/v1/question/info?id=${id}`, `/answer/api/v1/question/info?id=${id}`,
{ allow404: true },
); );
}; };

View File

@ -10,6 +10,7 @@ import pageTagStore from './pageTags';
import customizeStore from './customize'; import customizeStore from './customize';
import themeSettingStore from './themeSetting'; import themeSettingStore from './themeSetting';
import loginToContinueStore from './loginToContinue'; import loginToContinueStore from './loginToContinue';
import notFoundStore from './notFound';
export { export {
toastStore, toastStore,
@ -23,4 +24,5 @@ export {
themeSettingStore, themeSettingStore,
seoSettingStore, seoSettingStore,
loginToContinueStore, loginToContinueStore,
notFoundStore,
}; };

23
ui/src/stores/notFound.ts Normal file
View File

@ -0,0 +1,23 @@
import create from 'zustand';
interface NotFoundType {
visible: boolean;
show: () => void;
hide: () => void;
}
const notFound = create<NotFoundType>((set) => ({
visible: false,
show: () => {
set(() => {
return { visible: true };
});
},
hide: () => {
set(() => {
return { visible: false };
});
},
}));
export default notFound;

View File

@ -237,7 +237,26 @@ function htmlToReact(html: string) {
const cleanedHtml = DOMPurify.sanitize(html, { const cleanedHtml = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true }, USE_PROFILES: { html: true },
}); });
return parse(cleanedHtml);
const ele = document.createElement('div');
ele.innerHTML = cleanedHtml;
ele.querySelectorAll('table').forEach((table) => {
if (
(!table || (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);
});
return parse(ele.innerHTML);
} }
export { export {

View File

@ -63,7 +63,7 @@ export const deriveLoginState = (): TLoginState => {
return ls; return ls;
}; };
const isIgnoredPath = (ignoredPath: string | string[]) => { export const isIgnoredPath = (ignoredPath: string | string[]) => {
if (!Array.isArray(ignoredPath)) { if (!Array.isArray(ignoredPath)) {
ignoredPath = [ignoredPath]; ignoredPath = [ignoredPath];
} }

View File

@ -1,6 +1,8 @@
export { default as request } from './request'; export { default as request } from './request';
export { default as Storage } from './storage'; export { default as Storage } from './storage';
export { floppyNavigation } from './floppyNavigation'; export { floppyNavigation } from './floppyNavigation';
export { default as storageExpires } from './storageWithExpires';
export { default as SaveDraft } from './saveDraft';
export * from './common'; export * from './common';
export * from './color'; export * from './color';

View File

@ -2,19 +2,24 @@ import axios, { AxiosResponse } from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
import { Modal } from '@/components'; import { Modal } from '@/components';
import { loggedUserInfoStore, toastStore } from '@/stores'; import { loggedUserInfoStore, toastStore, notFoundStore } from '@/stores';
import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants'; import { LOGGED_TOKEN_STORAGE_KEY, IGNORE_PATH_LIST } from '@/common/constants';
import { RouteAlias } from '@/router/alias'; import { RouteAlias } from '@/router/alias';
import { getCurrentLang } from '@/utils/localize'; import { getCurrentLang } from '@/utils/localize';
import Storage from './storage'; import Storage from './storage';
import { floppyNavigation } from './floppyNavigation'; import { floppyNavigation } from './floppyNavigation';
import { isIgnoredPath } from './guard';
const baseConfig = { const baseConfig = {
timeout: 10000, timeout: 10000,
withCredentials: true, withCredentials: true,
}; };
interface APIconfig extends AxiosRequestConfig {
allow404: boolean;
}
class Request { class Request {
instance: AxiosInstance; instance: AxiosInstance;
@ -49,6 +54,9 @@ class Request {
(error) => { (error) => {
const { status, data: respData } = error.response || {}; const { status, data: respData } = error.response || {};
const { data = {}, msg = '', reason = '' } = respData || {}; const { data = {}, msg = '', reason = '' } = respData || {};
console.log('response error:', error);
if (status === 400) { if (status === 400) {
// show error message // show error message
if (data instanceof Object && data.err_type) { if (data instanceof Object && data.err_type) {
@ -99,12 +107,14 @@ class Request {
// 401: Re-login required // 401: Re-login required
if (status === 401) { if (status === 401) {
// clear userinfo // clear userinfo
notFoundStore.getState().hide();
loggedUserInfoStore.getState().clear(); loggedUserInfoStore.getState().clear();
floppyNavigation.navigateToLogin(); floppyNavigation.navigateToLogin();
return Promise.reject(false); return Promise.reject(false);
} }
if (status === 403) { if (status === 403) {
// Permission interception // Permission interception
notFoundStore.getState().hide();
if (data?.type === 'url_expired') { if (data?.type === 'url_expired') {
// url expired // url expired
floppyNavigation.navigate(RouteAlias.activationFailed, () => { floppyNavigation.navigate(RouteAlias.activationFailed, () => {
@ -135,6 +145,14 @@ class Request {
} }
return Promise.reject(false); return Promise.reject(false);
} }
if (status === 404 && error.config?.allow404) {
if (isIgnoredPath(IGNORE_PATH_LIST)) {
return Promise.reject(false);
}
notFoundStore.getState().show();
return Promise.reject(false);
}
if (status >= 500) { if (status >= 500) {
console.error( console.error(
`Request failed with status code ${status}, ${msg || ''}`, `Request failed with status code ${status}, ${msg || ''}`,
@ -149,7 +167,7 @@ class Request {
return this.instance.request(config); return this.instance.request(config);
} }
public get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> { public get<T = any>(url: string, config?: APIconfig): Promise<T> {
return this.instance.get(url, config); return this.instance.get(url, config);
} }

87
ui/src/utils/saveDraft.ts Normal file
View File

@ -0,0 +1,87 @@
import { debounce } from 'lodash';
import {
DRAFT_QUESTION_STORAGE_KEY,
DRAFT_ANSWER_STORAGE_KEY,
} from '@/common/constants';
import { storageExpires as storage } from '@/utils';
export type QuestionDraft = {
params: {
title: string;
content: string;
tags: any[];
answer: string;
};
callback?: () => void;
};
export type AnswerDraft = {
questionId: string;
content: string;
callback?: () => void;
};
type DraftType = {
type: 'question' | 'answer';
};
export type DraftParams = QuestionDraft | AnswerDraft;
class SaveDraft {
type: DraftType['type'];
status: 'save' | 'remove';
constructor({ type = 'question' }: DraftType) {
this.type = type;
this.status = 'save';
}
save = debounce((data: DraftParams) => {
// TODO
if (this.status === 'remove') {
return;
}
if (this.type === 'question') {
const { params, callback } = data as QuestionDraft;
this.storeDraft(params, callback);
}
if (this.type === 'answer') {
const { content, questionId, callback } = data as AnswerDraft;
if (!questionId || !content) {
return;
}
this.storeDraft({ content, questionId }, callback);
}
}, 3000);
remove() {
this.status = 'remove';
const that = this;
if (this.type === 'question') {
storage.remove(DRAFT_QUESTION_STORAGE_KEY, () => {
that.status = 'save';
});
}
if (this.type === 'answer') {
storage.remove(DRAFT_ANSWER_STORAGE_KEY, () => {
that.status = 'save';
});
}
}
private storeDraft = (params: any, callback) => {
const key =
this.type === 'question'
? DRAFT_QUESTION_STORAGE_KEY
: DRAFT_ANSWER_STORAGE_KEY;
storage.set(key, params);
callback?.();
};
}
export default SaveDraft;

View File

@ -0,0 +1,50 @@
import { DRAFT_TIMESIGH_STORAGE_KEY as timeSign } from '@/common/constants';
const store = {
storage: localStorage || window.localStorage,
set(key: string, value, time?: number): void {
const t = time || Date.now() + 1000 * 60 * 60 * 24 * 7; // default 7 days
try {
this.storage.setItem(key, `${t}${timeSign}${JSON.stringify(value)}`);
} catch {
// ignore
console.error('set storage error: the key is', key);
}
},
get(key: string): any {
const timeSignLen = timeSign.length;
let index = 0;
let time = 0;
let res;
try {
res = this.storage.getItem(key);
} catch {
console.error('get storage error: the key is', key);
}
if (res) {
index = res.indexOf(timeSign);
time = +res.slice(0, index);
if (time > new Date().getTime()) {
res = res.slice(index + timeSignLen);
try {
res = JSON.parse(res);
} catch {
// ignore
}
} else {
// timeout remove storage
res = null;
this.storage.removeItem(key);
}
return res;
}
return res;
},
remove(key: string, callback?: () => void): void {
this.storage.removeItem(key);
callback?.();
},
};
export default store;

View File

@ -3,28 +3,28 @@
<div class="justify-content-center row"> <div class="justify-content-center row">
<div class="col-xxl-7 col-lg-8 col-sm-12"> <div class="col-xxl-7 col-lg-8 col-sm-12">
<div class="d-flex flex-column flex-md-row mb-4"> <div class="d-flex flex-column flex-md-row mb-4">
<a href="/users/{{.userinfo.Info.Username}}"><img <a href="/users/{{.userinfo.Username}}"><img
src="{{.userinfo.Info.Avatar}}" src="{{.userinfo.Avatar}}"
width="160px" height="160px" class="rounded" alt="" /></a> width="160px" height="160px" class="rounded" alt="" /></a>
<div class="ms-0 ms-md-4 mt-4 mt-md-0"> <div class="ms-0 ms-md-4 mt-4 mt-md-0">
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<a class="link-dark h3 mb-0" href="/users/{{.userinfo.Info.Username}}">{{.userinfo.Info.DisplayName}}</a> <a class="link-dark h3 mb-0" href="/users/{{.userinfo.Username}}">{{.userinfo.DisplayName}}</a>
</div> </div>
<div class="text-secondary mb-4">@{{.userinfo.Info.Username}}</div> <div class="text-secondary mb-4">@{{.userinfo.Username}}</div>
<div class="d-flex flex-wrap mb-3"> <div class="d-flex flex-wrap mb-3">
<div class="me-3"> <div class="me-3">
<strong class="fs-5">{{.userinfo.Info.Rank}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_reputation"}}</span> <strong class="fs-5">{{.userinfo.Rank}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_reputation"}}</span>
</div> </div>
<div class="me-3"> <div class="me-3">
<strong class="fs-5">{{.userinfo.Info.AnswerCount}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_answers"}}</span> <strong class="fs-5">{{.userinfo.AnswerCount}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_answers"}}</span>
</div> </div>
<div> <div>
<strong class="fs-5">{{.userinfo.Info.QuestionCount}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_questions"}}</span> <strong class="fs-5">{{.userinfo.QuestionCount}}</strong><span class="text-secondary"> {{translator $.language "ui.personal.x_questions"}}</span>
</div> </div>
</div> </div>
{{if .userinfo.Info.Website }} {{if .userinfo.Website }}
<div class="d-flex align-items-center"><i class="br bi-house-door-fill me-2"></i><a class="link-secondary" href="{{.userinfo.Info.Website}}">{{.userinfo.Info.Website}}</a></div> <div class="d-flex align-items-center"><i class="br bi-house-door-fill me-2"></i><a class="link-secondary" href="{{.userinfo.Website}}">{{.userinfo.Website}}</a></div>
{{else}} {{else}}
{{end}} {{end}}
<div class="d-flex text-secondary"></div> <div class="d-flex text-secondary"></div>