Merge branch 'feat/1.1.2/user-center' of github.com:answerdev/answer into feat/1.1.2/user-center

This commit is contained in:
aichy126 2023-04-23 15:10:52 +08:00
commit bed55985dd
138 changed files with 4009 additions and 1766 deletions

View File

@ -3654,6 +3654,45 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/question/operation": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Operation question \\n operation [pin unpin hide show]",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "Operation question",
"parameters": [
{
"description": "question",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.OperationQuestionReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question/page": {
"get": {
"description": "get questions by page",
@ -7442,6 +7481,21 @@ const docTemplate = `{
}
}
},
"schema.OperationQuestionReq": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"operation": {
"description": "operation [pin unpin hide show]",
"type": "string"
}
}
},
"schema.PermissionMemberAction": {
"type": "object",
"properties": {
@ -7615,6 +7669,14 @@ const docTemplate = `{
"operator": {
"$ref": "#/definitions/schema.QuestionPageRespOperator"
},
"pin": {
"description": "1: unpin, 2: pin",
"type": "integer"
},
"show": {
"description": "0: show, 1: hide",
"type": "integer"
},
"status": {
"type": "integer"
},
@ -7923,10 +7985,6 @@ const docTemplate = `{
"custom_header": {
"type": "string",
"maxLength": 65536
},
"custom_sidebar": {
"type": "string",
"maxLength": 65536
}
}
},
@ -7948,10 +8006,6 @@ const docTemplate = `{
"custom_header": {
"type": "string",
"maxLength": 65536
},
"custom_sidebar": {
"type": "string",
"maxLength": 65536
}
}
},
@ -8053,18 +8107,10 @@ const docTemplate = `{
"schema.SiteInterfaceReq": {
"type": "object",
"required": [
"default_avatar",
"language",
"time_zone"
],
"properties": {
"default_avatar": {
"type": "string",
"enum": [
"system",
"gravatar"
]
},
"language": {
"type": "string",
"maxLength": 128
@ -8078,18 +8124,10 @@ const docTemplate = `{
"schema.SiteInterfaceResp": {
"type": "object",
"required": [
"default_avatar",
"language",
"time_zone"
],
"properties": {
"default_avatar": {
"type": "string",
"enum": [
"system",
"gravatar"
]
},
"language": {
"type": "string",
"maxLength": 128

View File

@ -3642,6 +3642,45 @@
}
}
},
"/answer/api/v1/question/operation": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Operation question \\n operation [pin unpin hide show]",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "Operation question",
"parameters": [
{
"description": "question",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.OperationQuestionReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question/page": {
"get": {
"description": "get questions by page",
@ -7430,6 +7469,21 @@
}
}
},
"schema.OperationQuestionReq": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"operation": {
"description": "operation [pin unpin hide show]",
"type": "string"
}
}
},
"schema.PermissionMemberAction": {
"type": "object",
"properties": {
@ -7603,6 +7657,14 @@
"operator": {
"$ref": "#/definitions/schema.QuestionPageRespOperator"
},
"pin": {
"description": "1: unpin, 2: pin",
"type": "integer"
},
"show": {
"description": "0: show, 1: hide",
"type": "integer"
},
"status": {
"type": "integer"
},
@ -7911,10 +7973,6 @@
"custom_header": {
"type": "string",
"maxLength": 65536
},
"custom_sidebar": {
"type": "string",
"maxLength": 65536
}
}
},
@ -7936,10 +7994,6 @@
"custom_header": {
"type": "string",
"maxLength": 65536
},
"custom_sidebar": {
"type": "string",
"maxLength": 65536
}
}
},
@ -8041,18 +8095,10 @@
"schema.SiteInterfaceReq": {
"type": "object",
"required": [
"default_avatar",
"language",
"time_zone"
],
"properties": {
"default_avatar": {
"type": "string",
"enum": [
"system",
"gravatar"
]
},
"language": {
"type": "string",
"maxLength": 128
@ -8066,18 +8112,10 @@
"schema.SiteInterfaceResp": {
"type": "object",
"required": [
"default_avatar",
"language",
"time_zone"
],
"properties": {
"default_avatar": {
"type": "string",
"enum": [
"system",
"gravatar"
]
},
"language": {
"type": "string",
"maxLength": 128

View File

@ -1128,6 +1128,16 @@ definitions:
description: inbox achievement
type: string
type: object
schema.OperationQuestionReq:
properties:
id:
type: string
operation:
description: operation [pin unpin hide show]
type: string
required:
- id
type: object
schema.PermissionMemberAction:
properties:
action:
@ -1252,6 +1262,12 @@ definitions:
type: string
operator:
$ref: '#/definitions/schema.QuestionPageRespOperator'
pin:
description: '1: unpin, 2: pin'
type: integer
show:
description: '0: show, 1: hide'
type: integer
status:
type: integer
tags:
@ -1466,9 +1482,6 @@ definitions:
custom_header:
maxLength: 65536
type: string
custom_sidebar:
maxLength: 65536
type: string
type: object
schema.SiteCustomCssHTMLResp:
properties:
@ -1484,9 +1497,6 @@ definitions:
custom_header:
maxLength: 65536
type: string
custom_sidebar:
maxLength: 65536
type: string
type: object
schema.SiteGeneralReq:
properties:
@ -1557,11 +1567,6 @@ definitions:
type: object
schema.SiteInterfaceReq:
properties:
default_avatar:
enum:
- system
- gravatar
type: string
language:
maxLength: 128
type: string
@ -1569,17 +1574,11 @@ definitions:
maxLength: 128
type: string
required:
- default_avatar
- language
- time_zone
type: object
schema.SiteInterfaceResp:
properties:
default_avatar:
enum:
- system
- gravatar
type: string
language:
maxLength: 128
type: string
@ -1587,7 +1586,6 @@ definitions:
maxLength: 128
type: string
required:
- default_avatar
- language
- time_zone
type: object
@ -4426,6 +4424,30 @@ paths:
summary: get question details
tags:
- Question
/answer/api/v1/question/operation:
put:
consumes:
- application/json
description: Operation question \n operation [pin unpin hide show]
parameters:
- description: question
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.OperationQuestionReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: Operation question
tags:
- Question
/answer/api/v1/question/page:
get:
consumes:

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -29,7 +37,7 @@ backend:
admin:
other: Gweinyddwr
moderator:
other: Aroglygydd
other: Cymedrolwr
description:
user:
other: Diofyn heb unrhyw fynediad arbennig.
@ -53,7 +61,7 @@ backend:
other: Nid yw e-bost a chyfrinair yn cyfateb.
answer:
not_found:
other: Ateb heb ei ddarganfod.
other: Ni cheir yr ateb.
cannot_deleted:
other: Dim caniatâd i ddileu.
cannot_update:
@ -119,7 +127,7 @@ backend:
other: Heb ganfod yr adroddiad.
tag:
already_exist:
other: Tag yn bodoli eisoes.
other: Mae tag eisoes yn bodoli.
not_found:
other: Tag heb ei ddarganfod.
recommend_tag_not_found:
@ -158,7 +166,7 @@ backend:
username_duplicate:
other: Cymerwyd yr enw defnyddiwr eisoes.
set_avatar:
other: Methodd gwneud avatar.
other: Methodd set avatar.
cannot_update_your_role:
other: Ni allwch addasu eich rôl.
not_allowed_registration:
@ -176,7 +184,7 @@ backend:
other: Methu creu'r ffeil config.yaml.
upload:
unsupported_file_format:
other: Fformat ffeil anghydnaws.
other: Fformat ffeil heb ei gefnogi.
report:
spam:
name:
@ -200,7 +208,7 @@ backend:
other: Postiwyd hwn fel ateb, ond nid yw'n ceisio ateb y cwestiwn. Mae'n bosibl y dylai fod yn olygiad, yn sylwad, yn gwestiwn arall, neu'n cael ei ddileu yn gyfan gwbl.
not_need:
name:
other: nad oes ei angen yn pellach
other: nad oes ei angen mwyach
desc:
other: Mae'r sylw hwn yn hen ffasiwn, yn sgyrsiol neu ddim yn berthnasol i'r post hwn.
other:
@ -268,7 +276,7 @@ ui:
how_to_format:
title: Sut i Fformatio
desc: >-
<ul class="mb-0"><li><p class="mb-2">i wneud cysylltiadau</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">rhoi toriad llinell rhwng paragraffau</p></li><li><p class="mb-2"><em>_italig_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">cod mewnoliad gan 4 bylchau</p></li><li><p class="mb-2">dyfyniad trwy osod <code>&gt;</code> ar ddechrau'r llinell</p></li><li><p class="mb-2">backtic yn dianc<code>`fel_hyn_`</code></p></li><li><p class="mb-2">creu ffensys cod gyda backticks <code>`</code></p><pre class="mb-0"><code>```<br/>cod yma<br/>```</code></pre></li></ul>
<ul class="mb-0"><li><p class="mb-2">i wneud cysylltiadau</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">rhoi dychweliadau rhwng paragraffau</p></li><li><p class="mb-2"><em>_italic_</em> neu **<strong>beiddgar</strong>**</p></li><li><p class="mb-2">cod mewnoliad gan 4 bwlch</p></li><li><p class="mb-2">dyfyniad drwy osod <code>&gt;</code> ar ddechrau'r llinell</p></li><li><p class="mb-2">backtick yn dianc o <code>`like _this_`</code></p></li><li><p class="mb-2">creu ffensys cod gyda thic wrth gefn <code>`</code></p><pre class="mb-0"><code>```<br/>cod yma<br/>```</code></pre></li></ul>
pagination:
prev: Cynt
next: Nesaf
@ -277,7 +285,7 @@ ui:
questions: Cwestiynau
tag: Tag
tags: Tagiau
tag_wiki: wici tag
tag_wiki: tag wiki
create_tag: Creu Tag
edit_tag: Golygu Tag
ask_a_question: Ychwanegu Cwestiwn
@ -312,7 +320,7 @@ ui:
title: Mae'ch Cyfrif wedi'i Atal
until_time: "Cafodd eich cyfrif ei atal tan {{ time }}."
forever: Cafodd y defnyddiwr hwn ei atal am byth.
end: Nid ydych yn bodloni canllaw cymunedol.
end: Nid ydych yn arwain cymunedol.
editor:
blockquote:
text: Dyfyniad
@ -359,7 +367,7 @@ ui:
help:
text: Cymorth
hr:
text: Llinell lorweddol
text: Rheol Llorweddol
image:
text: Delwedd
add_image: Ychwanegu delwedd
@ -759,6 +767,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -824,6 +835,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1385,6 +1397,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -759,6 +767,7 @@ ui:
btn: Frage hinzufügen
answers: antworten
question_detail:
action: Action
Asked: Fragte
asked: fragte
update: Geändert
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Diesen Beitrag erneut öffnen
content: Möchten Sie wirklich wieder öffnen?
success: Dieser Beitrag wurde wieder geöffnet
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Diesen Beitrag löschen
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>Wir empfehlen nicht, <strong>akzeptierte Antworten zu löschen</strong>, da dies zukünftigen Lesern dieses Wissen entzieht. </p> Das wiederholte Löschen akzeptierter Antworten kann dazu führen, dass Ihr Konto für das Antworten gesperrt wird. Möchten Sie wirklich löschen?
other: Sind Sie sicher, dass Sie löschen möchten?
tip_question_deleted: Dieser Beitrag wurde gelöscht
tip_answer_deleted: Diese Antwort wurde gelöscht
btns:
confirm: Bestätigen
@ -824,6 +835,7 @@ ui:
reject: Ablehnen
skip: Überspringen
discard_draft: Entwurf verwerfen
pinned: Pinned
search:
title: Suchergebnisse
keywords: Schlüsselwörter
@ -901,63 +913,63 @@ ui:
question: frage
bookmarks: Lesezeichen
reputation: Ruf
comments: Comments
votes: Votes
newest: Newest
score: Score
edit_profile: Edit Profile
visited_x_days: "Visited {{ count }} days"
viewed: Viewed
joined: Joined
last_login: Seen
about_me: About Me
about_me_empty: "// Hello, World !"
top_answers: Top Answers
top_questions: Top Questions
stats: Stats
list_empty: No posts found.<br />Perhaps you'd like to select a different tab?
accepted: Accepted
answered: answered
asked: asked
upvote: upvote
downvote: downvote
comments: Kommentare
votes: Stimmen
newest: Neueste
score: Punktzahl
edit_profile: Profil bearbeiten
visited_x_days: "{{ count }} Tage besucht"
viewed: Gesehen
joined: Beigetreten
last_login: Gesehen
about_me: Über mich
about_me_empty: "// Hallo Welt !"
top_answers: Top-Antworten
top_questions: Top-Fragen
stats: Statistiken
list_empty: Keine Beiträge gefunden.<br />Vielleicht möchten Sie einen anderen Tab auswählen?
accepted: Akzeptiert
answered: antwortete
asked: fragte
upvote: positiv bewerten
downvote: ablehnen
mod_short: Mod
mod_long: Moderators
x_reputation: reputation
x_votes: votes received
x_answers: answers
x_questions: questions
mod_long: Moderatoren
x_reputation: ruf
x_votes: stimmen erhalten
x_answers: antworten
x_questions: fragen
install:
title: Answer
next: Next
done: Done
config_yaml_error: Can't create the config.yaml file.
title: Antwort
next: Nächste
done: Erledigt
config_yaml_error: Die Datei config.yaml kann nicht erstellt werden.
lang:
label: Please Choose a Language
label: Bitte wählen Sie eine Sprache
db_type:
label: Database Engine
label: Datenbank-Engine
db_username:
label: Username
placeholder: root
msg: Username cannot be empty.
label: Nutzername
placeholder: wurzel
msg: Benutzername darf nicht leer sein.
db_password:
label: Passwort
placeholder: root
msg: Password cannot be empty.
placeholder: wurzel
msg: Passwort kann nicht leer sein.
db_host:
label: Database Host
label: Datenbank-Host
placeholder: "db:3306"
msg: Database Host cannot be empty.
msg: Datenbankhost darf nicht leer sein.
db_name:
label: Database Name
label: Name der Datenbank
placeholder: answer
msg: Database Name cannot be empty.
msg: Der Datenbankname darf nicht leer sein.
db_file:
label: Database File
placeholder: /data/answer.db
msg: Database File cannot be empty.
label: Datenbankdatei
placeholder: /data/answer.Weder noch
msg: Datenbankdatei darf nicht leer sein.
config_yaml:
title: Create config.yaml
title: Erstellen Sie config.yaml
label: Die erstellte config.yaml-Datei.
desc: >-
Sie können die <1>config.yaml</1> Datei manuell im <1>/var/wwww/xxx/</1> Verzeichnis erstellen und den folgenden Text einfügen.
@ -1066,18 +1078,18 @@ ui:
answer_links: Antwortlinks
documents: Unterlagen
feedback: Rückmeldung
support: Support
review: Review
config: Config
update_to: Update to
latest: Latest
check_failed: Check failed
support: Unterstützung
review: Rezension
config: Konfig
update_to: Aktualisieren zu
latest: Neueste
check_failed: Prüfung fehlgeschlagen
"yes": "Ja"
"no": "Nein"
not_allowed: Not allowed
allowed: Allowed
enabled: Enabled
disabled: Disabled
not_allowed: Nicht erlaubt
allowed: Erlaubt
enabled: Ermöglicht
disabled: Behinderte
flags:
title: Flaggen
pending: Ausstehend
@ -1133,282 +1145,290 @@ ui:
all: Alle
staff: Mitarbeiter
inactive: Inaktiv
suspended: Suspendiert
deleted: Deleted
suspended: Ausgesetzt
deleted: Gelöscht
normal: Normal
Moderator: Moderator
Admin: Admin
User: User
Moderator: Moderation
Admin: Administrator
User: Benutzer
filter:
placeholder: "Filter by name, user:id"
set_new_password: Set new password
change_status: Change status
change_role: Change role
show_logs: Show logs
add_user: Add user
placeholder: "Filtern Sie nach Name, Benutzer:Id"
set_new_password: Neues Passwort festlegen
change_status: Status ändern
change_role: Rolle wechseln
show_logs: Protokolle anzeigen
add_user: Benutzer hinzufügen
new_password_modal:
title: Set new password
title: Neues Passwort festlegen
form:
fields:
password:
label: Password
text: The user will be logged out and need to login again.
msg: Password must be at 8-32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
label: Passwort
text: Der Benutzer wird abgemeldet und muss sich erneut anmelden.
msg: Das Passwort muss zwischen 8 und 32 Zeichen lang sein.
btn_cancel: Stornieren
btn_submit: Senden
user_modal:
title: Add new user
title: Neuen Benutzer hinzufügen
form:
fields:
display_name:
label: Display Name
msg: Display Name must be at 3-30 characters in length.
label: Anzeigename
msg: Der Anzeigename muss zwischen 3 und 30 Zeichen lang sein.
email:
label: Email
msg: Email is not valid.
msg: Email ist ungültig.
password:
label: Password
msg: Password must be at 8-32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
label: Passwort
msg: Das Passwort muss zwischen 8 und 32 Zeichen lang sein.
btn_cancel: Stornieren
btn_submit: Senden
questions:
page_title: Questions
page_title: Fragen
normal: Normal
closed: Closed
deleted: Deleted
closed: Geschlossen
deleted: Gelöscht
post: Post
votes: Votes
answers: Answers
created: Created
votes: Stimmen
answers: Antworten
created: Erstellt
status: Status
action: Action
change: Change
action: Aktion
change: Ändern
filter:
placeholder: "Filter by title, question:id"
placeholder: "Filtern nach Titel, Frage:Id"
answers:
page_title: Answers
page_title: Antworten
normal: Normal
deleted: Deleted
deleted: Gelöscht
post: Post
votes: Votes
created: Created
votes: Stimmen
created: Erstellt
status: Status
action: Action
change: Change
action: Aktion
change: Ändern
filter:
placeholder: "Filter by title, answer:id"
placeholder: "Filtern nach Titel, Antwort: id"
general:
page_title: General
page_title: Allgemein
name:
label: Site Name
msg: Site name cannot be empty.
text: "The name of this site, as used in the title tag."
label: Seitenname
msg: Der Site-Name darf nicht leer sein.
text: "Der Name dieser Website, wie er im Titel-Tag verwendet wird."
site_url:
label: Site URL
msg: Site url cannot be empty.
validate: Please enter a valid URL.
text: The address of your site.
label: Seiten-URL
msg: Die Website-Url darf nicht leer sein.
validate: Bitte geben Sie eine gültige URL ein.
text: Die Adresse Ihrer Website.
short_desc:
label: Short Site Description
msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage."
label: Kurze Seitenbeschreibung
msg: Die kurze Website-Beschreibung darf nicht leer sein.
text: "Kurze Beschreibung, wie im Titel-Tag auf der Homepage verwendet."
desc:
label: Site Description
msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag."
label: Seitenbeschreibung
msg: Die Websitebeschreibung darf nicht leer sein.
text: "Beschreiben Sie diese Website in einem Satz, wie er im Meta-Beschreibungs-Tag verwendet wird."
contact_email:
label: Contact Email
msg: Contact email cannot be empty.
validate: Contact email is not valid.
text: Email address of key contact responsible for this site.
label: Kontakt E-mail
msg: Kontakt-E-Mail darf nicht leer sein.
validate: Kontakt-E-Mail ist ungültig.
text: E-Mail-Adresse des Hauptkontakts, der für diese Website verantwortlich ist.
interface:
page_title: Interface
page_title: Schnittstelle
language:
label: Interface Language
msg: Interface language cannot be empty.
text: User interface language. It will change when you refresh the page.
label: Schnittstellensprache
msg: Sprache der Benutzeroberfläche darf nicht leer sein.
text: Sprache der Benutzeroberfläche. Sie ändert sich, wenn Sie die Seite aktualisieren.
time_zone:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
label: Zeitzone
msg: Die Zeitzone darf nicht leer sein.
text: Wählen Sie eine Stadt in derselben Zeitzone wie Sie.
avatar:
label: Standard-Avatar
text: Für Benutzer ohne eigenen Avatar.
smtp:
page_title: SMTP
from_email:
label: From Email
msg: From email cannot be empty.
text: The email address which emails are sent from.
label: Absender-E-Mail
msg: Von E-Mail darf nicht leer sein.
text: Die E-Mail-Adresse, von der E-Mails gesendet werden.
from_name:
label: From Name
msg: From name cannot be empty.
text: The name which emails are sent from.
label: Von Namen
msg: Absendername darf nicht leer sein.
text: Der Name, von dem E-Mails gesendet werden.
smtp_host:
label: SMTP Host
msg: SMTP host cannot be empty.
text: Your mail server.
label: SMTP-Host
msg: Der SMTP-Host darf nicht leer sein.
text: Ihr Mailserver.
encryption:
label: Encryption
msg: Encryption cannot be empty.
text: For most servers SSL is the recommended option.
label: Verschlüsselung
msg: Verschlüsselung darf nicht leer sein.
text: Für die meisten Server ist SSL die empfohlene Option.
ssl: SSL
none: None
none: Keine
smtp_port:
label: SMTP-Port
msg: SMTP port must be number 1 ~ 65535.
text: The port to your mail server.
msg: SMTP-Port muss Nummer 1 ~ 65535 sein.
text: Der Port zu Ihrem Mailserver.
smtp_username:
label: SMTP Username
msg: SMTP username cannot be empty.
label: SMTP-Benutzername
msg: Der SMTP-Benutzername darf nicht leer sein.
smtp_password:
label: SMTP Password
msg: SMTP password cannot be empty.
label: SMTP-Passwort
msg: Das SMTP-Passwort darf nicht leer sein.
test_email_recipient:
label: Test Email Recipients
text: Provide email address that will receive test sends.
msg: Test email recipients is invalid
label: E-Mail-Empfänger testen
text: Geben Sie eine E-Mail-Adresse an, die Testsendungen erhalten soll.
msg: Test-E-Mail-Empfänger ist ungültig
smtp_authentication:
label: Enable authentication
title: SMTP Authentication
msg: SMTP authentication cannot be empty.
"yes": "Yes"
"no": "No"
label: Authentifizierung aktivieren
title: SMTP-Authentifizierung
msg: Die SMTP-Authentifizierung darf nicht leer sein.
"yes": "Ja"
"no": "Nein"
branding:
page_title: Branding
logo:
label: Logo
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.
msg: Logo darf nicht leer sein.
text: Das Logobild oben links auf Ihrer Website. Verwenden Sie ein breites rechteckiges Bild mit einer Höhe von 56 und einem Seitenverhältnis von mehr als 3:1. Wenn das Feld leer gelassen wird, wird der Seitentiteltext angezeigt.
mobile_logo:
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.
label: Handy-Logo
text: Das Logo, das auf der mobilen Version Ihrer Website verwendet wird. Verwenden Sie ein breites rechteckiges Bild mit einer Höhe von 56. Wenn Sie dieses Feld leer lassen, wird das Bild aus der Einstellung "Logo" verwendet.
square_icon:
label: Square Icon
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
label: Quadratisches Symbol
msg: Quadratisches Symbol darf nicht leer sein.
text: Bild, das als Basis für Metadatensymbole verwendet wird. Sollte idealerweise größer als 512x512 sein.
favicon:
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: Ein Favicon für Ihre Website. Um korrekt über ein CDN zu funktionieren, muss es ein PNG sein. Wird auf 32x32 verkleinert. Wenn das Feld leer gelassen wird, wird "quadratisches Symbol" verwendet.
legal:
page_title: Legal
page_title: Gesetzlich
terms_of_service:
label: Terms of Service
text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here."
label: Nutzungsbedingungen
text: "Hier können Sie Inhalt der Nutzungsbedingungen hinzufügen. Wenn Sie bereits ein Dokument haben, das woanders gehostet wird, geben Sie hier die vollständige URL an."
privacy_policy:
label: Privacy Policy
text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here."
label: Datenschutz-Bestimmungen
text: "Hier können Sie Inhalte zur Datenschutzrichtlinie hinzufügen. Wenn Sie bereits ein Dokument haben, das woanders gehostet wird, geben Sie hier die vollständige URL an."
write:
page_title: Write
page_title: Schreiben
recommend_tags:
label: Recommend Tags
text: "Please input tag slug above, one tag per line."
label: Tags empfehlen
text: "Bitte geben Sie den Tag-Slug oben ein, ein Tag pro Zeile."
required_tag:
title: Required Tag
label: Set recommend tag as required
text: "Every new question must have at least one recommend tag."
title: Erforderliches Tag
label: Legen Sie das Empfehlungs-Tag nach Bedarf fest
text: "Jede neue Frage muss mindestens ein Empfehlungs-Tag haben."
reserved_tags:
label: Reserved Tags
text: "Reserved tags can only be added to a post by moderator."
label: Reservierte Tags
text: "Reservierte Tags können nur vom Moderator zu einem Beitrag hinzugefügt werden."
seo:
page_title: SEO
permalink:
label: Permalink
text: Custom URL structures can improve the usability, and forward-compatibility of your links.
label: Dauerlink
text: Benutzerdefinierte URL-Strukturen können die Benutzerfreundlichkeit und Aufwärtskompatibilität Ihrer Links verbessern.
robots:
label: robots.txt
text: This will permanently override any related site settings.
text: Dadurch werden alle zugehörigen Site-Einstellungen dauerhaft überschrieben.
themes:
page_title: Themes
page_title: Themen
themes:
label: Themes
text: Select an existing theme.
label: Themen
text: Wählen Sie ein vorhandenes Thema aus.
navbar_style:
label: Navbar Style
text: Select an existing theme.
label: Navbar-Stil
text: Wählen Sie ein vorhandenes Thema aus.
primary_color:
label: Primary Color
text: Modify the colors used by your themes
label: Primärfarbe
text: Ändern Sie die von Ihren Designs verwendeten Farben
css_and_html:
page_title: CSS and HTML
custom_css:
label: Custom CSS
text: This will insert as <link>
label: Benutzerdefinierte CSS
text: Dies wird als <link> eingefügt
head:
label: Head
text: This will insert before </head>
label: Kopf
text: Dies wird vor </head> eingefügt
header:
label: Header
text: This will insert after <body>
text: Dies wird nach <body> eingefügt
footer:
label: Footer
text: This will insert before </html>.
label: Fusszeile
text: Dies wird vor </html> eingefügt.
login:
page_title: Login
page_title: Anmeldung
membership:
title: Membership
label: Allow new registrations
text: Turn off to prevent anyone from creating a new account.
title: Mitgliedschaft
label: Neuregistrierungen zulassen
text: Deaktivieren Sie diese Option, um zu verhindern, dass jemand ein neues Konto erstellt.
private:
title: Private
label: Login required
text: Only logged in users can access this community.
title: Privatgelände
label: Anmeldung erforderlich
text: Nur angemeldete Benutzer können auf diese Community zugreifen.
form:
optional: (optional)
empty: cannot be empty
invalid: is invalid
btn_submit: Save
not_found_props: "Required property {{ key }} not found."
empty: kann nicht leer sein
invalid: ist ungültig
btn_submit: Speichern
not_found_props: "Erforderliche Eigenschaft {{ key }} nicht gefunden."
page_review:
review: Review
proposed: proposed
question_edit: Question edit
answer_edit: Answer edit
tag_edit: Tag edit
edit_summary: Edit summary
edit_question: Edit question
edit_answer: Edit answer
edit_tag: Edit tag
empty: No review tasks left.
review: Rezension
proposed: vorgeschlagen
question_edit: Frage bearbeiten
answer_edit: Antwort bearbeiten
tag_edit: Tag bearbeiten
edit_summary: Zusammenfassung bearbeiten
edit_question: Frage bearbeiten
edit_answer: Antwort bearbeiten
edit_tag: Tag bearbeiten
empty: Keine Überprüfungsaufgaben mehr übrig.
timeline:
undeleted: undeleted
deleted: deleted
downvote: downvote
upvote: upvote
accept: accept
cancelled: cancelled
commented: commented
rollback: rollback
edited: edited
answered: answered
asked: asked
closed: closed
reopened: reopened
created: created
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
undeleted: ungelöscht
deleted: gelöscht
downvote: ablehnen
upvote: positiv bewerten
accept: akzeptieren
cancelled: abgebrochen
commented: kommentiert
rollback: zurückrollen
edited: bearbeitet
answered: antwortete
asked: fragte
closed: geschlossen
reopened: wiedereröffnet
created: erstellt
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "Geschichte für"
tag_title: "Zeitleiste für"
show_votes: "Stimmen anzeigen"
n_or_a: N/A
title_for_question: "Timeline for"
title_for_answer: "Timeline for answer to {{ title }} by {{ author }}"
title_for_tag: "Timeline for tag"
datetime: Datetime
type: Type
by: By
comment: Comment
no_data: "We couldn't find anything."
title_for_question: "Zeitleiste für"
title_for_answer: "Zeitachse für die Antwort auf {{ title }} von {{ author }}"
title_for_tag: "Zeitachse für Tag"
datetime: Terminzeit
type: Typ
by: Von
comment: Kommentar
no_data: "Wir konnten nichts finden."
users:
title: Users
title: Benutzer
users_with_the_most_reputation: Users with the highest reputation scores this week
users_with_the_most_vote: Users who voted the most this week
staffs: Our community staff
reputation: reputation
votes: votes
staffs: Benutzer
reputation: ruf
votes: stimmen
prompt:
leave_page: Are you sure you want to leave the page?
changes_not_save: Your changes may not be saved.
leave_page: Möchten Sie die Seite wirklich verlassen?
changes_not_save: Ihre Änderungen werden möglicherweise nicht gespeichert.
draft:
discard_confirm: Are you sure you want to discard your draft?
discard_confirm: Möchten Sie Ihren Entwurf wirklich verwerfen?
messages:
post_deleted: This post has been deleted.
post_deleted: Dieser Beitrag wurde gelöscht.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -25,6 +25,14 @@ backend:
other: Reopen
forbidden_error:
other: Forbidden.
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -40,6 +48,58 @@ backend:
other: Have the full power to access the site.
moderator:
other: Has access to all posts except admin settings.
privilege:
level_1:
description:
other: Level 1 (less reputation required for private team, group)
level_2:
description:
other: Level 2 (low reputation required for startup community)
level_3:
description:
other: Level 3 (high reputation required for mature community)
rank_question_add_label:
other: Ask question
rank_answer_add_label:
other: Write answer
rank_comment_add_label:
other: Write comment
rank_report_add_label:
other: Flag
rank_comment_vote_up_label:
other: Upvote comment
rank_link_url_limit_label:
other: Post more than 2 links at a time
rank_question_vote_up_label:
other: Upvote question
rank_answer_vote_up_label:
other: Upvote answer
rank_question_vote_down_label:
other: Downvote question
rank_answer_vote_down_label:
other: Downvote answer
rank_tag_add_label:
other: Create new tag
rank_tag_edit_label:
other: Edit tag description (need to review)
rank_question_edit_label:
other: Edit other's question (need to review)
rank_answer_edit_label:
other: Edit other's answer (need to review)
rank_question_edit_without_review_label:
other: Edit other's question without review
rank_answer_edit_without_review_label:
other: Edit other's answer without review
rank_question_audit_label:
other: Review question edits
rank_answer_audit_label:
other: Review answer edits
rank_tag_audit_label:
other: Review tag edits
rank_tag_edit_without_review_label:
other: Edit tag description without review
rank_tag_synonym_label:
other: Manage tag synonyms
email:
other: Email
password:
@ -78,7 +138,7 @@ backend:
verify_url_expired:
other: Email verified URL has expired, please resend the email.
illegal_email_domain_error:
other: The domain name of the current email address cannot be registered.
other: Email is not allowed from that email domain. Please use another one.
lang:
not_found:
other: Language file not found.
@ -170,6 +230,10 @@ backend:
other: You cannot modify your role.
not_allowed_registration:
other: Currently the site is not open for registration
access_denied:
other: Access denied
page_access_denied:
other: You do not have access to this page.
config:
read_config_failed:
other: Read config failed
@ -814,6 +878,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -852,13 +917,14 @@ ui:
li1_2: Back up any statements you make with references or personal experience.
header_2: But <strong>avoid</strong> ...
li2_1: Asking for help, seeking clarification, or responding to other answers.
reopen:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -872,7 +938,6 @@ ui:
of accepted answers can result in your account being blocked from answering.
Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -888,6 +953,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1110,14 +1176,16 @@ ui:
seo: SEO
customize: Customize
themes: Themes
css-html: CSS/HTML
css_html: CSS/HTML
login: Login
privileges: Privileges
plugins: Plugins
installed_plugins: Installed Plugins
website_welcome: Welcome to {{site_name}}
plugins:
login: Login
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
oauth:
connect: Connect with {{ auth_name }}
remove: Remove {{ auth_name }}
@ -1314,9 +1382,6 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:
@ -1426,6 +1491,9 @@ ui:
footer:
label: Footer
text: This will insert before </html>.
sidebar:
label: Sidebar
text: This will insert in sidebar.
login:
page_title: Login
membership:
@ -1460,7 +1528,30 @@ ui:
deactivate: Deactivate
activate: Activate
settings: Settings
settings_users:
title: Users
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
profile_editable:
title: Profile Editable
allow_update_display_name:
label: Allow users to change their display name
allow_update_username:
label: Allow users to change their username
allow_update_avatar:
label: Allow users to change their profile image
allow_update_bio:
label: Allow users to change their about me
allow_update_website:
label: Allow users to change their website
allow_update_location:
label: Allow users to change their location
privilege:
title: Privileges
level:
label: Reputation required level
text: Choose the reputation required for the privileges
form:
optional: (optional)
@ -1496,6 +1587,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1521,5 +1616,9 @@ ui:
draft:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -46,9 +54,9 @@ backend:
error:
admin:
cannot_update_their_password:
other: You cannot modify your password.
other: No puede modificar su contraseña.
cannot_modify_self_status:
other: You cannot modify your status.
other: No puede modificar su contraseña.
email_or_password_wrong:
other: Contraseña o correo incorrecto.
answer:
@ -131,7 +139,7 @@ backend:
cannot_update:
other: Sin permiso para actualizar.
is_used_cannot_delete:
other: You cannot delete a tag that is in use
other: No puede eliminar una etiqueta que está en uso
cannot_set_synonym_as_itself:
other: No se puede establecer como sinónimo de una etiqueta la propia etiqueta.
smtp:
@ -176,7 +184,7 @@ backend:
other: No es posible crear el archivo config.yaml.
upload:
unsupported_file_format:
other: Unsupported file format.
other: Formato de archivo no soportado.
report:
spam:
name:
@ -359,7 +367,7 @@ ui:
msg:
empty: Código no puede estar vacío.
language:
label: Language
label: Idioma
placeholder: Detección automática
btn_cancel: Cancelar
btn_confirm: Añadir
@ -395,7 +403,7 @@ ui:
only_image: Solo se permiten archivos de imagen.
max_size: El tamaño del archivo no puede superar 4MB.
desc:
label: Description
label: Descripción
tab_url: URL de la imagen
form_url:
fields:
@ -424,7 +432,7 @@ ui:
msg:
empty: La dirección no puede estar vacía.
name:
label: Description
label: Descripción
btn_cancel: Cancelar
btn_confirm: Añadir
ordered_list:
@ -466,13 +474,13 @@ ui:
range: Nombre a mostrar con un máximo de 35 caracteres.
slug_name:
label: URL amigable
desc: 'Debe usar el conjunto de caracteres "a-z", "0-9", "+ # - ."'
desc: Slug de URL de hasta 35 caracteres.
msg:
empty: URL no puede estar vacío.
range: URL slug hasta 35 caracteres.
character: La URL amigable contiene caracteres no permitidos.
desc:
label: Description
label: Descripción
btn_cancel: Cancelar
btn_submit: Enviar
btn_post: Post new tag
@ -491,10 +499,10 @@ ui:
delete:
title: Eliminar esta etiqueta
tip_with_posts: >-
<p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
<p>No permitimos <strong>eliminar etiquetas con publicaciones</strong>.</p> <p>Primero elimine esta etiqueta de las publicaciones.</p>
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?
<p>No permitimos <strong>eliminar etiquetas con sinónimos</strong>.</p> <p>Elimine primero los sinónimos de esta etiqueta.</p>
tip: '¿Estás seguro de que deseas borrarlo?'
close: Cerrar
edit_tag:
title: Editar etiqueta
@ -714,12 +722,12 @@ ui:
display_name:
label: Nombre a mostrar
msg: El nombre a mostrar no puede estar vacío.
msg_range: Display name up to 30 characters.
msg_range: Mostrar nombre de hasta 30 caracteres.
username:
label: Nombre de usuario
caption: La gente puede mencionarte con "@nombredeusuario".
msg: El nombre de usuario no puede estar vacío.
msg_range: Username up to 30 characters.
msg_range: Nombre de usuario de hasta 30 caracteres.
character: 'Debe usar el conjunto de caracteres "a-z", "0-9", " - . _"'
avatar:
label: Imagen de perfil
@ -731,13 +739,13 @@ ui:
default: Sistema
msg: Por favor, sube una imagen
bio:
label: About Me
label: Sobre mí
website:
label: Website
label: Sitio Web
placeholder: "https://example.com"
msg: Formato del sitio web incorrecto
location:
label: Location
label: Ubicación
placeholder: "Ciudad, País"
notification:
heading: Notificaciones
@ -753,7 +761,7 @@ ui:
email:
label: Correo electrónico
msg: El correo electrónico no puede estar vacío.
password_title: Password
password_title: Contraseña
current_pass:
label: Contraseña actual
msg:
@ -780,6 +788,7 @@ ui:
btn: Añadir pregunta
answers: respuestas
question_detail:
action: Action
Asked: Preguntada
asked: preguntada
update: Modificada
@ -810,7 +819,7 @@ ui:
confirm_info: >-
<p>¿Seguro que quieres añadir otra respuesta?</p><p>Puedes utilizar el enlace de edición para detallar y mejorar tu respuesta existente en su lugar.</p>
empty: La respuesta no puede estar vacía.
characters: content must be at least 6 characters in length.
characters: el contenido debe tener al menos 6 caracteres.
tips:
header_1: Thanks for your answer
li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.
@ -821,7 +830,10 @@ ui:
confirm_btn: Reopen
title: Reabrir esta publicación
content: '¿Seguro que quieres reabrir esta publicación?'
success: Esta publicación ha sido reabierta
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Eliminar esta publicación
question: >-
@ -833,7 +845,6 @@ ui:
El borrado repetido de respuestas aceptadas puede resultar en que tu cuenta se bloquee para responder. ¿Estás seguro de que deseas borrarlo?
other: '¿Estás seguro de que deseas borrarlo?'
tip_question_deleted: Esta publicación ha sido eliminada
tip_answer_deleted: Esta respuesta ha sido eliminada
btns:
confirm: Confirmar
@ -848,7 +859,8 @@ ui:
approve: Aprobar
reject: Rechazar
skip: Omitir
discard_draft: Discard draft
discard_draft: Descartar borrador
pinned: Pinned
search:
title: Resultados de la búsqueda
keywords: Palabras claves
@ -883,7 +895,7 @@ ui:
modal_confirm:
title: Error...
account_result:
page_title: Welcome to {{site_name}}
page_title: Bienvenido a {{site_name}}
success: Tu nueva cuenta ha sido confirmada, serás redirigido a la página de inicio.
link: Continuar a la página de inicio
invalid: >-
@ -894,7 +906,7 @@ ui:
unsubscribe:
page_title: Desuscribir
success_title: Desuscrito con éxito
success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.
success_desc: Ha sido eliminado con éxito de esta lista de suscriptores y no recibirá más correos electrónicos nuestros.
link: Cambiar ajustes
question:
following_tags: Etiquetas seguidas
@ -934,109 +946,109 @@ ui:
visited_x_days: "Visitado {{ count }} días"
viewed: Visto
joined: Unido
last_login: Seen
last_login: Visto
about_me: Sobre mí
about_me_empty: "// ¡Hola Mundo!"
top_answers: Mejores respuestas
top_questions: Top Questions
top_questions: Preguntas Principales
stats: Estadísticas
list_empty: No posts found.<br />Perhaps you'd like to select a different tab?
list_empty: No se encontraron publicaciones.<br />¿Quizás le gustaría seleccionar una pestaña diferente?
accepted: Aceptada
answered: answered
asked: asked
upvote: upvote
downvote: downvote
mod_short: Mod
mod_long: Moderators
x_reputation: reputation
x_votes: votes received
x_answers: answers
x_questions: questions
answered: respondida
asked: preguntó
upvote: votar a favor
downvote: voto negativo
mod_short: Modificación
mod_long: Moderadores
x_reputation: reputación
x_votes: votos recibidos
x_answers: respuestas
x_questions: preguntas
install:
title: Answer
next: Next
done: Done
config_yaml_error: Can't create the config.yaml file.
title: Respuesta
next: Próximo
done: Hecho
config_yaml_error: No se puede crear el archivo config.yaml.
lang:
label: Please Choose a Language
label: Elija un idioma
db_type:
label: Database Engine
label: Motor de base de datos
db_username:
label: Username
placeholder: root
msg: Username cannot be empty.
label: Nombre de usuario
placeholder: raíz
msg: El nombre de usuario no puede estar vacío.
db_password:
label: Password
placeholder: root
msg: Password cannot be empty.
label: Contraseña
placeholder: raíz
msg: La contraseña no puede estar vacía.
db_host:
label: Database Host
label: Anfitrión de la base de datos
placeholder: "db:3306"
msg: Database Host cannot be empty.
msg: El host de la base de datos no puede estar vacío.
db_name:
label: Database Name
placeholder: answer
msg: Database Name cannot be empty.
label: Nombre de la base de datos
placeholder: respuesta
msg: El nombre de la base de datos no puede estar vacío.
db_file:
label: Database File
placeholder: /data/answer.db
msg: Database File cannot be empty.
label: Archivo de base de datos
placeholder: /data/respuesta.db
msg: El archivo de la base de datos no puede estar vacío.
config_yaml:
title: Create config.yaml
label: The config.yaml file created.
title: Crear config.yaml
label: El archivo config.yaml creado.
desc: >-
You can create the <1>config.yaml</1> file manually in the <1>/var/wwww/xxx/</1> directory and paste the following text into it.
info: After you've done that, click "Next" button.
site_information: Site Information
admin_account: Admin Account
Puede crear el archivo <1>config.yaml</1> manualmente en el directorio <1>/var/www/xxx/</1> y pegar el siguiente texto en él.
info: Después de haber hecho eso, haga clic en el botón "Siguiente".
site_information: Información del sitio
admin_account: Cuenta de administrador
site_name:
label: Site Name
msg: Site Name cannot be empty.
label: Nombre del sitio
msg: El nombre del sitio no puede estar vacío.
site_url:
label: Site URL
text: The address of your site.
label: Sitio URL
text: La dirección de su sitio.
msg:
empty: Site URL cannot be empty.
incorrect: Site URL incorrect format.
empty: La URL del sitio no puede estar vacía.
incorrect: Formato incorrecto de la URL del sitio.
contact_email:
label: Contact Email
text: Email address of key contact responsible for this site.
label: Email de contacto
text: Dirección de correo electrónico del contacto clave responsable de este sitio.
msg:
empty: Contact Email cannot be empty.
incorrect: Contact Email incorrect format.
empty: El correo electrónico de contacto no puede estar vacío.
incorrect: Correo electrónico de contacto formato incorrecto.
admin_name:
label: Name
msg: Name cannot be empty.
label: Nombre
msg: El nombre no puede estar vacío.
admin_password:
label: Password
label: Contraseña
text: >-
You will need this password to log in. Please store it in a secure location.
msg: Password cannot be empty.
Necesitará esta contraseña para iniciar sesión. Guárdela en un lugar seguro.
msg: La contraseña no puede estar vacía.
admin_email:
label: Email
text: You will need this email to log in.
label: Correo electrónico
text: Necesitará este correo electrónico para iniciar sesión.
msg:
empty: Email cannot be empty.
incorrect: Email incorrect format.
ready_title: Your Answer is Ready!
empty: El correo electrónico no puede estar vacío.
incorrect: Correo electrónico con formato incorrecto.
ready_title: '¡Tu respuesta está lista!'
ready_desc: >-
If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.
good_luck: "Have fun, and good luck!"
warn_title: Warning
Si alguna vez desea cambiar más configuraciones, visite la <1>sección de administración</1>; encuéntrelo en el menú del sitio.
good_luck: "¡Diviértete y buena suerte!"
warn_title: Advertencia
warn_desc: >-
The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.
install_now: You may try <1>installing now</1>.
installed: Already installed
El archivo <1>config.yaml</1> ya existe. Si necesita restablecer alguno de los elementos de configuración de este archivo, elimínelo primero.
install_now: Puede intentar <1>instalar ahora</1>.
installed: Ya instalado
installed_desc: >-
You appear to have already installed. To reinstall please clear your old database tables first.
db_failed: Database connection failed
Parece que ya lo has instalado. Para reinstalar, borre primero las tablas de la base de datos anterior.
db_failed: La conexión a la base de datos falló
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.
Esto significa que la información de la base de datos en su archivo <1>config.yaml</1> es incorrecta o que no se pudo establecer contacto con el servidor de la base de datos. Esto podría significar que el servidor de la base de datos de su host está inactivo.
counts:
views: views
votes: votes
answers: answers
accepted: Accepted
views: puntos de vista
votes: votos
answers: respuestas
accepted: Aceptado
page_error:
http_error: HTTP Error {{ code }}
desc_403: You dont have permission to access this page.
@ -1044,323 +1056,323 @@ ui:
desc_50X: The server encountered an error and could not complete your request.
back_home: Back to homepage
page_maintenance:
desc: "We are under maintenance, we'll be back soon."
desc: "Estamos en mantenimiento, pronto estaremos de vuelta."
nav_menus:
dashboard: Dashboard
contents: Contents
questions: Questions
answers: Answers
users: Users
flags: Flags
settings: Settings
dashboard: Panel
contents: Contenido
questions: Preguntas
answers: Respuestas
users: Usuarios
flags: Banderas
settings: Ajustes
general: General
interface: Interface
interface: Interfaz
smtp: SMTP
branding: Branding
branding: Marca
legal: Legal
write: Write
tos: Terms of Service
privacy: Privacy
seo: SEO
customize: Customize
themes: Themes
write: Escribir
tos: Términos de servicio
privacy: Privacidad
seo: ESTE
customize: Personalizar
themes: Temas
css-html: CSS/HTML
login: Login
login: Iniciar sesión
admin:
admin_header:
title: Admin
title: Administrador
dashboard:
title: Dashboard
welcome: Welcome to Answer Admin!
site_statistics: Site Statistics
questions: "Questions:"
answers: "Answers:"
comments: "Comments:"
votes: "Votes:"
active_users: "Active users:"
flags: "Flags:"
site_health_status: Site Health Status
version: "Version:"
title: Panel
welcome: '¡Bienvenido a Answer Admin!'
site_statistics: Estadísticas del sitio
questions: "Preguntas:"
answers: "Respuestas:"
comments: "Comentarios:"
votes: "Votos:"
active_users: "Usuarios activos:"
flags: "Banderas:"
site_health_status: Estado de salud del sitio
version: "Versión:"
https: "HTTPS:"
uploading_files: "Uploading files:"
uploading_files: "Subiendo archivos:"
smtp: "SMTP:"
timezone: "Timezone:"
system_info: System Info
storage_used: "Storage used:"
uptime: "Uptime:"
answer_links: Answer Links
documents: Documents
feedback: Feedback
support: Support
review: Review
config: Config
update_to: Update to
latest: Latest
check_failed: Check failed
"yes": "Yes"
timezone: "Zona horaria:"
system_info: Información del sistema
storage_used: "Almacenamiento utilizado:"
uptime: "Tiempo ejecutándose:"
answer_links: Enlaces de respuesta
documents: Documentos
feedback: Comentario
support: Soporte
review: Revisar
config: Configuración
update_to: Actualizar para
latest: Lo más nuevo
check_failed: Comprobación fallida
"yes": "Si"
"no": "No"
not_allowed: Not allowed
allowed: Allowed
enabled: Enabled
disabled: Disabled
not_allowed: No permitido
allowed: Permitido
enabled: Activado
disabled: Desactivado
flags:
title: Flags
pending: Pending
completed: Completed
flagged: Flagged
created: Created
action: Action
review: Review
title: Banderas
pending: Pendiente
completed: Terminado
flagged: Marcado
created: Creado
action: Acción
review: Revisar
change_modal:
title: Change user status to...
btn_cancel: Cancel
btn_submit: Submit
title: Cambiar estado de usuario a...
btn_cancel: Cancelar
btn_submit: Entregar
normal_name: normal
normal_desc: A normal user can ask and answer questions.
suspended_name: suspended
suspended_desc: A suspended user can't log in.
deleted_name: deleted
deleted_desc: "Delete profile, authentication associations."
inactive_name: inactive
inactive_desc: An inactive user must re-validate their email.
confirm_title: Delete this user
confirm_content: Are you sure you want to delete this user? This is permanent!
confirm_btn: Delete
normal_desc: Un usuario normal puede hacer y responder preguntas.
suspended_name: suspendido
suspended_desc: Un usuario suspendido no puede iniciar sesión.
deleted_name: eliminado
deleted_desc: "Eliminar perfil, asociaciones de autenticación."
inactive_name: inactivo
inactive_desc: Un usuario inactivo debe volver a validar su correo electrónico.
confirm_title: Eliminar este usuario
confirm_content: '¿Está seguro de que desea eliminar este usuario? ¡Esto es permanente!'
confirm_btn: Borrar
msg:
empty: Please select a reason.
empty: Por favor seleccione una razón.
status_modal:
title: "Change {{ type }} status to..."
title: "Cambiar el estado de {{ type }} a..."
normal_name: normal
normal_desc: A normal post available to everyone.
closed_name: closed
closed_desc: "A closed question can't answer, but still can edit, vote and comment."
deleted_name: deleted
deleted_desc: All reputation gained and lost will be restored.
btn_cancel: Cancel
btn_submit: Submit
btn_next: Next
normal_desc: Un puesto normal al alcance de todos.
closed_name: cerrada
closed_desc: "Una pregunta cerrada no puede responder, pero aún puede editar, votar y comentar."
deleted_name: eliminado
deleted_desc: Toda la reputación ganada y perdida será restaurada.
btn_cancel: Cancelar
btn_submit: Entregar
btn_next: Próximo
user_role_modal:
title: Change user role to...
btn_cancel: Cancel
btn_submit: Submit
title: Cambiar rol de usuario a...
btn_cancel: Cancelar
btn_submit: Entregar
users:
title: Users
name: Name
email: Email
reputation: Reputation
created_at: Created Time
delete_at: Deleted Time
suspend_at: Suspended Time
status: Status
title: Usuarios
name: Nombre
email: Correo electrónico
reputation: Reputación
created_at: Creado Tiempo
delete_at: Hora eliminada
suspend_at: Tiempo suspendido
status: Estado
role: Role
action: Action
change: Change
all: All
staff: Staff
inactive: Inactive
suspended: Suspended
deleted: Deleted
action: Acción
change: Cambiar
all: Todo
staff: Personal
inactive: Inactivo
suspended: Suspendido
deleted: Eliminado
normal: Normal
Moderator: Moderator
Admin: Admin
User: User
Moderator: Moderador
Admin: Administrador
User: Usuario
filter:
placeholder: "Filter by name, user:id"
set_new_password: Set new password
change_status: Change status
change_role: Change role
show_logs: Show logs
add_user: Add user
placeholder: "Filtrar por nombre, usuario:id"
set_new_password: Establecer nueva contraseña
change_status: Cambiar Estado
change_role: Cambiar rol
show_logs: Mostrar registros
add_user: Agregar usuario
new_password_modal:
title: Set new password
title: Establecer nueva contraseña
form:
fields:
password:
label: Password
text: The user will be logged out and need to login again.
msg: Password must be at 8-32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
label: Contraseña
text: El usuario se desconectará y deberá volver a iniciar sesión.
msg: La contraseña debe tener entre 8 y 32 caracteres de longitud.
btn_cancel: Cancelar
btn_submit: Entregar
user_modal:
title: Add new user
title: Añadir nuevo usuario
form:
fields:
display_name:
label: Display Name
msg: Display Name must be at 3-30 characters in length.
label: Nombre para mostrar
msg: El nombre para mostrar debe tener entre 3 y 30 caracteres de longitud.
email:
label: Email
msg: Email is not valid.
label: Correo electrónico
msg: El correo no es válido.
password:
label: Password
msg: Password must be at 8-32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
label: Contraseña
msg: La contraseña debe tener entre 8 y 32 caracteres de longitud.
btn_cancel: Cancelar
btn_submit: Entregar
questions:
page_title: Questions
page_title: Preguntas
normal: Normal
closed: Closed
deleted: Deleted
post: Post
votes: Votes
answers: Answers
created: Created
status: Status
action: Action
change: Change
closed: Cerrado
deleted: Eliminado
post: Correo
votes: Votos
answers: Respuestas
created: Creado
status: Estado
action: Acción
change: Cambiar
filter:
placeholder: "Filter by title, question:id"
placeholder: "Filtrar por título, pregunta:id"
answers:
page_title: Answers
page_title: Respuestas
normal: Normal
deleted: Deleted
post: Post
votes: Votes
created: Created
status: Status
action: Action
change: Change
deleted: Eliminado
post: Correo
votes: Votos
created: Creado
status: Estado
action: Acción
change: Cambiar
filter:
placeholder: "Filter by title, answer:id"
placeholder: "Filtrar por título, respuesta: id"
general:
page_title: General
name:
label: Site Name
msg: Site name cannot be empty.
text: "The name of this site, as used in the title tag."
label: Nombre del sitio
msg: El nombre del sitio no puede estar vacío.
text: "El nombre de este sitio, tal como se usa en la etiqueta del título."
site_url:
label: Site URL
msg: Site url cannot be empty.
validate: Please enter a valid URL.
text: The address of your site.
label: Sitio URL
msg: La url del sitio no puede estar vacía.
validate: Por favor introduzca un URL válido.
text: La dirección de su sitio.
short_desc:
label: Short Site Description
msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage."
label: Breve descripción del sitio
msg: La descripción breve del sitio no puede estar vacía.
text: "Breve descripción, tal como se usa en la etiqueta del título en la página de inicio."
desc:
label: Site Description
msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag."
label: Descripción del lugar
msg: La descripción del sitio no puede estar vacía.
text: "Describa este sitio en una oración, como se usa en la etiqueta de meta descripción."
contact_email:
label: Contact Email
msg: Contact email cannot be empty.
validate: Contact email is not valid.
text: Email address of key contact responsible for this site.
label: Email de contacto
msg: El correo electrónico de contacto no puede estar vacío.
validate: El correo electrónico de contacto no es válido.
text: Dirección de correo electrónico del contacto clave responsable de este sitio.
interface:
page_title: Interface
page_title: Interfaz
language:
label: Interface Language
msg: Interface language cannot be empty.
text: User interface language. It will change when you refresh the page.
label: Lenguaje de interfaz
msg: El idioma de la interfaz no puede estar vacío.
text: Idioma de la interfaz de usuario. Cambiará cuando actualice la página.
time_zone:
label: Timezone
label: Zona horaria
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
text: Elija una ciudad en la misma zona horaria que usted.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:
label: From Email
msg: From email cannot be empty.
text: The email address which emails are sent from.
label: Desde el e-mail
msg: Desde el correo electrónico no puede estar vacío.
text: La dirección de correo electrónico desde la que se envían los correos electrónicos.
from_name:
label: From Name
msg: From name cannot be empty.
text: The name which emails are sent from.
label: De nombre
msg: Desde el nombre no puede estar vacío.
text: El nombre desde el que se envían los correos electrónicos.
smtp_host:
label: SMTP Host
msg: SMTP host cannot be empty.
text: Your mail server.
label: Anfitrión SMTP
msg: El host SMTP no puede estar vacío.
text: Su servidor de correo.
encryption:
label: Encryption
msg: Encryption cannot be empty.
text: For most servers SSL is the recommended option.
label: Cifrado
msg: El cifrado no puede estar vacío.
text: Para la mayoría de los servidores, SSL es la opción recomendada.
ssl: SSL
none: None
none: Ninguno
smtp_port:
label: SMTP Port
msg: SMTP port must be number 1 ~ 65535.
text: The port to your mail server.
label: Puerto SMTP
msg: El puerto SMTP debe ser el número 1 ~ 65535.
text: El puerto a su servidor de correo.
smtp_username:
label: SMTP Username
label: Nombre de usuario SMTP
msg: SMTP username cannot be empty.
smtp_password:
label: SMTP Password
msg: SMTP password cannot be empty.
label: Contraseña SMTP
msg: La contraseña SMTP no puede estar vacía.
test_email_recipient:
label: Test Email Recipients
text: Provide email address that will receive test sends.
msg: Test email recipients is invalid
label: Destinatarios de correo electrónico de prueba
text: Proporcione la dirección de correo electrónico que recibirá los envíos de prueba.
msg: Los destinatarios de correo electrónico de prueba no son válidos
smtp_authentication:
label: Enable authentication
title: SMTP Authentication
msg: SMTP authentication cannot be empty.
"yes": "Yes"
label: Habilitar autenticación
title: Autenticación SMTP
msg: La autenticación SMTP no puede estar vacía.
"yes": "Si"
"no": "No"
branding:
page_title: Branding
page_title: Marca
logo:
label: Logo
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.
msg: El logotipo no puede estar vacío.
text: La imagen del logotipo en la parte superior izquierda de su sitio. Utilice una imagen rectangular ancha con una altura de 56 y una relación de aspecto superior a 3:1. Si se deja en blanco, se mostrará el texto del título del sitio.
mobile_logo:
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.
label: Logotipo móvil
text: El logotipo utilizado en la versión móvil de su sitio. Utilice una imagen rectangular ancha con una altura de 56. Si se deja en blanco, se utilizará la imagen de la configuración de "logotipo".
square_icon:
label: Square Icon
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
label: Icono cuadrado
msg: El icono cuadrado no puede estar vacío.
text: Imagen utilizada como base para los iconos de metadatos. Idealmente, debería ser más grande que 512x512.
favicon:
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.
label: Icono de favoritos
text: Un favicon para su sitio. Para que funcione correctamente sobre un CDN, debe ser un png. Se cambiará el tamaño a 32x32. Si se deja en blanco, se utilizará el "icono cuadrado".
legal:
page_title: Legal
terms_of_service:
label: Terms of Service
text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here."
label: Términos de servicio
text: "Puede agregar términos de contenido de servicio aquí. Si ya tiene un documento alojado en otro lugar, proporcione la URL completa aquí."
privacy_policy:
label: Privacy Policy
text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here."
label: Política de privacidad
text: "Puede agregar contenido de política de privacidad aquí. Si ya tiene un documento alojado en otro lugar, proporcione la URL completa aquí."
write:
page_title: Write
page_title: Escribir
recommend_tags:
label: Recommend Tags
text: "Please input tag slug above, one tag per line."
label: Recomendar etiquetas
text: "Ingrese el slug de la etiqueta arriba, una etiqueta por línea."
required_tag:
title: Required Tag
label: Set recommend tag as required
text: "Every new question must have at least one recommend tag."
title: Etiqueta requerida
label: Establezca la etiqueta de recomendación según sea necesario
text: "Cada nueva pregunta debe tener al menos una etiqueta de recomendación."
reserved_tags:
label: Reserved Tags
text: "Reserved tags can only be added to a post by moderator."
label: Etiquetas reservadas
text: "Solo el moderador puede agregar etiquetas reservadas a una publicación."
seo:
page_title: SEO
permalink:
label: Permalink
text: Custom URL structures can improve the usability, and forward-compatibility of your links.
label: Enlace permanente
text: Las estructuras de URL personalizadas pueden mejorar la facilidad de uso y la compatibilidad futura de sus enlaces.
robots:
label: robots.txt
text: This will permanently override any related site settings.
text: Esto anulará permanentemente cualquier configuración del sitio relacionada.
themes:
page_title: Themes
page_title: Temas
themes:
label: Themes
text: Select an existing theme.
label: Temas
text: Seleccione un tema existente.
navbar_style:
label: Navbar Style
text: Select an existing theme.
label: Estilo de la barra de navegación
text: Seleccione un tema existente.
primary_color:
label: Color principal
text: Modify the colors used by your themes
text: Modifica los colores usados por tus temas
css_and_html:
page_title: CSS y HTML
custom_css:
label: CSS personalizado
text: Esto insertará como <link>
head:
label: Head
label: Cabeza
text: Esto se insertará antes de </head>
header:
label: Encabezado
@ -1398,30 +1410,34 @@ ui:
timeline:
undeleted: recuperado
deleted: eliminado
downvote: downvote
upvote: upvote
accept: accept
downvote: voto negativo
upvote: votar a favor
accept: aceptar
cancelled: cancelado
commented: comentado
rollback: rollback
edited: edited
answered: answered
asked: asked
rollback: retroceder
edited: editada
answered: contestada
asked: preguntó
closed: cerrado
reopened: reabierto
created: creado
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "Historial para"
tag_title: "Línea temporal para"
show_votes: "Mostrar votos"
n_or_a: N/A
title_for_question: "Timeline for"
title_for_answer: "Timeline for answer to {{ title }} by {{ author }}"
title_for_tag: "Timeline for tag"
datetime: Datetime
type: Type
by: By
title_for_question: "Línea de tiempo para"
title_for_answer: "Cronología de la respuesta a {{ title }} por {{ author }}"
title_for_tag: "Cronología de la etiqueta"
datetime: Fecha y hora
type: Tipo
by: Por
comment: Comentario
no_data: "We couldn't find anything."
no_data: "No pudimos encontrar nada."
users:
title: Usuarios
users_with_the_most_reputation: Users with the highest reputation scores this week
@ -1430,10 +1446,14 @@ ui:
reputation: reputación
votes: votos
prompt:
leave_page: Are you sure you want to leave the page?
changes_not_save: Your changes may not be saved.
leave_page: '¿Seguro que quieres salir de la página?'
changes_not_save: Es posible que sus cambios no se guarden.
draft:
discard_confirm: Are you sure you want to discard your draft?
discard_confirm: '¿Está seguro de que desea descartar este borrador?'
messages:
post_deleted: This post has been deleted.
post_deleted: Esta publicación ha sido eliminada.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -759,6 +767,7 @@ ui:
btn: Ajouter une question
answers: réponses
question_detail:
action: Action
Asked: Demandé
asked: demandé
update: Modifié
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Rouvrir ce message
content: Êtes-vous sûr de vouloir rouvrir ?
success: Ce message a été rouvert
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Supprimer la publication
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>Nous ne recommandons pas <strong>de supprimer la réponse acceptée</strong> car cela prive les futurs lecteurs de cette connaissance. </p> La suppression répétée des réponses acceptées peut empêcher votre compte de répondre. Êtes-vous sûr de vouloir supprimer ?
other: Êtes-vous sûr de vouloir supprimer ?
tip_question_deleted: Ce message a été supprimé
tip_answer_deleted: Cette réponse a été supprimée
btns:
confirm: Confimer
@ -824,6 +835,7 @@ ui:
reject: Rejeter
skip: Ignorer
discard_draft: Abandonner le brouillon
pinned: Pinned
search:
title: Résultats de la recherche
keywords: Mots-clés
@ -1385,6 +1397,10 @@ ui:
closed: fermé
reopened: réouvert
created: créé
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "Historique de"
tag_title: "Chronologie de"
show_votes: "Afficher les votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Êtes-vous sûr de vouloir abandonner ce brouillon ?
messages:
post_deleted: Ce message a été supprimé.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -759,6 +767,7 @@ ui:
btn: Tambahkan pertanyaan
answers: jawaban
question_detail:
action: Action
Asked: Ditanyakan
asked: ditanyakan
update: Diubah
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Buka kembali postingan ini
content: Kamu yakin ingin membuka kembali?
success: Postingan ini telah dibuka kembali
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Hapus pos ini
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>Kami tidak menyarankan <strong>menghapus jawaban yang diterima</strong> karena hal itu menghilangkan pengetahuan ini dari pembaca di masa mendatang. </p> Penghapusan berulang dari jawaban yang diterima dapat menyebabkan akun Anda diblokir dari menjawab. Apakah Anda yakin ingin menghapus?
other: Anda yakin ingin menghapusnya?
tip_question_deleted: Kiriman ini sudah dihapus
tip_answer_deleted: Jawaban ini telah dihapus
btns:
confirm: Konfirmasi
@ -824,6 +835,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1385,6 +1397,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -38,9 +46,9 @@ backend:
moderator:
other: Ha accesso a tutti i post tranne le impostazioni di amministratore.
email:
other: email
other: E-mail
password:
other: password
other: Chiave di accesso
email_or_password_wrong_error:
other: Email o password errati
error:
@ -445,7 +453,7 @@ ui:
range: Display name up to 35 characters.
slug_name:
label: URL Slug
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
desc: URL slug fino a 35 caratteri.
msg:
empty: URL slug cannot be empty.
range: URL slug up to 35 characters.
@ -759,6 +767,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -824,6 +835,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1385,6 +1397,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -13,15 +13,23 @@ backend:
other: データサーバーエラー
action:
report:
other: Flag
other: フラグ
edit:
other: Edit
other: 編集
delete:
other: Delete
other: 削除
close:
other: Close
other: 閉じる
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -53,7 +61,7 @@ backend:
other: メールアドレスとパスワードが一致しません。
answer:
not_found:
other: Answer do not found.
other: 回答が見つかりません。
cannot_deleted:
other: 削除する権限がありません。
cannot_update:
@ -139,7 +147,7 @@ backend:
other: The From Name cannot be a email address.
theme:
not_found:
other: Theme not found.
other: テーマが見つかりません。
revision:
review_underway:
other: Can't edit currently, there is a version in the review queue.
@ -148,9 +156,9 @@ backend:
user:
email_or_password_wrong:
other:
other: Email and password do not match.
other: メールアドレスとパスワードが一致しません。
not_found:
other: User not found.
other: ユーザーが見つかりません。
suspended:
other: User has been suspended.
username_invalid:
@ -170,7 +178,7 @@ backend:
connection_failed:
other: Database connection failed
create_table_failed:
other: Create table failed
other: テーブルの作成に失敗しました
install:
create_config_failed:
other: Can't create the config.yaml file.
@ -180,7 +188,7 @@ backend:
report:
spam:
name:
other: spam
other: スパム
desc:
other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.
rude:
@ -212,7 +220,7 @@ backend:
close:
duplicate:
name:
other: spam
other: スパム
desc:
other: This question has been asked before and already has an answer.
guideline:
@ -232,11 +240,11 @@ backend:
other: This post requires another reason not listed above.
operation_type:
asked:
other: asked
other: 質問済み
answered:
other: answered
other: 回答済み
modified:
other: modified
other: 修正済み
notification:
action:
update_question:
@ -270,48 +278,48 @@ ui:
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>
pagination:
prev: Prev
next: Next
prev: 前へ
next: 次へ
page_title:
question: Question
questions: Questions
tag: Tag
tags: Tags
question: 質問
questions: 質問
tag: タグ
tags: タグ
tag_wiki: tag wiki
create_tag: Create Tag
edit_tag: Edit Tag
ask_a_question: Add Question
edit_question: Edit Question
edit_answer: Edit Answer
search: Search
create_tag: タグを作成
edit_tag: タグを編集
ask_a_question: 質問を追加
edit_question: 質問を編集
edit_answer: 回答を編集 
search: 検索
posts_containing: Posts containing
settings: Settings
settings: 設定
notifications: Notifications
login: Log In
sign_up: Sign Up
account_recovery: Account Recovery
login: ログイン
sign_up: 新規登録
account_recovery: アカウントの復旧
account_activation: Account Activation
confirm_email: Confirm Email
confirm_email: メールアドレスを確認
account_suspended: Account Suspended
admin: Admin
change_email: Modify Email
admin: 管理者
change_email: メールアドレスを変更
install: Answer Installation
upgrade: Answer Upgrade
maintenance: Website Maintenance
users: Users
http_404: HTTP Error 404
http_50X: HTTP Error 500
http_403: HTTP Error 403
users: ユーザー
http_404: HTTP エラー 404
http_50X: HTTP エラー 500
http_403: HTTP エラー 403
notifications:
title: Notifications
inbox: Inbox
title: 通知
inbox: 受信トレイ
achievement: Achievements
all_read: Mark all as read
show_more: Show more
show_more: もっと見る
suspended:
title: Your Account has been Suspended
until_time: "Your account was suspended until {{ time }}."
forever: This user was suspended forever.
title: あなたのアカウントは停止されています。
until_time: "あなたのアカウントは {{ time }} まで停止されました。"
forever: このユーザーは永久に停止されました。
end: You don't meet a community guideline.
editor:
blockquote:
@ -401,83 +409,83 @@ ui:
url:
label: URL
msg:
empty: URL cannot be empty.
empty: URLを入力してください。
name:
label: Description
btn_cancel: Cancel
btn_confirm: Add
label: 説明
btn_cancel: キャンセル
btn_confirm: 追加
ordered_list:
text: Numbered List
text: 番号付きリスト
unordered_list:
text: Bulleted List
text: 箇条書きリスト
table:
text: Table
heading: Heading
cell: Cell
text: ' テーブル'
heading: 見出し
cell: セル
close_modal:
title: I am closing this post as...
btn_cancel: Cancel
btn_submit: Submit
btn_cancel: キャンセル
btn_submit: 送信
remark:
empty: Cannot be empty.
empty: 入力してください。
msg:
empty: Please select a reason.
empty: 理由を選んでください。
report_modal:
flag_title: I am flagging to report this post as...
close_title: I am closing this post as...
review_question_title: Review question
review_answer_title: Review answer
review_comment_title: Review comment
btn_cancel: Cancel
btn_submit: Submit
btn_cancel: キャンセル
btn_submit: 送信
remark:
empty: Cannot be empty.
empty: 入力してください。
msg:
empty: Please select a reason.
empty: 理由を選んでください。
tag_modal:
title: Create new tag
title: 新しいタグを作成
form:
fields:
display_name:
label: Display Name
label: 表示名
msg:
empty: Display name cannot be empty.
range: Display name up to 35 characters.
empty: 表示名を入力してください。
range: 表示名は最大 35 文字までです。
slug_name:
label: URL Slug
label: URLスラッグ
desc: '文字セット「a-z」、「0-9」、「+ # -」を使用する必要があります。'
msg:
empty: URL slug cannot be empty.
empty: URL スラッグを空にすることはできません。
range: URL slug up to 35 characters.
character: URL slug contains unallowed character set.
desc:
label: Description
btn_cancel: Cancel
btn_submit: Submit
btn_post: Post new tag
label: 説明
btn_cancel: キャンセル
btn_submit: 送信
btn_post: 新しいタグを投稿
tag_info:
created_at: Created
edited_at: Edited
history: History
created_at: 作成
edited_at: 編集済
history: 履歴
synonyms:
title: Synonyms
title: 類義語
text: The following tags will be remapped to
empty: No synonyms found.
btn_add: Add a synonym
btn_edit: Edit
btn_save: Save
btn_edit: 編集
btn_save: 保存
synonyms_text: The following tags will be remapped to
delete:
title: Delete this tag
title: このタグを削除
tip_with_posts: >-
<p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
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: 閉じる
edit_tag:
title: Edit Tag
default_reason: Edit tag
title: タグを編集
default_reason: タグを編集
form:
fields:
revision:
@ -621,43 +629,43 @@ ui:
another: >-
We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.
btn_name: Resend activation email
change_btn_name: Change email
change_btn_name: メールアドレスを変更
msg:
empty: Cannot be empty.
login:
page_title: Welcome to {{site_name}}
page_title: '{{site_name}} へようこそ'
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
forgot_pass: Forgot password?
forgot_pass: パスワードをお忘れですか?
name:
label: Name
msg:
empty: Name cannot be empty.
range: Name up to 30 characters.
email:
label: Email
label: メールアドレス
msg:
empty: Email cannot be empty.
empty: メールアドレスを入力してください。
password:
label: Password
label: パスワード
msg:
empty: Password cannot be empty.
empty: パスワードを入力してください。
different: The passwords entered on both sides are inconsistent
account_forgot:
page_title: Forgot Your Password
page_title: パスワードを忘れた方はこちら
btn_name: Send me recovery email
send_success: >-
If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.
email:
label: Email
label: メールアドレス
msg:
empty: Email cannot be empty.
empty: メールアドレスを入力してください。
change_email:
page_title: Welcome to {{site_name}}
btn_cancel: Cancel
btn_update: Update email address
page_title: '{{site_name}} へようこそ'
btn_cancel: キャンセル
btn_update: メールアドレスを更新
send_success: >-
If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.
email:
@ -665,17 +673,17 @@ ui:
msg:
empty: Email cannot be empty.
password_reset:
page_title: Password Reset
btn_name: Reset my password
page_title: パスワード再設定
btn_name: パスワードをリセット
reset_success: >-
You successfully changed your password; you will be redirected to the log in page.
link_invalid: >-
Sorry, this password reset link is no longer valid. Perhaps your password is already reset?
to_login: Continue to log in page
password:
label: Password
label: パスワード
msg:
empty: Password cannot be empty.
empty: パスワードを入力してください。
length: The length needs to be between 8 and 32
different: The passwords entered on both sides are inconsistent
password_confirm:
@ -712,8 +720,8 @@ ui:
bio:
label: About Me
website:
label: Website
placeholder: "https://example.com"
label: ウェブサイト
placeholder: "http://example.com"
msg: Website incorrect format
location:
label: Location
@ -740,7 +748,7 @@ ui:
length: The length needs to be between 8 and 32.
different: The two entered passwords do not match.
new_pass:
label: New Password
label: 新しいパスワード
pass_confirm:
label: Confirm New Password
interface:
@ -759,6 +767,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -775,10 +784,10 @@ ui:
answer_useful: It is useful
answer_un_useful: It is not useful
answers:
title: Answers
score: Score
newest: Newest
btn_accept: Accept
title: 回答
score: スコア
newest: 最新
btn_accept: 承認
btn_accepted: Accepted
write_answer:
title: Your Answer
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -816,7 +827,7 @@ ui:
save: Save
delete: Delete
login: Log in
signup: Sign up
signup: 新規登録
logout: Log out
verify: Verify
add_question: Add question
@ -824,6 +835,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -848,17 +860,17 @@ ui:
is_answer: "<1>is:answer</1> search answers"
empty: We couldn't find anything. <br /> Try different or less specific keywords.
share:
name: Share
copy: Copy link
name: シェア
copy: リンクをコピー
via: Share post via...
copied: Copied
facebook: Share to Facebook
twitter: Share to Twitter
facebook: Facebookで共有
twitter: Twitterで共有
cannot_vote_for_self: You can't vote for your own post
modal_confirm:
title: Error...
account_result:
page_title: Welcome to {{site_name}}
page_title: '{{site_name}} へようこそ'
success: Your new account is confirmed; you will be redirected to the home page.
link: Continue to homepage
invalid: >-
@ -880,24 +892,24 @@ ui:
all_questions: All Questions
x_questions: "{{ count }} Questions"
x_answers: "{{ count }} answers"
questions: Questions
answers: Answers
newest: Newest
active: Active
frequent: Frequent
score: Score
unanswered: Unanswered
modified: modified
answered: answered
asked: asked
closed: closed
follow_a_tag: Follow a tag
more: More
questions: 質問
answers: 回答
newest: 最新
active: 有効
frequent: 頻繁
score: スコア
unanswered: 未回答
modified: 修正済み
answered: 回答済み
asked: 質問済み
closed: 終了
follow_a_tag: タグをフォロー
more: その他
personal:
overview: Overview
answers: Answers
answer: answer
questions: Questions
overview: 概要
answers: 回答
answer: 回答
questions: 質問
question: question
bookmarks: Bookmarks
reputation: Reputation
@ -918,32 +930,32 @@ ui:
list_empty: No posts found.<br />Perhaps you'd like to select a different tab?
accepted: Accepted
answered: answered
asked: asked
upvote: upvote
downvote: downvote
asked: 質問済み
upvote: 賛成
downvote: 反対票を投じる
mod_short: Mod
mod_long: Moderators
mod_long: モデレーター
x_reputation: reputation
x_votes: votes received
x_answers: answers
x_questions: questions
x_answers: 回答
x_questions: 質問
install:
title: Answer
next: Next
done: Done
title: 回答
next: 次へ
done: 完了
config_yaml_error: Can't create the config.yaml file.
lang:
label: Please Choose a Language
db_type:
label: Database Engine
db_username:
label: Username
label: ユーザー名
placeholder: root
msg: Username cannot be empty.
db_password:
label: Password
label: パスワード
placeholder: root
msg: Password cannot be empty.
msg: パスワードを入力してください。
db_host:
label: Database Host
placeholder: "db:3306"
@ -963,12 +975,12 @@ ui:
You can create the <1>config.yaml</1> file manually in the <1>/var/wwww/xxx/</1> directory and paste the following text into it.
info: After you've done that, click "Next" button.
site_information: Site Information
admin_account: Admin Account
admin_account: 管理者アカウント
site_name:
label: Site Name
msg: Site Name cannot be empty.
label: サイト名
msg: サイト名を入力してください。
site_url:
label: Site URL
label: サイトURL
text: The address of your site.
msg:
empty: Site URL cannot be empty.
@ -983,12 +995,12 @@ ui:
label: Name
msg: Name cannot be empty.
admin_password:
label: Password
label: パスワード
text: >-
You will need this password to log in. Please store it in a secure location.
msg: Password cannot be empty.
admin_email:
label: Email
label: メールアドレス
text: You will need this email to log in.
msg:
empty: Email cannot be empty.
@ -1026,10 +1038,10 @@ ui:
questions: Questions
answers: Answers
users: Users
flags: Flags
settings: Settings
general: General
interface: Interface
flags: フラグ
settings: 設定
general: 一般
interface: 外観
smtp: SMTP
branding: Branding
legal: Legal
@ -1037,21 +1049,21 @@ ui:
tos: Terms of Service
privacy: Privacy
seo: SEO
customize: Customize
themes: Themes
customize: カスタマイズ
themes: テーマ
css-html: CSS/HTML
login: Login
login: ログイン
admin:
admin_header:
title: Admin
title: 管理者
dashboard:
title: Dashboard
title: ダッシュボード
welcome: Welcome to Answer Admin!
site_statistics: Site Statistics
questions: "Questions:"
answers: "Answers:"
comments: "Comments:"
votes: "Votes:"
site_statistics: サイト統計
questions: "質問:"
answers: "回答:"
comments: "評論:"
votes: "投票:"
active_users: "Active users:"
flags: "Flags:"
site_health_status: Site Health Status
@ -1065,15 +1077,15 @@ ui:
uptime: "Uptime:"
answer_links: Answer Links
documents: Documents
feedback: Feedback
support: Support
review: Review
config: Config
feedback: フィードバック
support: サポート
review: レビュー
config: 設定
update_to: Update to
latest: Latest
check_failed: Check failed
"yes": "Yes"
"no": "No"
"yes": "はい"
"no": "いいえ"
not_allowed: Not allowed
allowed: Allowed
enabled: Enabled
@ -1121,7 +1133,7 @@ ui:
users:
title: Users
name: Name
email: Email
email: メールアドレス
reputation: Reputation
created_at: Created Time
delete_at: Deleted Time
@ -1136,51 +1148,51 @@ ui:
suspended: Suspended
deleted: Deleted
normal: Normal
Moderator: Moderator
Admin: Admin
User: User
Moderator: モデレーター
Admin: 管理者
User: ユーザー
filter:
placeholder: "Filter by name, user:id"
set_new_password: Set new password
change_status: Change status
change_role: Change role
show_logs: Show logs
add_user: Add user
set_new_password: 新しいパスワードを設定します。
change_status: ステータスを変更
change_role: ロールを変更
show_logs: ログを表示
add_user: ユーザを追加
new_password_modal:
title: Set new password
title: 新しいパスワードを設定
form:
fields:
password:
label: Password
label: パスワード
text: The user will be logged out and need to login again.
msg: Password must be at 8-32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
btn_cancel: キャンセル
btn_submit: 送信
user_modal:
title: Add new user
title: 新しいユーザーを追加
form:
fields:
display_name:
label: Display Name
msg: Display Name must be at 3-30 characters in length.
label: 表示名
msg: 表示名の長さは 3 30 文字にする必要があります。
email:
label: Email
msg: Email is not valid.
label: メールアドレス
msg: メールアドレスが無効です。
password:
label: Password
msg: Password must be at 8-32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
label: パスワード
msg: パスワードの長さは 8 32 文字である必要があります。
btn_cancel: キャンセル
btn_submit: 送信
questions:
page_title: Questions
normal: Normal
closed: Closed
deleted: Deleted
post: Post
votes: Votes
answers: Answers
created: Created
status: Status
page_title: 質問
normal: 正常
closed: 終了
deleted: 削除済み
post: 投稿
votes: 投票
answers: 回答
created: 作成
status: ステータス
action: Action
change: Change
filter:
@ -1204,7 +1216,7 @@ ui:
msg: Site name cannot be empty.
text: "The name of this site, as used in the title tag."
site_url:
label: Site URL
label: サイトURL
msg: Site url cannot be empty.
validate: Please enter a valid URL.
text: The address of your site.
@ -1253,17 +1265,17 @@ ui:
msg: Encryption cannot be empty.
text: For most servers SSL is the recommended option.
ssl: SSL
none: None
none: なし
smtp_port:
label: SMTP Port
msg: SMTP port must be number 1 ~ 65535.
label: SMTP ポート
msg: SMTPポートは1〜65535でなければなりません。
text: The port to your mail server.
smtp_username:
label: SMTP Username
msg: SMTP username cannot be empty.
label: SMTP ユーザー名
msg: SMTP ユーザー名を空にすることはできません。
smtp_password:
label: SMTP Password
msg: SMTP password cannot be empty.
label: SMTP パスワード
msg: SMTP パスワードを入力してください。
test_email_recipient:
label: Test Email Recipients
text: Provide email address that will receive test sends.
@ -1272,8 +1284,8 @@ ui:
label: Enable authentication
title: SMTP Authentication
msg: SMTP authentication cannot be empty.
"yes": "Yes"
"no": "No"
"yes": "はい"
"no": "いいえ"
branding:
page_title: Branding
logo:
@ -1357,7 +1369,7 @@ ui:
optional: (optional)
empty: cannot be empty
invalid: is invalid
btn_submit: Save
btn_submit: 保存
not_found_props: "Required property {{ key }} not found."
page_review:
review: Review
@ -1385,6 +1397,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -759,6 +767,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -824,6 +835,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1385,6 +1397,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -759,6 +767,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -824,6 +835,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1385,6 +1397,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -759,6 +767,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -824,6 +835,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1385,6 +1397,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -34,9 +42,9 @@ backend:
user:
other: Predvolené bez špeciálneho prístupu.
admin:
other: Má plnú moc a prístup k stránke.
other: Má plnú moc a prístup ku stránke.
moderator:
other: Má prístup ku všetkým príspevkom okrem nastavení správcu.
other: Má prístup ku všetkým príspevkom okrem nastavenia správcu.
email:
other: E-mail
password:
@ -46,7 +54,7 @@ backend:
error:
admin:
cannot_update_their_password:
other: Svoje heslo nemôžete upraviť.
other: Svoje heslo upraviť.
cannot_modify_self_status:
other: Nemôžete upraviť svoj stav.
email_or_password_wrong:
@ -116,10 +124,10 @@ backend:
handle_failed:
other: Spracovanie prehľadu zlyhalo.
not_found:
other: Prehľad sa nenašiel.
other: Hlásenie sa nenašlo.
tag:
already_exist:
other: Tento tag už existuje.
other: Značka už existuje.
not_found:
other: Značka sa nenašla.
recommend_tag_not_found:
@ -131,7 +139,7 @@ backend:
cannot_update:
other: Žiadne povolenie na aktualizáciu.
is_used_cannot_delete:
other: Nemôžete odstrániť značku, ktorá sa používa
other: Zadajte e-mailovú adresu, na ktorú sa budú odosielať testy
cannot_set_synonym_as_itself:
other: Synonymum aktuálnej značky nemôžete nastaviť ako samotnú.
smtp:
@ -268,7 +276,7 @@ ui:
how_to_format:
title: Ako formátovať
desc: >-
<ul class="mb-0"><li><p class="mb-2">Vytvorenie odkazov</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">vložiť návratky medzi odseky</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">odsadenie kódu o 4 medzery</p></li><li><p class="mb-2">citovať podľa umiestnenie <code>&gt;</code> na začiatok riadku</p></li><li><p class="mb-2">backtick označenie <code>`ako _this_`</code></p></li><li><p class="mb-2">vytvorte kódové ploty s <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">na vytváranie odkazov</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">vracať medzi odsekmi</p></li><li><p class="mb-2"><em>_italic_</em> alebo **<strong>tučné</strong>**</p></li><li><p class="mb-2">odsadenie kódu o 4 medzery</p></li><li><p class="mb-2">citát umiestnením <code>&gt;</code> na začiatku riadku</p></li><li><p class="mb-2">backtick uniká <code>`ako _this_`</code></p></li><li><p class="mb-2">vytvorte kódové ploty s <code>`</code></p><pre class="mb-0"><code>```<br/>kódom tu<br/>```</code></pre></li></ul>
pagination:
prev: Predch
next: Ďalšie
@ -295,7 +303,7 @@ ui:
account_suspended: Účet pozastavený
admin: Administrátor
change_email: Upraviť e-mail
install: Answer Inštalácia
install: Odpoveď Inštalácia
upgrade: Answer Upgrade
maintenance: Údržba webových stránok
users: Užívatelia
@ -386,7 +394,7 @@ ui:
label: Description
btn_cancel: Zrušiť
btn_confirm: Pridať
uploading: Nahrávanie
uploading: Nahráva sa
indent:
text: Indent
outdent:
@ -399,7 +407,7 @@ ui:
form:
fields:
url:
label: URL adresa
label: URL
msg:
empty: URL adresa nemôže byť prázdna.
name:
@ -423,7 +431,7 @@ ui:
msg:
empty: Vyberte dôvod.
report_modal:
flag_title: Označujem, aby som tento príspevok nahlásil ako ...
flag_title: Nahlasujem nahlásenie tohto príspevku ako...
close_title: Tento príspevok zatváram ako ...
review_question_title: Kontrola otázky
review_answer_title: Kontrola odpovede
@ -445,7 +453,7 @@ ui:
range: Zobrazovaný názov do 35 znakov.
slug_name:
label: URL Slug
desc: 'Musíte použiť znakovú sadu "a-z", "0-9", "+ # - ."'
desc: URL slug do 35 znakov.
msg:
empty: URL slug nemôže byť prázdny.
range: URL slug do 35 znakov.
@ -546,7 +554,7 @@ ui:
button_follow: Sledovať
button_following: Sledované
tag_label: otázky
search_placeholder: Filter podľa názvu značky
search_placeholder: Filtrujte podľa názvu značky
no_desc: Značka nemá popis.
more: Viac
ask:
@ -560,10 +568,10 @@ ui:
label: Revízia
title:
label: Názov
placeholder: Buďte konkrétny a predstavte si, že kladiete otázku inej osobe
placeholder: Buďte konkrétni a predstavte si, že kladiete otázku inej osobe
msg:
empty: Názov nemôže byť prázdny.
range: Názov má viac ako 150 znakov --
range: Názov do 150 znakov
body:
label: Telo
msg:
@ -571,7 +579,7 @@ ui:
tags:
label: Značky --
msg:
empty: Značky nemôžu byť prázdne.
empty: Štítky nemôžu byť prázdne.
answer:
label: Odpoveď
msg:
@ -580,17 +588,17 @@ ui:
label: Upraviť zhrnutie
placeholder: >-
Stručne vysvetlite svoje zmeny (opravený pravopis, opravená gramatika, vylepšené formátovanie)
btn_post_question: Pošlite svoju otázku
btn_post_question: Uverejnite svoju otázku
btn_save_edits: Uložiť úpravy
answer_question: Odpovedzte na svoju vlastnú otázku
post_question&answer: Pošlite svoju otázku a odpoveď
post_question&answer: Uverejnite svoju otázku a odpoveď
tag_selector:
add_btn: Pridať značku
create_btn: Vytvoriť novú značku
search_tag: Vyhľadať značku --
hint: "„Popíšte, o čom je vaša otázka, vyžaduje sa aspoň jedna značka.“"
no_result: Žiadne značky sa neyhodujú
tag_required_text: Požadovaná značka (aspoň jedna)
hint: "Popíšte, čoho sa vaša otázka týka, vyžaduje sa aspoň jedna značka."
no_result: Nezodpovedajú žiadne značky
tag_required_text: Povinný štítok (aspoň jeden)
header:
nav:
question: Otázky
@ -605,7 +613,7 @@ ui:
placeholder: Vyhľadávanie
footer:
build_on: >-
Postavený na <1> Answer </1>- open-source software, ktorý poháňa Q&A komunity.<br />Vyrobené s láskou © {{cc}}.
Postavené na <1> Answer </1> softvéri s otvoreným zdrojom, ktorý poháňa komunity otázok a odpovedí.<br />Vyrobené s láskou © {{cc}}.
upload_img:
name: Zmena
loading: načítavanie...
@ -696,17 +704,17 @@ ui:
msg_range: Zobrazované meno nesmie mať viac ako 30 znakov.
username:
label: Užívateľské meno
caption: Ľudia vás môžu spomenúť ako "@username".
caption: Ľudia vás môžu spomenúť ako „@používateľské meno“.
msg: Užívateľské meno nemôže byť prázdne.
msg_range: Používateľské meno nesmie mať viac ako 30 znakov.
character: 'Musí použiť súpravu znakov "a-z", "0-9", " - . _"'
character: 'Musíte použiť znakovú sadu "a-z", "0-9", "- . _"'
avatar:
label: Profilová fotka
gravatar: Gravatar
gravatar_text: Fotku môžete zmeniť na <1>gravatar.com</1>
gravatar_text: Obrázok môžete zmeniť na <1>gravatar.com</1>
custom: Vlastný
btn_refresh: Obnoviť
custom_text: Môžete nahrať svoju fotku.
custom_text: Môžete nahrať svoj obrázok.
default: Systém
msg: Nahrajte avatara prosím
bio:
@ -728,17 +736,17 @@ ui:
change_email_btn: Zmeniť e-mail
change_pass_btn: Zmeniť heslo
change_email_info: >-
Na túto adresu sme poslali e-mail. Postupujte podľa inštrukcií.
Na túto adresu sme poslali e-mail. Postupujte podľa pokynov na potvrdenie.
email:
label: E-mail
msg: E-mail nemôže byť prázdny.
password_title: Heslo
current_pass:
label: Súčasné heslo
label: Aktuálne heslo
msg:
empty: Aktuálne heslo nemôže byť prázdne.
length: Dĺžka musí byť medzi 8 a 32.
different: Zadané heslá sa nezhodujú.
different: Dve zadané heslá sa nezhodujú.
new_pass:
label: Nové heslo
pass_confirm:
@ -751,14 +759,15 @@ ui:
toast:
update: aktualizácia úspešna
update_password: Heslo bolo úspešne zmenené.
flag_success: Ďakujeme za označenie.
forbidden_operate_self: Zakázané pracovať na sebe
flag_success: Ďakujeme za nahlásenie.
forbidden_operate_self: Zakázané operovať seba
review: Vaša revízia sa zobrazí po preskúmaní.
related_question:
title: Súvisiace otázky
btn: Pridať otázku
answers: odpovede
question_detail:
action: Action
Asked: Opýtané
asked: opýtané
update: Aktualizované
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Znovu otvoriť tento príspevok
content: Ste si istý, že ho chcete znovu otvoriť?
success: Tento príspevok bol znovu otvorený
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Odstrániť tento príspevok
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>Neodporúčame <strong>odstránenie akceptovanej odpovede</strong> pretože týmto oberáte budúcich čitateľov o tieto vedomostí.</p>Opakované mazanie akceptovaných odpovedí môže mať za následok zablokovanie možnosti odpovedať z vášho účtu. Ste si istí, že chcete odstrániť odpoveď?
other: Ste si istí, že ju chcete odstrániť?
tip_question_deleted: Tento príspevok bol odstránený
tip_answer_deleted: Táto odpoveď bola odstránená
btns:
confirm: Potvrdiť
@ -824,6 +835,7 @@ ui:
reject: Odmietnuť
skip: Preskočiť
discard_draft: Zahodiť koncept
pinned: Pinned
search:
title: Výsledky vyhľadávania
keywords: Kľúčové slová
@ -840,7 +852,7 @@ ui:
more: Viac
tips:
title: Tipy na pokročilé vyhľadávanie
tag: "<1>[tag]</1> vyhľadávať v rámci značky"
tag: "<1>[tag]</1> hľadať v rámci značky"
user: "<1>user:username</1> hľadať podľa autora"
answer: "<1>answers:0</1> nezodpovedané otázky"
score: "<1>score:3</1> Príspevky so skóre 3+"
@ -891,7 +903,7 @@ ui:
answered: zodpovedané
asked: opýtané
closed: uzatvorené
follow_a_tag: Sledovať značku
follow_a_tag: Postupujte podľa značky
more: Viac
personal:
overview: Prehľad
@ -915,7 +927,7 @@ ui:
top_answers: Najlepšie odpovede
top_questions: Najlepšie otázky
stats: Štatistiky
list_empty: Nenašli sa žiadne príspevky.<br/>Chceli by ste vybrať inú kartu?
list_empty: Nenašli sa žiadne príspevky.<br />Možno by ste chceli vybrať inú kartu?
accepted: Prijaté
answered: zodpovedané
asked: opýtané
@ -924,13 +936,13 @@ ui:
mod_short: mod
mod_long: Moderátori
x_reputation: reputácia
x_votes: Získané hlasy
x_votes: prijatých hlasov
x_answers: odpovede
x_questions: otázky
install:
title: Odpoveď
next: Ďalšie
done: hotovo
done: Hotový
config_yaml_error: Nie je možné vytvoriť súbor config.yaml.
lang:
label: Vyberte jazyk
@ -953,14 +965,14 @@ ui:
placeholder: odpoveď
msg: Názov databázy nemôže byť prázdny.
db_file:
label: Databázový súbor
label: Súbor databázy
placeholder: /data/answer.db
msg: Databázový súbor nemôže byť prázdny.
config_yaml:
title: Vytvoriť config.yaml
label: Vytvorený súbor Config.yaml.
desc: >-
Môžete vytvoriť <1>config.yaml</1> súbor manuálne v <1>/var/wwww/xxx/</1> adresári a vložte do neho nasledujúci text.
Súbor <1>config.yaml</1> môžete vytvoriť manuálne v adresári <1>/var/www/xxx/</1> a vložiť doň nasledujúci text.
info: Potom, čo ste to urobili, kliknite na tlačidlo „Ďalej“.
site_information: Informácie o stránke
admin_account: Správca
@ -999,16 +1011,16 @@ ui:
good_luck: "„Bavte sa a veľa šťastia!“"
warn_title: Upozornenie
warn_desc: >-
Súbor <1>config.yaml</1> už existuje. Ak v tomto súbore potrebujete resetovať niektorú z konfiguračných položiek, najskôr ho odstráňte.
Súbor <1>config.yaml</1> už existuje. Ak potrebujete resetovať niektorú z konfiguračných položiek v tomto súbore, najskôr ju odstráňte.
install_now: Môžete skúsiť <1>installing now</1>.
installed: Už nainštalované
installed_desc: >-
Zdá sa, že ste už aplikáciu answer nainštalovali. Ak chcete aplikáciu preinštalovať, najprv vymažte staré tabuľky z databázy.
db_failed: Databázové pripojenie zlyhalo
db_failed_desc: >-
To buď znamená, že informácia o databáze v súbore <1>config.yaml</1> sú nesprávna alebo že sa nepodarilo nadviazať kontakt s databázovým serverom. To môže znamenať, že databázový server vášho hostiteľa nefunguje.
To buď znamená, že informácie o databáze vo vašom súbore <1>config.yaml</1> sú nesprávne, alebo že sa nepodarilo nadviazať kontakt s databázovým serverom. Môže to znamenať, že databázový server vášho hostiteľa nefunguje.
counts:
views: videnia
views: názory
votes: hlasy
answers: odpovede
accepted: prijaté
@ -1106,7 +1118,7 @@ ui:
status_modal:
title: "Zmena {{ type }} stav na..."
normal_name: normálne
normal_desc: Normálny príspevok je k dispozícii pre všetkých.
normal_desc: Normálny príspevok dostupný pre každého.
closed_name: Uzavreté
closed_desc: "Na uzavretú otázku nemôžete odpovedať, ale stále ju môžete upravovať, hlasovať a komentovať."
deleted_name: Vymazané
@ -1202,20 +1214,20 @@ ui:
name:
label: Názov stránky
msg: Názov stránky nemôže byť prázdny.
text: "Názov tejto stránky, ako sa používa v titulnej značke."
text: "Názov tejto lokality, ako sa používa v značke názvu."
site_url:
label: URL stránky
msg: URL stránky nemôže byť prázdne.
validate: Prosím vložte platnú URL.
msg: Adresa Url stránky nemôže byť prázdna.
validate: Prosím uveďte platnú webovú adresu.
text: Adresa vašej stránky.
short_desc:
label: Krátky popis stránky
msg: Krátky popis stránky nemôže byť prázdny.
text: "Krátky popis, ako sa používa v titulnej značke na domovskej stránke."
text: "Krátky popis, ako sa používa v značke názvu na domovskej stránke."
desc:
label: Popis stránky
msg: Popis stránky nemôže byť prázdny.
text: "Popíšte túto stránku jednou vetou, ako sa používa v značke meta description."
text: "Opíšte túto stránku jednou vetou, ako sa používa v značke meta description."
contact_email:
label: Kontaktný e-mail
msg: Kontaktný e-mail nemôže byť prázdny.
@ -1226,11 +1238,11 @@ ui:
language:
label: Jazyk rozhrania
msg: Jazyk rozhrania nemôže byť prázdny.
text: Jazyk používateľského rozhrania. Zmení sa po obnovení stránky.
text: Jazyk používateľského rozhrania. Zmení sa, keď stránku obnovíte.
time_zone:
label: Časové pásmo
msg: Časové pásmo nemôže byť prázdne.
text: Vyberte si mesto v rovnakom časovom pásme v akom ste vy.
text: Vyberte si mesto v rovnakom časovom pásme ako vy.
avatar:
label: Predvolený avatar
text: Pre používateľov bez vlastného avatara.
@ -1238,11 +1250,11 @@ ui:
page_title: SMTP
from_email:
label: E-mail odosielateľa
msg: E-mail odosielateľa nemôže byť prázdny.
msg: Z e-mailu nemôže byť prázdne.
text: E-mailová adresa, z ktorej sa odosielajú e-maily.
from_name:
label: Meno odosielateľa
msg: Meno odosielateľa nemôže byť prázdne.
msg: Názov od nemôže byť prázdny.
text: Meno, z ktorého sa odosielajú e-maily.
smtp_host:
label: Hostiteľ SMTP
@ -1256,22 +1268,22 @@ ui:
none: Žiadne
smtp_port:
label: SMTP Port
msg: Port SMTP musí byť číslo z intervalu 1 ~ 65535.
text: Port na váš e-mailový server.
msg: Port SMTP musí byť číslo 1 ~ 65535.
text: Port na váš poštový server.
smtp_username:
label: Používateľské meno SMTP
label: Uživatelské Meno SMTP
msg: Používateľské meno SMTP nemôže byť prázdne.
smtp_password:
label: Heslo SMTP
msg: Heslo SMTP nemôže byť prázdne.
test_email_recipient:
label: Prijemcovia testovacieho e-mailu
label: Testovať príjemcov e-mailov
text: Zadajte e-mailovú adresu, na ktorú sa budú odosielať testy.
msg: Príjemcovia testovacieho e-mailu sú neplatní
smtp_authentication:
label: Povoliť autentifikáciu
title: Autentifikácia SMTP
msg: Autentifikácia SMTP nemôže byť prázdna.
title: Overenie SMTP
msg: Overenie SMTP nemôže byť prázdne.
"yes": "Áno"
"no": "Nie"
branding:
@ -1279,7 +1291,7 @@ ui:
logo:
label: Logo
msg: Logo nemôže byť prázdne.
text: Obrázok loga v ľavej hornej časti vašej stránky. Použite široký obdĺžnikový obrázok s výškou 56 a pomerom strán väčším ako 3:1. Ak nezadáte nič, tak sa zobrazí text názvu stránky.
text: Obrázok loga v ľavej hornej časti vašej stránky. Použite široký obdĺžnikový obrázok s výškou 56 a pomerom strán väčším ako 3:1. Ak ho ponecháte prázdne, zobrazí sa text názvu stránky.
mobile_logo:
label: Logo mobilu
text: Logo použité na mobilnej verzii vášho webu. Použite široký obdĺžnikový obrázok s výškou 56. Ak pole ponecháte prázdne, použije sa obrázok z nastavenia „logo“.
@ -1289,7 +1301,7 @@ ui:
text: Obrázok použitý ako základ pre ikony metadát. V ideálnom prípade by mal byť väčšií ako 512 x 512.
favicon:
label: favicon
text: Favicon pre váš web. Ak chcete cez CDN fungovať správne, musí byť vo formáte png. Veľkosť sa zmení na 32 x 32. Ak nebude nič zadané, použije sa „štvorcová ikona“.
text: Favicon pre váš web. Ak chcete správne fungovať cez CDN, musí to byť png. Veľkosť sa zmení na 32 x 32. Ak zostane prázdne, použije sa „štvorcová ikona“.
legal:
page_title: Legálne
terms_of_service:
@ -1301,15 +1313,15 @@ ui:
write:
page_title: Písať
recommend_tags:
label: Odporúčané značky
label: Odporučiť značky
text: "Vyššie zadajte tag slug, jednu značku na riadok."
required_tag:
title: Požadovaná značka
title: Povinný štítok
label: Nastavte odporúčanú značku podľa potreby
text: "Každá nová otázka musí mať aspoň jedenu odporúčaciu značku."
reserved_tags:
label: Vyhradené značky
text: "Vyhradené značky môže k príspevku pridať iba moderátor"
text: "Vyhradené štítky môže k príspevku pridať iba moderátor."
seo:
page_title: SEO
permalink:
@ -1385,12 +1397,16 @@ ui:
closed: uzavreté
reopened: znovu otvorené
created: vytvorené
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "História pre"
tag_title: "Časová os pre"
show_votes: "Zobraziť hlasy"
n_or_a: N/A
title_for_question: "Časová os pre"
title_for_answer: "Časová os odpovede pre {{ title }} od {{ author }}"
title_for_answer: "Časová os odpovede na {{ title }} od {{ author }}"
title_for_tag: "Časová os pre značku"
datetime: Dátum a čas
type: Typ
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Naozaj chcete zahodiť svoj koncept?
messages:
post_deleted: Tento príspevok bol odstránený.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -759,6 +767,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -824,6 +835,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1385,6 +1397,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -759,6 +767,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -824,6 +835,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1385,6 +1397,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: 关闭
reopen:
other: 重新打开
pin:
other: 置顶
hide:
other: 隐藏
unpin:
other: 取消置顶
show:
other: 显示
role:
name:
user:
@ -37,6 +45,58 @@ backend:
other: 拥有管理网站的全部权限。
moderator:
other: 拥有访问除管理员设置以外的所有权限。
privilege:
level_1:
description:
other: 等级1创业社区所需的声望最低
level_2:
description:
other: 等级2创业社区所需的声望较低
level_3:
description:
other: 等级3成熟社区所需的声望较高
rank_question_add_label:
other: 提问
rank_answer_add_label:
other: 回答问题
rank_comment_add_label:
other: 发表评论
rank_report_add_label:
other: 举报
rank_comment_vote_up_label:
other: 评论点赞
rank_link_url_limit_label:
other: 一次发布超过两个链接
rank_question_vote_up_label:
other: 问题点赞
rank_answer_vote_up_label:
other: 答案点赞
rank_question_vote_down_label:
other: 问题点踩
rank_answer_vote_down_label:
other: 答案点踩
rank_tag_add_label:
other: 创建新标签
rank_tag_edit_label:
other: 编辑标签描述(需要审核)
rank_question_edit_label:
other: 编辑他人提问(需要审核)
rank_answer_edit_label:
other: 编辑他人回答(需要审核)
rank_question_edit_without_review_label:
other: 编辑他人提问(无需审核)
rank_answer_edit_without_review_label:
other: 编辑他人回答(无需审核)
rank_question_audit_label:
other: 审核问题编辑
rank_answer_audit_label:
other: 审核答案编辑
rank_tag_audit_label:
other: 审核标签编辑
rank_tag_edit_without_review_label:
other: 编辑标签描述(无需审核)
rank_tag_synonym_label:
other: 管理标签同义词
email:
other: 邮箱
password:
@ -74,6 +134,8 @@ backend:
other: 邮箱需要验证。
verify_url_expired:
other: 邮箱验证的网址已过期,请重新发送邮件。
illegal_email_domain_error:
other: 该域名的邮箱无法使用。请尝试更换其他邮箱。
lang:
not_found:
other: 语言未找到
@ -163,6 +225,10 @@ backend:
other: 您不能修改自己的角色。
not_allowed_registration:
other: 目前该站点未开放注册
access_denied:
other: 访问被拒绝
page_access_denied:
other: 你没有权限进入这个页面。
config:
read_config_failed:
other: 读取配置失败
@ -519,7 +585,7 @@ ui:
使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。
tip_answer: >-
使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。
tip_vote: It adds something useful to the post
tip_vote: 它给帖子添加了一些有用的内容
edit_answer:
title: 编辑回答
default_reason: 编辑回答
@ -760,6 +826,7 @@ ui:
btn: 我要提问
answers: 个回答
question_detail:
action: 操作
Asked: 提问于
asked: 提问于
update: 修改于
@ -770,11 +837,11 @@ ui:
answered: 回答于
closed_in: 关闭于
show_exist: 查看相关问题。
useful: Useful
question_useful: It is useful and clear
question_un_useful: It is unclear or not useful
answer_useful: It is useful
answer_un_useful: It is not useful
useful: 有用的
question_useful: 这是有用和清楚的
question_un_useful: 它不明确或没有用
answer_useful: 这是有用的
answer_un_useful: 它是没有用的
answers:
title: 个回答
score: 评分
@ -792,16 +859,19 @@ ui:
empty: 回答内容不能为空。
characters: 内容长度至少 6 个字符
tips:
header_1: Thanks for your answer
li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.
li1_2: Back up any statements you make with references or personal experience.
header_2: But <strong>avoid</strong> ...
li2_1: Asking for help, seeking clarification, or responding to other answers.
header_1: 感谢你的回答.
li1_1: 请务必 <strong>回答问题</strong>。提供详细信息并分享您的研究。
li1_2: 用参考资料或个人经历来支持你所做的任何陈述。
header_2: 但是 <strong>请避免</strong>...
li2_1: 请求帮助,寻求澄清,或答复其他答案。
reopen:
confirm_btn: 重新打开
title: 重新打开这个帖子
content: 确定要重新打开吗?
success: 这个帖子已被重新打开
pin:
title: 置顶此条帖子
content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。
confirm_btn: 置顶
delete:
title: 删除
question: >-
@ -809,7 +879,6 @@ ui:
answer_accepted: >-
<p>我们不建议<strong>删除被采纳的回答</strong>。因为这样做会使得后来的读者无法从该回答中获得帮助。</p>如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗?
other: 你确定要删除?
tip_question_deleted: 此问题已被删除
tip_answer_deleted: 此回答已被删除
btns:
confirm: 确认
@ -825,6 +894,7 @@ ui:
reject: 拒绝
skip: 略过
discard_draft: 丢弃草稿
pinned: 已置顶
search:
title: 搜索结果
keywords: 关键词
@ -1014,11 +1084,11 @@ ui:
answers: 个回答
accepted: 已被采纳
page_error:
http_error: HTTP Error {{ code }}
desc_403: You dont have permission to access this page.
desc_404: Unfortunately, this page doesn't exist.
desc_50X: The server encountered an error and could not complete your request.
back_home: Back to homepage
http_error: HTTP 错误 {{ code }}
desc_403: 您没有权限访问此页面。
desc_404: 很抱歉,此页面不存在。
desc_50X: 服务器遇到了一个错误,无法完成你的请求。
back_home: 返回首页
page_maintenance:
desc: "我们正在进行维护,我们将很快回来。"
nav_menus:
@ -1046,6 +1116,9 @@ ui:
installed_plugins: 插件列表
website_welcome: 欢迎来到 {{site_name}}
plugins:
login: 登录
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录
login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。
oauth:
connect: 连接到 {{ auth_name }}
remove: 解绑 {{ auth_name }}
@ -1410,6 +1483,10 @@ ui:
closed: 关闭
reopened: 重新开启
created: 创建于
pin: 已置顶
unpin: 取消置頂
show: 已显示
hide: 已隐藏
title: "历史记录"
tag_title: "时间线"
show_votes: "显示投票"
@ -1424,8 +1501,8 @@ ui:
no_data: "空空如也"
users:
title: 用户
users_with_the_most_reputation: Users with the highest reputation scores this week
users_with_the_most_vote: Users who voted the most this week
users_with_the_most_reputation: 本周声望最高的用户
users_with_the_most_vote: 本周投票最多的用户
staffs: 我们的社区工作人员
reputation: 声望值
votes: 投票
@ -1436,4 +1513,8 @@ ui:
discard_confirm: 您确定要丢弃您的草稿吗?
messages:
post_deleted: 该帖子已被删除。
post_pin: 该帖子已被置顶。
post_unpin: 该帖子已被取消置顶。
post_hide_list: 此帖子已经从列表中隐藏。
post_show_list: 该帖子已显示到列表中。
post_reopen: 这个帖子已被重新打开.

View File

@ -22,6 +22,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -759,6 +767,7 @@ ui:
btn: 發問
answers: 個回答
question_detail:
action: Action
Asked: 提問於
asked: 提問於
update: 修改於
@ -800,7 +809,10 @@ ui:
confirm_btn: Reopen
title: 重新打開這個貼文
content: 確定要重新打開嗎?
success: 這個貼文已被重新打開
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: 刪除此貼
question: >-
@ -808,7 +820,6 @@ ui:
answer_accepted: >-
<p>我們不建議<strong>刪除被採納的回答</strong>。因為這樣做會使得後來的讀者無法從該回答中獲得幫助。</p>如果刪除過多被採納的貼文,你的帳號將會被禁止回答任何提問。你確定要刪除嗎?
other: 你確定要刪除?
tip_question_deleted: 該帖子已被刪除。
tip_answer_deleted: 此回答已被刪除
btns:
confirm: 確認
@ -824,6 +835,7 @@ ui:
reject: 拒絕
skip: 略過
discard_draft: Discard draft
pinned: Pinned
search:
title: 搜尋結果
keywords: 關鍵詞
@ -1385,6 +1397,10 @@ ui:
closed: 關閉
reopened: 重新開啟
created: 創建於
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "歷史記錄"
tag_title: "時間線"
show_votes: "顯示投票"
@ -1411,4 +1427,8 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -14,6 +14,10 @@ const (
ActFollow = "follow"
ActAccepted = "accepted"
ActAccept = "accept"
ActPin = "pin"
ActUnPin = "unpin"
ActShow = "show"
ActHide = "hide"
)
const (
@ -29,6 +33,10 @@ const (
ActQuestionRollback ActivityTypeKey = "question.rollback"
ActQuestionDeleted ActivityTypeKey = "question.deleted"
ActQuestionUndeleted ActivityTypeKey = "question.undeleted"
ActQuestionPin ActivityTypeKey = "question.pin"
ActQuestionUnPin ActivityTypeKey = "question.unpin"
ActQuestionHide ActivityTypeKey = "question.hide"
ActQuestionShow ActivityTypeKey = "question.show"
)
const (

View File

@ -1,9 +1,11 @@
package constant
import "github.com/answerdev/answer/internal/base/reason"
type Privilege struct {
Key string `json:"key"`
Label string `json:"label"`
Value int `json:"value"`
Key string `json:"-"`
}
const (
@ -41,80 +43,28 @@ const (
RankTagUseReservedTagKey = "rank.tag.use_reserved_tag"
)
//| Permission | Level 1 | Level 2 | Level 3 | Custom Level |
//| -------------------------------------- | ------------------------------------------------ | --------------------------------------------- | --------------------------------------------- | ------------ |
//| Description | less reputation required for private team, group | low reputation required for startup community | high reputation required for mature community | |
//| Ask question | 1 | 1 | 1 | |
//| Write answer | 1 | 1 | 1 | |
//| Write comment | 1 | 1 | 1 | |
//| Accept answer | 1 | 1 | 1 | |
//| Flag | 1 | 1 | 1 | |
//| Upvote comment | 1 | 1 | 1 | |
//| Post more than 2 links at a time | 1 | 10 | 10 | |
//| Upvote question | 1 | 1 | 15 | |
//| Upvote answer | 1 | 1 | 15 | |
//| Downvote question | 125 | 125 | 125 | |
//| Downvote answer | 125 | 125 | 125 | |
//| Create new tag | 1 | 750 | 1500 | |
//| Edit tag description (need to review) | 1 | 50 | 100 | |
//| Edit other's question (need to review) | 1 | 100 | 200 | |
//| Edit other's answer (need to review) | 1 | 100 | 200 | |
//| Edit other's question without review | 1 | 1000 | 2000 | |
//| Edit other's answer without review | 1 | 1000 | 2000 | |
//| Revew question edits | 1 | 1000 | 2000 | |
//| Review answer edits | 1 | 1000 | 2000 | |
//| Review tag edits | 1 | 2500 | 5000 | |
//| Edit tag description without review | 1 | 10000 | 20000 | |
//| Manage tag synonyms | 1 | 10000 | 20000 | |
const (
RankQuestionAddLabel = "Ask question"
RankAnswerAddLabel = "Write answer"
RankCommentAddLabel = "Write comment"
RankAnswerAcceptLabel = "Accept answer"
RankReportAddLabel = "Flag"
RankCommentVoteUpLabel = "Upvote comment"
RankLinkUrlLimitLabel = "Post more than 2 links at a time"
RankQuestionVoteUpLabel = "Upvote question"
RankAnswerVoteUpLabel = "Upvote answer"
RankQuestionVoteDownLabel = "Downvote question"
RankAnswerVoteDownLabel = "Downvote answer"
RankTagAddLabel = "Create new tag"
RankTagEditLabel = "Edit tag description (need to review)"
RankQuestionEditLabel = "Edit other's question (need to review)"
RankAnswerEditLabel = "Edit other's answer (need to review)"
RankQuestionEditWithoutReviewLabel = "Edit other's question without review"
RankAnswerEditWithoutReviewLabel = "Edit other's answer without review"
RankQuestionAuditLabel = "Review question edits"
RankAnswerAuditLabel = "Review answer edits"
RankTagAuditLabel = "Review tag edits"
RankTagEditWithoutReviewLabel = "Edit tag description without review"
RankTagSynonymLabel = "Manage tag synonyms"
)
var (
RankAllPrivileges = []*Privilege{
{Label: RankQuestionAddLabel, Key: RankQuestionAddKey},
{Label: RankAnswerAddLabel, Key: RankAnswerAddKey},
{Label: RankCommentAddLabel, Key: RankCommentAddKey},
{Label: RankAnswerAcceptLabel, Key: RankAnswerAcceptKey},
{Label: RankReportAddLabel, Key: RankReportAddKey},
{Label: RankCommentVoteUpLabel, Key: RankCommentVoteUpKey},
{Label: RankLinkUrlLimitLabel, Key: RankLinkUrlLimitKey},
{Label: RankQuestionVoteUpLabel, Key: RankQuestionVoteUpKey},
{Label: RankAnswerVoteUpLabel, Key: RankAnswerVoteUpKey},
{Label: RankQuestionVoteDownLabel, Key: RankQuestionVoteDownKey},
{Label: RankAnswerVoteDownLabel, Key: RankAnswerVoteDownKey},
{Label: RankTagAddLabel, Key: RankTagAddKey},
{Label: RankTagEditLabel, Key: RankTagEditKey},
{Label: RankQuestionEditLabel, Key: RankQuestionEditKey},
{Label: RankAnswerEditLabel, Key: RankAnswerEditKey},
{Label: RankQuestionEditWithoutReviewLabel, Key: RankQuestionEditWithoutReviewKey},
{Label: RankAnswerEditWithoutReviewLabel, Key: RankAnswerEditWithoutReviewKey},
{Label: RankQuestionAuditLabel, Key: RankQuestionAuditKey},
{Label: RankAnswerAuditLabel, Key: RankAnswerAuditKey},
{Label: RankTagAuditLabel, Key: RankTagAuditKey},
{Label: RankTagEditWithoutReviewLabel, Key: RankTagEditWithoutReviewKey},
{Label: RankTagSynonymLabel, Key: RankTagSynonymKey},
{Label: reason.RankQuestionAddLabel, Key: RankQuestionAddKey},
{Label: reason.RankAnswerAddLabel, Key: RankAnswerAddKey},
{Label: reason.RankCommentAddLabel, Key: RankCommentAddKey},
{Label: reason.RankReportAddLabel, Key: RankReportAddKey},
{Label: reason.RankCommentVoteUpLabel, Key: RankCommentVoteUpKey},
{Label: reason.RankLinkUrlLimitLabel, Key: RankLinkUrlLimitKey},
{Label: reason.RankQuestionVoteUpLabel, Key: RankQuestionVoteUpKey},
{Label: reason.RankAnswerVoteUpLabel, Key: RankAnswerVoteUpKey},
{Label: reason.RankQuestionVoteDownLabel, Key: RankQuestionVoteDownKey},
{Label: reason.RankAnswerVoteDownLabel, Key: RankAnswerVoteDownKey},
{Label: reason.RankTagAddLabel, Key: RankTagAddKey},
{Label: reason.RankTagEditLabel, Key: RankTagEditKey},
{Label: reason.RankQuestionEditLabel, Key: RankQuestionEditKey},
{Label: reason.RankAnswerEditLabel, Key: RankAnswerEditKey},
{Label: reason.RankQuestionEditWithoutReviewLabel, Key: RankQuestionEditWithoutReviewKey},
{Label: reason.RankAnswerEditWithoutReviewLabel, Key: RankAnswerEditWithoutReviewKey},
{Label: reason.RankQuestionAuditLabel, Key: RankQuestionAuditKey},
{Label: reason.RankAnswerAuditLabel, Key: RankAnswerAuditKey},
{Label: reason.RankTagAuditLabel, Key: RankTagAuditKey},
{Label: reason.RankTagEditWithoutReviewLabel, Key: RankTagEditWithoutReviewKey},
{Label: reason.RankTagSynonymLabel, Key: RankTagSynonymKey},
}
)

View File

@ -8,9 +8,13 @@ import (
"github.com/segmentfault/pacman/errors"
)
// BanAPIWhenUserCenterEnabled ban api when user center enabled
func BanAPIWhenUserCenterEnabled(ctx *gin.Context) {
if plugin.UserCenterEnabled() {
// BanAPIForUserCenter ban api for user center
func BanAPIForUserCenter(ctx *gin.Context) {
uc, ok := plugin.GetUserCenter()
if !ok {
return
}
if !uc.Description().EnabledOriginalUserSystem {
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
ctx.Abort()
return

View File

@ -0,0 +1,29 @@
package reason
const (
PrivilegeLevel1Desc = "privilege.level_1.description"
PrivilegeLevel2Desc = "privilege.level_2.description"
PrivilegeLevel3Desc = "privilege.level_3.description"
RankQuestionAddLabel = "privilege.rank_question_add_label"
RankAnswerAddLabel = "privilege.rank_answer_add_label"
RankCommentAddLabel = "privilege.rank_comment_add_label"
RankReportAddLabel = "privilege.rank_report_add_label"
RankCommentVoteUpLabel = "privilege.rank_comment_vote_up_label"
RankLinkUrlLimitLabel = "privilege.rank_link_url_limit_label"
RankQuestionVoteUpLabel = "privilege.rank_question_vote_up_label"
RankAnswerVoteUpLabel = "privilege.rank_answer_vote_up_label"
RankQuestionVoteDownLabel = "privilege.rank_question_vote_down_label"
RankAnswerVoteDownLabel = "privilege.rank_answer_vote_down_label"
RankTagAddLabel = "privilege.rank_tag_add_label"
RankTagEditLabel = "privilege.rank_tag_edit_label"
RankQuestionEditLabel = "privilege.rank_question_edit_label"
RankAnswerEditLabel = "privilege.rank_answer_edit_label"
RankQuestionEditWithoutReviewLabel = "privilege.rank_question_edit_without_review_label"
RankAnswerEditWithoutReviewLabel = "privilege.rank_answer_edit_without_review_label"
RankQuestionAuditLabel = "privilege.rank_question_audit_label"
RankAnswerAuditLabel = "privilege.rank_answer_audit_label"
RankTagAuditLabel = "privilege.rank_tag_audit_label"
RankTagEditWithoutReviewLabel = "privilege.rank_tag_edit_without_review_label"
RankTagSynonymLabel = "privilege.rank_tag_synonym_label"
)

View File

@ -74,4 +74,6 @@ const (
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
UserExternalLoginUnbindingForbidden = "error.user.external_login_unbinding_forbidden"
UserAccessDenied = "error.user.access_denied"
UserPageAccessDenied = "error.user.page_access_denied"
)

View File

@ -68,6 +68,7 @@ func NewHTTPServer(debug bool,
// plugin routes
pluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1)
pluginAPIRouter.RegisterAuthUserConnectorRouter(authV1)
pluginAPIRouter.RegisterAuthAdminConnectorRouter(adminauthV1)
_ = plugin.CallAgent(func(agent plugin.Agent) error {
agent.RegisterUnAuthRouter(unAuthV1)

View File

@ -130,7 +130,7 @@ func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn
return
}
if len(resp.AccessToken) > 0 {
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/oauth?access_token=%s",
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
siteGeneral.SiteUrl, resp.AccessToken))
} else {
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/confirm-email?binding_key=%s",

View File

@ -3,8 +3,6 @@ package controller
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
@ -62,9 +60,11 @@ func (uc *UserCenterController) UserCenterAgent(ctx *gin.Context) {
_ = plugin.CallUserCenter(func(uc plugin.UserCenter) error {
info := uc.Description()
resp.AgentInfo.Name = info.Name
resp.AgentInfo.DisplayName = info.DisplayName.Translate(ctx)
resp.AgentInfo.Icon = info.Icon
resp.AgentInfo.Url = info.Url
resp.AgentInfo.ControlCenterItems = make([]*schema.ControlCenter, 0)
resp.AgentInfo.EnabledOriginalUserSystem = info.EnabledOriginalUserSystem
items := uc.ControlCenterItems()
for _, item := range items {
resp.AgentInfo.ControlCenterItems = append(resp.AgentInfo.ControlCenterItems, &schema.ControlCenter{
@ -126,7 +126,9 @@ func (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) {
userInfo, err := userCenter.LoginCallback(ctx)
if err != nil {
log.Error(err)
ctx.Redirect(http.StatusFound, "/50x")
if !ctx.IsAborted() {
ctx.Redirect(http.StatusFound, "/50x")
}
return
}
@ -137,11 +139,11 @@ func (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) {
return
}
if len(resp.ErrMsg) > 0 {
ctx.String(http.StatusOK, resp.ErrMsg)
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
return
}
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/oauth?access_token=%s",
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
siteGeneral.SiteUrl, resp.AccessToken))
}
@ -172,11 +174,11 @@ func (uc *UserCenterController) UserCenterSignUpCallback(ctx *gin.Context) {
return
}
if len(resp.ErrMsg) > 0 {
ctx.String(http.StatusOK, resp.ErrMsg)
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
return
}
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/oauth?access_token=%s",
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
siteGeneral.SiteUrl, resp.AccessToken))
}
@ -192,17 +194,3 @@ func (uc *UserCenterController) UserCenterAdminFunctionAgent(ctx *gin.Context) {
resp, err := uc.userCenterLoginService.UserCenterAdminFunctionAgent(ctx)
handler.HandleResponse(ctx, err, resp)
}
func (uc *UserCenterController) formatRedirectURL(ctx *gin.Context, redirectURL string) string {
if !strings.Contains(redirectURL, "CALLBACK_URL") {
return redirectURL
}
general, err := uc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Error(err)
ctx.Redirect(http.StatusFound, "/50x")
return ""
}
callbackURL := fmt.Sprintf("%s%s%s", general.SiteUrl, commonRouterPrefix, "/user-center/login/callback")
return strings.ReplaceAll(redirectURL, "CALLBACK_URL", url.QueryEscape(callbackURL))
}

View File

@ -70,6 +70,47 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// OperationQuestion Operation question
// @Summary Operation question
// @Description Operation question \n operation [pin unpin hide show]
// @Tags Question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.OperationQuestionReq true "question"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question/operation [put]
func (qc *QuestionController) OperationQuestion(ctx *gin.Context) {
req := &schema.OperationQuestionReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.ID = uid.DeShortID(req.ID)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.QuestionPin,
permission.QuestionUnPin,
permission.QuestionHide,
permission.QuestionShow,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanPin = canList[0]
req.CanList = canList[1]
if (req.Operation == schema.QuestionOperationPin || req.Operation == schema.QuestionOperationUnPin) && !req.CanPin {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
if (req.Operation == schema.QuestionOperationHide || req.Operation == schema.QuestionOperationShow) && !req.CanList {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err = qc.questionService.OperationQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// CloseQuestion Close question
// @Summary Close question
// @Description Close question
@ -152,6 +193,10 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
permission.QuestionDelete,
permission.QuestionClose,
permission.QuestionReopen,
permission.QuestionPin,
permission.QuestionUnPin,
permission.QuestionHide,
permission.QuestionShow,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
@ -163,6 +208,10 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
req.CanDelete = canList[1]
req.CanClose = canList[2]
req.CanReopen = canList[3]
req.CanPin = canList[4]
req.CanUnPin = canList[5]
req.CanHide = canList[6]
req.CanShow = canList[7]
info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req)
if err != nil {

View File

@ -32,7 +32,7 @@ func NewUserAdminController(userService *user_admin.UserAdminService) *UserAdmin
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/status [put]
func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) {
if plugin.UserCenterEnabled() {
if u, ok := plugin.GetUserCenter(); ok && u.Description().UserStatusAgentEnabled {
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
return
}

View File

@ -6,4 +6,5 @@ type UserCacheInfo struct {
UserStatus int `json:"user_status"`
EmailStatus int `json:"email_status"`
RoleID int `json:"role_id"`
ExternalID string `json:"external_id"`
}

View File

@ -8,6 +8,10 @@ const (
QuestionStatusAvailable = 1
QuestionStatusClosed = 2
QuestionStatusDeleted = 10
QuestionUnPin = 1
QuestionPin = 2
QuestionShow = 1
QuestionHide = 2
)
var AdminQuestionSearchStatus = map[string]int{
@ -32,6 +36,8 @@ type Question struct {
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Pin int `xorm:"not null default 1 INT(11) pin"`
Show int `xorm:"not null default 1 INT(11) show"`
Status int `xorm:"not null default 1 INT(11) status"`
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`

View File

@ -179,6 +179,25 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
if err != nil {
return err
}
usersData := map[string]any{
"default_avatar": "gravatar",
"allow_update_display_name": true,
"allow_update_username": true,
"allow_update_avatar": true,
"allow_update_bio": true,
"allow_update_website": true,
"allow_update_location": true,
}
usersDataBytes, _ := json.Marshal(usersData)
_, err = engine.InsertOne(&entity.SiteInfo{
Type: "users",
Content: string(usersDataBytes),
Status: 1,
})
if err != nil {
return err
}
return err
}
@ -272,7 +291,7 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 41, Key: "rank.answer.add", Value: `1`},
{ID: 42, Key: "rank.answer.edit", Value: `200`},
{ID: 43, Key: "rank.answer.delete", Value: `-1`},
{ID: 44, Key: "rank.answer.accept", Value: `1`},
{ID: 44, Key: "rank.answer.accept", Value: `-1`},
{ID: 45, Key: "rank.answer.vote_up", Value: `15`},
{ID: 46, Key: "rank.answer.vote_down", Value: `125`},
{ID: 47, Key: "rank.comment.add", Value: `1`},
@ -347,6 +366,10 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
{ID: 118, Key: "plugin.status", Value: `{}`},
{ID: 119, Key: "question.pin", Value: `-1`},
{ID: 120, Key: "question.unpin", Value: `-1`},
{ID: 121, Key: "question.show", Value: `-1`},
{ID: 122, Key: "question.hide", Value: `-1`},
}
_, err := engine.Insert(defaultConfigTable)
return err
@ -397,6 +420,10 @@ func initRolePower(engine *xorm.Engine) (err error) {
{ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
{ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
{ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
{ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"},
{ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"},
{ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"},
{ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"},
}
_, err = engine.Insert(powers)
if err != nil {
@ -438,6 +465,10 @@ func initRolePower(engine *xorm.Engine) (err error) {
{RoleID: 2, PowerType: permission.QuestionAudit},
{RoleID: 2, PowerType: permission.TagAudit},
{RoleID: 2, PowerType: permission.TagUseReservedTag},
{RoleID: 2, PowerType: permission.QuestionPin},
{RoleID: 2, PowerType: permission.QuestionHide},
{RoleID: 2, PowerType: permission.QuestionUnPin},
{RoleID: 2, PowerType: permission.QuestionShow},
{RoleID: 3, PowerType: permission.QuestionAdd},
{RoleID: 3, PowerType: permission.QuestionEdit},
@ -472,6 +503,10 @@ func initRolePower(engine *xorm.Engine) (err error) {
{RoleID: 3, PowerType: permission.QuestionAudit},
{RoleID: 3, PowerType: permission.TagAudit},
{RoleID: 3, PowerType: permission.TagUseReservedTag},
{RoleID: 3, PowerType: permission.QuestionPin},
{RoleID: 3, PowerType: permission.QuestionHide},
{RoleID: 3, PowerType: permission.QuestionUnPin},
{RoleID: 3, PowerType: permission.QuestionShow},
}
_, err = engine.Insert(rolePowerRels)
if err != nil {

View File

@ -56,6 +56,8 @@ var migrations = []Migration{
NewMigration("add user role", addRoleFeatures, false),
NewMigration("add theme and private mode", addThemeAndPrivateMode, true),
NewMigration("add new answer notification", addNewAnswerNotification, true),
NewMigration("add user pin hide features", addRolePinAndHideFeatures, true),
NewMigration("update accept answer rank", updateAcceptAnswerRank, true),
NewMigration("add plugin", addPlugin, false),
NewMigration("add login limitations", addLoginLimitations, true),
}

View File

@ -0,0 +1,69 @@
package migrations
import (
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/tidwall/gjson"
"xorm.io/xorm"
)
func addLoginLimitations(x *xorm.Engine) error {
loginSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeLogin,
}
exist, err := x.Get(loginSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
content := &schema.SiteLoginReq{}
_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)
content.AllowEmailRegistrations = true
content.AllowEmailDomains = make([]string, 0)
_, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
}
interfaceSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeInterface,
}
exist, err = x.Get(interfaceSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
siteUsers := &schema.SiteUsersReq{
AllowUpdateDisplayName: true,
AllowUpdateUsername: true,
AllowUpdateAvatar: true,
AllowUpdateBio: true,
AllowUpdateWebsite: true,
AllowUpdateLocation: true,
}
if exist {
siteUsers.DefaultAvatar = gjson.Get(interfaceSiteInfo.Content, "default_avatar").String()
}
data, _ := json.Marshal(siteUsers)
exist, err = x.Get(&entity.SiteInfo{Type: constant.SiteTypeUsers})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if !exist {
usersSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeUsers,
Content: string(data),
Status: 1,
}
_, err = x.InsertOne(usersSiteInfo)
if err != nil {
return fmt.Errorf("insert site info failed: %w", err)
}
}
return nil
}

View File

@ -110,7 +110,7 @@ ON "question" (
{ID: 41, Key: "rank.answer.add", Value: `1`},
{ID: 42, Key: "rank.answer.edit", Value: `200`},
{ID: 43, Key: "rank.answer.delete", Value: `-1`},
{ID: 44, Key: "rank.answer.accept", Value: `1`},
{ID: 44, Key: "rank.answer.accept", Value: `-1`},
{ID: 45, Key: "rank.answer.vote_up", Value: `15`},
{ID: 46, Key: "rank.answer.vote_down", Value: `125`},
{ID: 47, Key: "rank.comment.add", Value: `1`},

118
internal/migrations/v8.go Normal file
View File

@ -0,0 +1,118 @@
package migrations
import (
"fmt"
"time"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addRolePinAndHideFeatures(x *xorm.Engine) error {
powers := []*entity.Power{
{ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"},
{ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"},
{ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"},
{ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"},
}
// insert default powers
for _, power := range powers {
exist, err := x.Get(&entity.Power{ID: power.ID})
if err != nil {
return err
}
if exist {
_, err = x.ID(power.ID).Update(power)
} else {
_, err = x.Insert(power)
}
if err != nil {
return err
}
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.QuestionPin},
{RoleID: 2, PowerType: permission.QuestionHide},
{RoleID: 2, PowerType: permission.QuestionUnPin},
{RoleID: 2, PowerType: permission.QuestionShow},
{RoleID: 3, PowerType: permission.QuestionPin},
{RoleID: 3, PowerType: permission.QuestionHide},
{RoleID: 3, PowerType: permission.QuestionUnPin},
{RoleID: 3, PowerType: permission.QuestionShow},
}
// insert default powers
for _, rel := range rolePowerRels {
exist, err := x.Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})
if err != nil {
return err
}
if exist {
continue
}
_, err = x.Insert(rel)
if err != nil {
return err
}
}
defaultConfigTable := []*entity.Config{
{ID: 119, Key: "question.pin", Value: `-1`},
{ID: 120, Key: "question.unpin", Value: `-1`},
{ID: 121, Key: "question.show", Value: `-1`},
{ID: 122, Key: "question.hide", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
type Question struct {
ID string `xorm:"not null pk BIGINT(20) id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 INT(11) status"`
Pin int `xorm:"not null default 1 INT(11) pin"`
Show int `xorm:"not null default 1 INT(11) show"`
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
AnswerCount int `xorm:"not null default 0 INT(11) answer_count"`
CollectionCount int `xorm:"not null default 0 INT(11) collection_count"`
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"`
LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
err := x.Sync(new(Question))
if err != nil {
return err
}
return nil
}

View File

@ -1,31 +1,18 @@
package migrations
import (
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addLoginLimitations(x *xorm.Engine) error {
loginSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeLogin,
}
exist, err := x.Get(loginSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
content := &schema.SiteLoginReq{}
_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)
content.AllowEmailRegistrations = true
_, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
func updateAcceptAnswerRank(x *xorm.Engine) error {
c := &entity.Config{ID: 44, Key: "rank.answer.accept", Value: `-1`}
if _, err := x.Update(c, &entity.Config{ID: 44, Key: "rank.answer.accept"}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
return nil
}

View File

@ -125,6 +125,15 @@ func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Contex
return nil
}
func (qr *questionRepo) UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) {
question.ID = uid.DeShortID(question.ID)
_, err = qr.data.DB.Where("id =?", question.ID).Cols("pin", "show").Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) {
question.ID = uid.DeShortID(question.ID)
_, err = qr.data.DB.Where("id =?", question.ID).Cols("accepted_answer_id").Update(question)
@ -224,6 +233,7 @@ func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize i
offset := page * pageSize
session := qr.data.DB.Table("question")
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
session.And("question.show = ?", entity.QuestionShow)
session = session.Limit(pageSize, offset)
session = session.OrderBy("question.created_at asc")
err = session.Select("id,title,created_at,post_update_time").Find(&rows)
@ -258,19 +268,22 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
}
if len(userID) > 0 {
session.And("question.user_id = ?", userID)
} else {
session.And("question.show = ?", entity.QuestionShow)
}
switch orderCond {
case "newest":
session.OrderBy("question.created_at DESC")
session.OrderBy("question.pin desc,question.created_at DESC")
case "active":
session.OrderBy("question.post_update_time DESC, question.updated_at DESC")
session.OrderBy("question.pin desc,question.post_update_time DESC, question.updated_at DESC")
case "frequent":
session.OrderBy("question.view_count DESC")
session.OrderBy("question.pin desc,question.view_count DESC")
case "score":
session.OrderBy("question.vote_count DESC, question.view_count DESC")
session.OrderBy("question.pin desc,question.vote_count DESC, question.view_count DESC")
case "unanswered":
session.Where("question.last_answer_id = 0")
session.OrderBy("question.created_at DESC")
session.OrderBy("question.pin desc,question.created_at DESC")
}
total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session)

View File

@ -94,12 +94,14 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
ub = builder.MySQL().Select(afs...).From("`answer`").
LeftJoin("`question`", "`question`.id = `answer`.question_id")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted})
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Eq{"`question`.`show`": entity.QuestionShow})
ub.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted})
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).
And(builder.Eq{"`question`.`show`": entity.QuestionShow})
argsQ = append(argsQ, entity.QuestionStatusDeleted)
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
argsQ = append(argsQ, entity.QuestionStatusDeleted, entity.QuestionShow)
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
for i, word := range words {
if i == 0 {
@ -228,8 +230,8 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagID
b := builder.MySQL().Select(qfs...).From("question")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted})
args = append(args, entity.QuestionStatusDeleted)
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
args = append(args, entity.QuestionStatusDeleted, entity.QuestionShow)
for i, word := range words {
if i == 0 {
@ -343,8 +345,8 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs
LeftJoin("`question`", "`question`.id = `answer`.question_id")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted})
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
for i, word := range words {
if i == 0 {

View File

@ -290,8 +290,7 @@ func decorateByUserCenterUser(original *entity.User, ucUser *plugin.UserCenterBa
original.Mobile = ucUser.Mobile
}
if len(ucUser.Bio) > 0 {
original.Bio = ucUser.Bio
original.BioHTML = converter.Markdown2HTML(ucUser.Bio)
original.BioHTML = converter.Markdown2HTML(ucUser.Bio) + original.BioHTML
}
// If plugin enable rank agent, use rank from user center.

View File

@ -102,8 +102,8 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
// user
r.GET("/user/info", a.userController.GetUserInfoByUserID)
routerGroup := r.Group("", middleware.BanAPIWhenUserCenterEnabled)
r.POST("/user/login/email", a.userController.UserEmailLogin)
routerGroup := r.Group("", middleware.BanAPIForUserCenter)
routerGroup.POST("/user/login/email", a.userController.UserEmailLogin)
routerGroup.POST("/user/register/email", a.userController.UserRegisterByEmail)
routerGroup.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
routerGroup.POST("/user/email/verification", a.userController.UserVerifyEmail)
@ -117,8 +117,8 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// user
r.GET("/user/logout", a.userController.UserLogout)
r.POST("/user/email/change/code", middleware.BanAPIWhenUserCenterEnabled, a.userController.UserChangeEmailSendCode)
r.POST("/user/email/verification/send", middleware.BanAPIWhenUserCenterEnabled, a.userController.UserVerifyEmailSend)
r.POST("/user/email/change/code", middleware.BanAPIForUserCenter, a.userController.UserChangeEmailSendCode)
r.POST("/user/email/verification/send", middleware.BanAPIForUserCenter, a.userController.UserVerifyEmailSend)
r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername)
r.GET("/user/ranking", a.userController.UserRanking)
@ -195,6 +195,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.PUT("/question", a.questionController.UpdateQuestion)
r.DELETE("/question", a.questionController.RemoveQuestion)
r.PUT("/question/status", a.questionController.CloseQuestion)
r.PUT("/question/operation", a.questionController.OperationQuestion)
r.PUT("/question/reopen", a.questionController.ReopenQuestion)
r.GET("/question/similar", a.questionController.SearchByTitleLike)
@ -205,8 +206,8 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.DELETE("/answer", a.answerController.RemoveAnswer)
// user
r.PUT("/user/password", middleware.BanAPIWhenUserCenterEnabled, a.userController.UserModifyPassWord)
r.PUT("/user/info", middleware.BanAPIWhenUserCenterEnabled, a.userController.UserUpdateInfo)
r.PUT("/user/password", middleware.BanAPIForUserCenter, a.userController.UserModifyPassWord)
r.PUT("/user/info", a.userController.UserUpdateInfo)
r.PUT("/user/interface", a.userController.UserUpdateInterface)
r.POST("/user/notice/set", a.userController.UserNoticeSet)
@ -247,8 +248,8 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
r.GET("/users/page", a.adminUserController.GetUserPage)
r.PUT("/user/status", a.adminUserController.UpdateUserStatus)
r.PUT("/user/role", a.adminUserController.UpdateUserRole)
r.POST("/user", middleware.BanAPIWhenUserCenterEnabled, a.adminUserController.AddUser)
r.PUT("/user/password", middleware.BanAPIWhenUserCenterEnabled, a.adminUserController.UpdateUserPassword)
r.POST("/user", middleware.BanAPIForUserCenter, a.adminUserController.AddUser)
r.PUT("/user/password", middleware.BanAPIForUserCenter, a.adminUserController.UpdateUserPassword)
// reason
r.GET("/reasons", a.reasonController.Reasons)

View File

@ -56,10 +56,17 @@ func (g *GetPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.
UIOptions: ConfigFieldUIOptions{
Rows: field.UIOptions.Rows,
InputType: string(field.UIOptions.InputType),
Variant: field.UIOptions.Variant,
},
}
configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx)
configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx)
configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx)
if field.UIOptions.Action != nil {
configField.UIOptions.Action = &ConfigFieldUIOptionAction{
Url: field.UIOptions.Action.Url,
}
}
for _, option := range field.Options {
configField.Options = append(configField.Options, ConfigFieldOption{
@ -83,10 +90,13 @@ type ConfigField struct {
}
type ConfigFieldUIOptions struct {
Placeholder string `json:"placeholder,omitempty"`
Rows string `json:"rows,omitempty"`
InputType string `json:"input_type,omitempty"`
Label string `json:"label,omitempty"`
Placeholder string `json:"placeholder,omitempty"`
Rows string `json:"rows,omitempty"`
InputType string `json:"input_type,omitempty"`
Label string `json:"label,omitempty"`
Action *ConfigFieldUIOptionAction `json:"action,omitempty"`
Variant string `json:"variant,omitempty"`
Text string `json:"text,omitempty"`
}
type ConfigFieldOption struct {
@ -94,6 +104,10 @@ type ConfigFieldOption struct {
Value string `json:"value"`
}
type ConfigFieldUIOptionAction struct {
Url string `json:"url"`
}
type UpdatePluginConfigReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"`
ConfigFields map[string]any `json:"config_fields"`

View File

@ -6,12 +6,14 @@ type UserCenterAgentResp struct {
}
type AgentInfo struct {
Name string `json:"name"`
Icon string `json:"icon"`
Url string `json:"url"`
LoginRedirectURL string `json:"login_redirect_url"`
SignUpRedirectURL string `json:"sign_up_redirect_url"`
ControlCenterItems []*ControlCenter `json:"control_center"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
Url string `json:"url"`
LoginRedirectURL string `json:"login_redirect_url"`
SignUpRedirectURL string `json:"sign_up_redirect_url"`
ControlCenterItems []*ControlCenter `json:"control_center"`
EnabledOriginalUserSystem bool `json:"enabled_original_user_system"`
}
type ControlCenter struct {

View File

@ -8,9 +8,13 @@ import (
)
const (
SitemapMaxSize = 50000
SitemapCachekey = "answer@sitemap"
SitemapPageCachekey = "answer@sitemap@page%d"
SitemapMaxSize = 50000
SitemapCachekey = "answer@sitemap"
SitemapPageCachekey = "answer@sitemap@page%d"
QuestionOperationPin = "pin"
QuestionOperationUnPin = "unpin"
QuestionOperationHide = "hide"
QuestionOperationShow = "show"
)
// RemoveQuestionReq delete question request
@ -28,6 +32,14 @@ type CloseQuestionReq struct {
UserID string `json:"-"` // user_id
}
type OperationQuestionReq struct {
ID string `validate:"required" json:"id"`
Operation string `json:"operation"` // operation [pin unpin hide show]
UserID string `json:"-"` // user_id
CanPin bool `json:"-"`
CanList bool `json:"-"`
}
type CloseQuestionMeta struct {
CloseType int `json:"close_type"`
CloseMsg string `json:"close_msg"`
@ -101,6 +113,12 @@ type QuestionPermission struct {
CanClose bool `json:"-"`
// whether user can reopen it
CanReopen bool `json:"-"`
// whether user can pin it
CanPin bool `json:"-"`
CanUnPin bool `json:"-"`
// whether user can hide it
CanHide bool `json:"-"`
CanShow bool `json:"-"`
// whether user can use reserved it
CanUseReservedTag bool `json:"-"`
}
@ -168,6 +186,8 @@ type QuestionInfo struct {
UpdateTime int64 `json:"-"` // update_time
PostUpdateTime int64 `json:"update_time"`
QuestionUpdateTime int64 `json:"edit_time"`
Pin int `json:"pin"` // 1: unpin, 2: pin
Show int `json:"show"` // 0: show, 1: hide
Status int `json:"status"`
Operation *Operation `json:"operation,omitempty"`
UserID string `json:"-" `
@ -296,6 +316,8 @@ type QuestionPageResp struct {
Title string `json:"title"`
UrlTitle string `json:"url_title"`
Description string `json:"description"`
Pin int `json:"pin"` // 1: unpin, 2: pin
Show int `json:"show"` // 0: show, 1: hide
Status int `json:"status"`
Tags []*TagResp `json:"tags"`

View File

@ -43,9 +43,8 @@ func (r *SiteGeneralReq) FormatSiteUrl() {
// SiteInterfaceReq site interface request
type SiteInterfaceReq struct {
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
DefaultAvatar string `validate:"required,oneof=system gravatar" form:"default_avatar" json:"default_avatar"`
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
}
// SiteBrandingReq site branding request
@ -275,6 +274,7 @@ type GetPrivilegesConfigResp struct {
// PrivilegeOption privilege option
type PrivilegeOption struct {
Level PrivilegeLevel `json:"level"`
LevelDesc string `json:"level_desc"`
Privileges []*constant.Privilege `json:"privileges"`
}
@ -289,7 +289,6 @@ var (
constant.RankQuestionAddKey: {1, 1, 1},
constant.RankAnswerAddKey: {1, 1, 1},
constant.RankCommentAddKey: {1, 1, 1},
constant.RankAnswerAcceptKey: {1, 1, 1},
constant.RankReportAddKey: {1, 1, 1},
constant.RankCommentVoteUpKey: {1, 1, 1},
constant.RankLinkUrlLimitKey: {1, 10, 10},
@ -312,21 +311,27 @@ var (
)
func init() {
for _, option := range []PrivilegeLevel{PrivilegeLevel1, PrivilegeLevel2, PrivilegeLevel3} {
op := &PrivilegeOption{
Level: option,
}
DefaultPrivilegeOptions = append(DefaultPrivilegeOptions, &PrivilegeOption{
Level: PrivilegeLevel1,
LevelDesc: reason.PrivilegeLevel1Desc,
}, &PrivilegeOption{
Level: PrivilegeLevel2,
LevelDesc: reason.PrivilegeLevel2Desc,
}, &PrivilegeOption{
Level: PrivilegeLevel3,
LevelDesc: reason.PrivilegeLevel3Desc,
})
for _, option := range DefaultPrivilegeOptions {
for _, privilege := range constant.RankAllPrivileges {
if len(privilegeOptionsLevelMapping[privilege.Key]) == 0 {
fmt.Println("privilege key not found: ", privilege.Key)
continue
}
op.Privileges = append(op.Privileges, &constant.Privilege{
option.Privileges = append(option.Privileges, &constant.Privilege{
Label: privilege.Label,
Value: privilegeOptionsLevelMapping[privilege.Key][option-1],
Value: privilegeOptionsLevelMapping[privilege.Key][option.Level-1],
Key: privilege.Key,
})
}
DefaultPrivilegeOptions = append(DefaultPrivilegeOptions, op)
}
}

View File

@ -5,7 +5,8 @@ type UserExternalLoginResp struct {
BindingKey string `json:"binding_key"`
AccessToken string `json:"access_token"`
// ErrMsg error message, if not empty, means login failed and this message should be displayed.
ErrMsg string `json:"-"`
ErrMsg string `json:"-"`
ErrTitle string `json:"-"`
}
// ExternalLoginBindingUserSendEmailReq external login binding user request
@ -68,7 +69,8 @@ type UserCenterUserSettingsResp struct {
}
type UserCenterAdminFunctionAgentResp struct {
RoleAgentEnabled bool `json:"role_agent_enabled"`
UserStatusAgentEnabled bool `json:"user_status_agent_enabled"`
UserPasswordAgentEnabled bool `json:"user_password_agent_enabled"`
}
type UserSettingAgent struct {

View File

@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/token"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/log"
)
@ -52,6 +53,14 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
return nil, err
}
}
// try to get user status from user center
uc, ok := plugin.GetUserCenter()
if ok && len(userCacheInfo.ExternalID) > 0 {
if userStatus := uc.UserStatus(userCacheInfo.ExternalID); userStatus != plugin.UserStatusAvailable {
userCacheInfo.UserStatus = int(userStatus)
}
}
return userCacheInfo, nil
}

View File

@ -10,6 +10,10 @@ const (
QuestionReopen = "question.reopen"
QuestionVoteUp = "question.vote_up"
QuestionVoteDown = "question.vote_down"
QuestionPin = "question.pin" //Top the question
QuestionUnPin = "question.unpin" //untop the question
QuestionHide = "question.hide" //hide the question
QuestionShow = "question.show" //show the question
AnswerAdd = "answer.add"
AnswerEdit = "answer.edit"
AnswerEditWithoutReview = "answer.edit_without_review"
@ -43,4 +47,8 @@ const (
deleteActionName = "action.delete"
closeActionName = "action.close"
reopenActionName = "action.reopen"
pinActionName = "action.pin"
unpinActionName = "action.unpin"
hideActionName = "action.hide"
showActionName = "action.show"
)

View File

@ -10,7 +10,7 @@ import (
// GetQuestionPermission get question permission
func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string,
canEdit, canDelete, canClose, canReopen bool) (
canEdit, canDelete, canClose, canReopen, canPin, canHide, CanUnPin, canShow bool) (
actions []*schema.PermissionMemberAction) {
lang := handler.GetLangByCtx(ctx)
actions = make([]*schema.PermissionMemberAction, 0)
@ -42,6 +42,36 @@ func GetQuestionPermission(ctx context.Context, userID string, creatorUserID str
Type: "confirm",
})
}
if canPin {
actions = append(actions, &schema.PermissionMemberAction{
Action: "pin",
Name: translator.Tr(lang, pinActionName),
Type: "confirm",
})
}
if canHide {
actions = append(actions, &schema.PermissionMemberAction{
Action: "hide",
Name: translator.Tr(lang, hideActionName),
Type: "confirm",
})
}
if CanUnPin {
actions = append(actions, &schema.PermissionMemberAction{
Action: "unpin",
Name: translator.Tr(lang, unpinActionName),
Type: "confirm",
})
}
if canShow {
actions = append(actions, &schema.PermissionMemberAction{
Action: "show",
Name: translator.Tr(lang, showActionName),
Type: "confirm",
})
}
if canDelete || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",

View File

@ -36,6 +36,7 @@ type QuestionRepo interface {
questionList []*entity.Question, total int64, err error)
UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error)
UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error)
UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error)
SearchByTitleLike(ctx context.Context, title string) (questionList []*entity.Question, err error)
UpdatePvCount(ctx context.Context, questionID string) (err error)
UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error)
@ -271,6 +272,8 @@ func (qs *QuestionCommon) FormatQuestionsPage(
FollowCount: questionInfo.FollowCount,
AcceptedAnswerID: questionInfo.AcceptedAnswerID,
LastAnswerID: questionInfo.LastAnswerID,
Pin: questionInfo.Pin,
Show: questionInfo.Show,
}
questionIDs = append(questionIDs, questionInfo.ID)
@ -526,6 +529,8 @@ func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question)
info.QuestionUpdateTime = 0
}
info.Status = data.Status
info.Pin = data.Pin
info.Show = data.Show
info.UserID = data.UserID
info.LastEditUserID = data.LastEditUserID
if data.LastAnswerID != "0" {

View File

@ -270,6 +270,8 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
question.Status = entity.QuestionStatusAvailable
question.RevisionID = "0"
question.CreatedAt = now
question.Pin = entity.QuestionUnPin
question.Show = entity.QuestionShow
//question.UpdatedAt = nil
err = qs.questionRepo.AddQuestion(ctx, question)
if err != nil {
@ -319,6 +321,58 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
return
}
// OperationQuestion
func (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.OperationQuestionReq) (err error) {
questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
if err != nil {
return err
}
if !has {
return nil
}
// Hidden question cannot be placed at the top
if questionInfo.Show == entity.QuestionHide && req.Operation == schema.QuestionOperationPin {
return nil
}
// Question cannot be hidden when they are at the top
if questionInfo.Pin == entity.QuestionPin && req.Operation == schema.QuestionOperationHide {
return nil
}
switch req.Operation {
case schema.QuestionOperationHide:
questionInfo.Show = entity.QuestionHide
case schema.QuestionOperationShow:
questionInfo.Show = entity.QuestionShow
case schema.QuestionOperationPin:
questionInfo.Pin = entity.QuestionPin
case schema.QuestionOperationUnPin:
questionInfo.Pin = entity.QuestionUnPin
}
err = qs.questionRepo.UpdateQuestionOperation(ctx, questionInfo)
if err != nil {
return err
}
actMap := make(map[string]constant.ActivityTypeKey)
actMap[schema.QuestionOperationPin] = constant.ActQuestionPin
actMap[schema.QuestionOperationUnPin] = constant.ActQuestionUnPin
actMap[schema.QuestionOperationHide] = constant.ActQuestionHide
actMap[schema.QuestionOperationShow] = constant.ActQuestionShow
_, ok := actMap[req.Operation]
if ok {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: actMap[req.Operation],
})
}
return nil
}
// RemoveQuestion delete question
func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) {
questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
@ -632,6 +686,21 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
if question.Status == entity.QuestionStatusClosed {
per.CanClose = false
}
if question.Pin == entity.QuestionPin {
per.CanPin = false
per.CanHide = false
}
if question.Pin == entity.QuestionUnPin {
per.CanUnPin = false
}
if question.Show == entity.QuestionShow {
per.CanShow = false
}
if question.Show == entity.QuestionHide {
per.CanHide = false
per.CanPin = false
}
if question.Status == entity.QuestionStatusDeleted {
operation := &schema.Operation{}
operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionAlreadyDeleted)
@ -641,7 +710,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240)
question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID,
per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen)
per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen, per.CanPin, per.CanHide, per.CanUnPin, per.CanShow)
return question, nil
}
@ -735,14 +804,15 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o
if ok {
item.QuestionInfo = questionMaps[item.QuestionID]
}
}
for _, item := range answerlist {
info := &schema.UserAnswerInfo{}
_ = copier.Copy(info, item)
info.AnswerID = item.ID
info.QuestionID = item.QuestionID
userAnswerlist = append(userAnswerlist, info)
if item.QuestionInfo.Status != entity.QuestionStatusDeleted {
userAnswerlist = append(userAnswerlist, info)
}
}
return userAnswerlist, count, nil
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity"
@ -35,7 +36,7 @@ func NewSiteInfoService(
tagCommonService *tagcommon.TagCommonService,
configRepo config.ConfigRepo,
) *SiteInfoService {
resp, err := siteInfoCommonService.GetSiteInterface(context.Background())
resp, err := siteInfoCommonService.GetSiteUsers(context.Background())
if err != nil {
log.Error(err)
} else {
@ -131,29 +132,18 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe
}
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
var (
siteType = "interface"
content []byte
)
// check language
if !translator.CheckLanguageIsValid(req.Language) {
err = errors.BadRequest(reason.LangNotFound)
return
}
content, _ = json.Marshal(req)
content, _ := json.Marshal(req)
data := entity.SiteInfo{
Type: siteType,
Type: constant.SiteTypeInterface,
Content: string(content),
}
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
if err == nil {
constant.DefaultAvatar = req.DefaultAvatar
}
return
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeInterface, &data)
}
// SaveSiteBranding save site branding information
@ -235,7 +225,11 @@ func (s *SiteInfoService) SaveSiteUsers(ctx context.Context, req *schema.SiteUse
Content: string(content),
Status: 1,
}
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsers, data)
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsers, data)
if err == nil {
constant.DefaultAvatar = req.DefaultAvatar
}
return err
}
// GetSMTPConfig get smtp config
@ -329,8 +323,8 @@ func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema
return nil, err
}
resp = &schema.GetPrivilegesConfigResp{
Options: schema.DefaultPrivilegeOptions,
SelectedLevel: schema.PrivilegeLevel2,
Options: s.translatePrivilegeOptions(ctx),
SelectedLevel: schema.PrivilegeLevel3,
}
if privilege != nil && privilege.Level > 0 {
resp.SelectedLevel = privilege.Level
@ -338,6 +332,25 @@ func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema
return resp, nil
}
func (s *SiteInfoService) translatePrivilegeOptions(ctx context.Context) (options []*schema.PrivilegeOption) {
la := handler.GetLangByCtx(ctx)
for _, option := range schema.DefaultPrivilegeOptions {
op := &schema.PrivilegeOption{
Level: option.Level,
LevelDesc: translator.Tr(la, option.LevelDesc),
}
for _, privilege := range option.Privileges {
op.Privileges = append(op.Privileges, &constant.Privilege{
Key: privilege.Key,
Label: translator.Tr(la, privilege.Label),
Value: privilege.Value,
})
}
options = append(options, op)
}
return
}
func (s *SiteInfoService) UpdatePrivilegesConfig(ctx context.Context, req *schema.UpdatePrivilegesConfigReq) (err error) {
var chooseOption *schema.PrivilegeOption
for _, option := range schema.DefaultPrivilegeOptions {

View File

@ -150,7 +150,7 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use
return username + suffix, nil
}
func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, userStatus, emailStatus int) (
func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, userStatus, emailStatus int, externalID string) (
accessToken string, userCacheInfo *entity.UserCacheInfo, err error) {
roleID, err := us.userRoleService.GetUserRole(ctx, userID)
if err != nil {
@ -162,6 +162,7 @@ func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, use
EmailStatus: emailStatus,
UserStatus: userStatus,
RoleID: roleID,
ExternalID: externalID,
}
accessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)

View File

@ -14,6 +14,7 @@ import (
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/random"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/log"
@ -58,7 +59,9 @@ func (us *UserCenterLoginService) ExternalLogin(
if !checker.EmailInAllowEmailDomain(basicUserInfo.Email, siteInfo.AllowEmailDomains) {
log.Debugf("email domain not allowed: %s", basicUserInfo.Email)
return &schema.UserExternalLoginResp{
ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError)}, nil
ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),
ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError),
}, nil
}
}
@ -74,18 +77,24 @@ func (us *UserCenterLoginService) ExternalLogin(
return nil, err
}
if exist {
// if user is deleted, do not allow login
if oldUserInfo.Status == entity.UserStatusDeleted {
return &schema.UserExternalLoginResp{
ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),
ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserPageAccessDenied),
}, nil
}
if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil {
log.Errorf("update user last login date failed: %v", err)
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status)
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
}
// cache external user info, waiting for user enter email address.
if userCenter.Description().MustAuthEmailEnabled && len(basicUserInfo.Email) == 0 {
// TODO: check
return &schema.UserExternalLoginResp{ErrMsg: "Requires authorized email to login"}, nil
}
@ -97,7 +106,7 @@ func (us *UserCenterLoginService) ExternalLogin(
us.activeUser(ctx, oldUserInfo)
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status)
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
@ -126,7 +135,7 @@ func (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider
userInfo.Status = entity.UserStatusAvailable
userInfo.LastLoginDate = time.Now()
userInfo.Bio = basicUserInfo.Bio
userInfo.BioHTML = basicUserInfo.Bio
userInfo.BioHTML = converter.Markdown2HTML(basicUserInfo.Bio)
err = us.userRepo.AddUser(ctx, userInfo)
if err != nil {
return nil, err
@ -203,7 +212,11 @@ func (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Conte
return
}
desc := userCenter.Description()
resp.RoleAgentEnabled = desc.RoleAgentEnabled
// If user status agent is enabled, admin can not update user status in answer.
resp.UserStatusAgentEnabled = desc.UserStatusAgentEnabled
// If original user system is enabled, admin can update user password in answer.
// So user password agent is disabled.
resp.UserPasswordAgentEnabled = !desc.EnabledOriginalUserSystem
return resp, nil
}

View File

@ -83,7 +83,7 @@ func (us *UserExternalLoginService) ExternalLogin(
log.Error(err)
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status)
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
}
@ -122,7 +122,7 @@ func (us *UserExternalLoginService) ExternalLogin(
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status)
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
@ -251,7 +251,7 @@ func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail(
return nil, err
}
resp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo(
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status)
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID)
if err != nil {
log.Error(err)
}

View File

@ -499,7 +499,7 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
}
accessToken, userCacheInfo, err := us.userCommonService.CacheLoginUserInfo(
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status)
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, "")
if err != nil {
return nil, err
}

View File

@ -12,6 +12,7 @@ const (
ConfigTypeUpload ConfigType = "upload"
ConfigTypeTimezone ConfigType = "timezone"
ConfigTypeSwitch ConfigType = "switch"
ConfigTypeButton ConfigType = "button"
)
const (
@ -43,10 +44,13 @@ type ConfigField struct {
}
type ConfigFieldUIOptions struct {
Placeholder Translator `json:"placeholder,omitempty"`
Rows string `json:"rows,omitempty"`
InputType InputType `json:"input_type,omitempty"`
Label Translator `json:"label,omitempty"`
Placeholder Translator `json:"placeholder,omitempty"`
Rows string `json:"rows,omitempty"`
InputType InputType `json:"input_type,omitempty"`
Label Translator `json:"label,omitempty"`
Action *ConfigFieldUIOptionAction `json:"action,omitempty"`
Variant string `json:"variant,omitempty"`
Text Translator `json:"text,omitempty"`
}
type ConfigFieldOption struct {
@ -54,6 +58,10 @@ type ConfigFieldOption struct {
Value string `json:"value"`
}
type ConfigFieldUIOptionAction struct {
Url string `json:"url"`
}
type Config interface {
Base

View File

@ -12,6 +12,8 @@ type UserCenter interface {
SignUpCallback(ctx *GinContext) (userInfo *UserCenterBasicUserInfo, err error)
// UserInfo returns the user information
UserInfo(externalID string) (userInfo *UserCenterBasicUserInfo, err error)
// UserStatus returns the latest user status
UserStatus(externalID string) (userStatus UserStatus)
// UserList returns the user list information
UserList(externalIDs []string) (userInfo []*UserCenterBasicUserInfo, err error)
// UserSettings returns the user settings
@ -23,14 +25,16 @@ type UserCenter interface {
}
type UserCenterDesc struct {
Name string `json:"name"`
Icon string `json:"icon"`
Url string `json:"url"`
LoginRedirectURL string `json:"login_redirect_url"`
SignUpRedirectURL string `json:"sign_up_redirect_url"`
RankAgentEnabled bool `json:"rank_agent_enabled"`
RoleAgentEnabled bool `json:"role_agent_enabled"`
MustAuthEmailEnabled bool `json:"must_auth_email_enabled"`
Name string `json:"name"`
DisplayName Translator `json:"display_name"`
Icon string `json:"icon"`
Url string `json:"url"`
LoginRedirectURL string `json:"login_redirect_url"`
SignUpRedirectURL string `json:"sign_up_redirect_url"`
RankAgentEnabled bool `json:"rank_agent_enabled"`
UserStatusAgentEnabled bool `json:"user_status_agent_enabled"`
MustAuthEmailEnabled bool `json:"must_auth_email_enabled"`
EnabledOriginalUserSystem bool `json:"enabled_original_user_system"`
}
type UserStatus int

View File

@ -36,6 +36,7 @@ module.exports = {
'react/no-unescaped-entities': 'off',
'react/require-default-props': 'off',
'arrow-body-style': 'off',
"global-require": "off",
'react/prop-types': 0,
'react/no-danger': 'off',
'jsx-a11y/no-static-element-interactions': 'off',

View File

@ -1,6 +1,6 @@
const {
addWebpackModuleRule,
addWebpackAlias
addWebpackAlias,
} = require("customize-cra");
const path = require("path");

View File

@ -82,7 +82,7 @@
"react-app-rewired": "^2.2.1",
"react-scripts": "5.0.1",
"sass": "^1.54.4",
"typescript": "^4.9.5",
"typescript": "^4.8.3",
"yaml-loader": "^0.8.0"
},
"packageManager": "pnpm@7.9.5",

View File

@ -66,7 +66,7 @@ specifiers:
sass: ^1.54.4
semver: ^7.3.8
swr: ^1.3.0
typescript: ^4.9.5
typescript: ^4.8.3
urlcat: ^3.0.0
yaml-loader: ^0.8.0
zustand: ^4.1.1

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#000000" />
<meta name="generator" content="Answer - https://github.com/answerdev/answer">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -8,6 +8,12 @@ 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 USER_AGENT_NAMES = {
SegmentFault: 'SegmentFault',
WeChat: 'WeChat',
WeCom: 'WeCom',
DingTalk: 'DingTalk',
};
export const IGNORE_PATH_LIST = [
'/users/login',
@ -74,7 +80,8 @@ export const ADMIN_NAV_MENUS = [
name: 'themes',
},
{
name: 'css-html',
name: 'css_html',
path: 'css-html',
},
],
},
@ -89,6 +96,8 @@ export const ADMIN_NAV_MENUS = [
{ name: 'write' },
{ name: 'seo' },
{ name: 'login' },
{ name: 'users', path: 'settings-users' },
{ name: 'privileges' },
],
},
{
@ -96,6 +105,7 @@ export const ADMIN_NAV_MENUS = [
children: [
{
name: 'installed_plugins',
path: 'installed-plugins',
},
],
},
@ -599,6 +609,10 @@ export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
'upvote',
'reopened',
'closed',
'pin',
'unpin',
'show',
'hide',
];
export const SYSTEM_AVATAR_OPTIONS = [

View File

@ -312,7 +312,6 @@ export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {
export interface AdminSettingsInterface {
language: string;
time_zone?: string;
default_avatar?: string;
}
export interface AdminSettingsSmtp {
@ -327,6 +326,16 @@ export interface AdminSettingsSmtp {
test_email_recipient?: string;
}
export interface AdminSettingsUsers {
allow_update_avatar: boolean;
allow_update_bio: boolean;
allow_update_display_name: boolean;
allow_update_location: boolean;
allow_update_username: boolean;
allow_update_website: boolean;
default_avatar: string;
}
export interface SiteSettings {
branding: AdminSettingBranding;
general: AdminSettingsGeneral;
@ -335,6 +344,7 @@ export interface SiteSettings {
custom_css_html: AdminSettingsCustom;
theme: AdminSettingsTheme;
site_seo: AdminSettingsSeo;
site_users: AdminSettingsUsers;
version: string;
revision: string;
}
@ -385,6 +395,7 @@ export interface AdminSettingsCustom {
custom_head: string;
custom_header: string;
custom_footer: string;
custom_sidebar: string;
}
export interface AdminSettingsLogin {
@ -574,3 +585,8 @@ export interface PluginConfig {
slug_name: string;
config_fields: PluginItem[];
}
export interface QuestionOperationReq {
id: string;
operation: 'pin' | 'unpin' | 'hide' | 'show';
}

View File

@ -1,6 +1,9 @@
const pattern = {
email:
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/,
uaWeChat: /micromessenger/i,
uaWeCom: /wxwork/i,
uaDingTalk: /dingtalk/i,
};
export default pattern;

View File

@ -18,12 +18,12 @@ function MenuNode({
}) {
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
const isLeaf = !menu.children.length;
const href = isLeaf ? `${path}${menu.name}` : '#';
const href = isLeaf ? `${path}${menu.path}` : '#';
return (
<Nav.Item key={menu.name} className="w-100">
<Nav.Item key={menu.path} className="w-100">
<Nav.Link
eventKey={menu.name}
eventKey={menu.path}
as={isLeaf ? 'a' : 'button'}
onClick={(evt) => {
callback(evt, menu, href, isLeaf);
@ -31,7 +31,7 @@ function MenuNode({
href={href}
className={classNames(
'text-nowrap d-flex flex-nowrap align-items-center w-100',
{ expanding, 'link-dark': activeKey !== menu.name },
{ expanding, 'link-dark': activeKey !== menu.path },
)}>
<span className="me-auto text-truncate">
{menu.displayName ? menu.displayName : t(menu.name)}
@ -44,7 +44,7 @@ function MenuNode({
)}
</Nav.Link>
{menu.children.length ? (
<Accordion.Collapse eventKey={menu.name} className="ms-3">
<Accordion.Collapse eventKey={menu.path} className="ms-3">
<>
{menu.children.map((leaf) => {
return (
@ -53,7 +53,7 @@ function MenuNode({
callback={callback}
activeKey={activeKey}
path={path}
key={leaf.name}
key={leaf.path}
/>
);
})}
@ -73,17 +73,24 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
const pathMatch = useMatch(`${path}*`);
// auto set menu fields
menus.forEach((m) => {
if (!m.path) {
m.path = m.name;
}
if (!Array.isArray(m.children)) {
m.children = [];
}
m.children.forEach((sm) => {
if (!sm.path) {
sm.path = sm.name;
}
if (!Array.isArray(sm.children)) {
sm.children = [];
}
});
});
const splat = pathMatch && pathMatch.params['*'];
let activeKey = menus[0].name;
let activeKey = menus[0].path;
if (splat) {
activeKey = splat;
}
@ -92,10 +99,10 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
menus.forEach((li) => {
if (li.children.length) {
const matchedChild = li.children.find((el) => {
return el.name === activeKey;
return el.path === activeKey;
});
if (matchedChild) {
openKey = li.name;
openKey = li.path;
}
}
});
@ -111,7 +118,7 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
navigate(href);
}
} else {
setOpenKey(openKey === menu.name ? '' : menu.name);
setOpenKey(openKey === menu.path ? '' : menu.path);
}
};
useEffect(() => {
@ -127,8 +134,8 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
path={path}
callback={menuClick}
activeKey={activeKey}
expanding={openKey === li.name}
key={li.name}
expanding={openKey === li.path}
key={li.path}
/>
);
})}

View File

@ -9,9 +9,16 @@ interface Props {
value: string;
onChange: (value: string) => void;
acceptType?: string;
readOnly?: boolean;
}
const Index: FC<Props> = ({ type = 'post', value, onChange, acceptType }) => {
const Index: FC<Props> = ({
type = 'post',
value,
onChange,
acceptType,
readOnly = false,
}) => {
const onUpload = (imgPath: string) => {
onChange(imgPath);
};
@ -29,11 +36,15 @@ const Index: FC<Props> = ({ type = 'post', value, onChange, acceptType }) => {
type={type}
uploadCallback={onUpload}
className="mb-0"
disabled={readOnly}
acceptType={acceptType}>
<Icon name="cloud-upload" />
</UploadImg>
<Button variant="outline-secondary" onClick={onRemove}>
<Button
disabled={readOnly}
variant="outline-secondary"
onClick={onRemove}>
<Icon name="trash" />
</Button>
</ButtonGroup>

View File

@ -0,0 +1,11 @@
import { memo } from 'react';
import { customizeStore } from '@/stores';
const Index = () => {
const { custom_sidebar } = customizeStore((state) => state);
if (!custom_sidebar) return null;
return <div dangerouslySetInnerHTML={{ __html: custom_sidebar }} />;
};
export default memo(Index);

View File

@ -34,7 +34,9 @@ const ActivateScriptNodes = (el, part) => {
}
scriptList?.forEach((so) => {
const script = document.createElement('script');
script.text = so.text;
script.text = `(() => {
${so.text}
})();`;
for (let i = 0; i < so.attributes.length; i += 1) {
const attr = so.attributes[i];
script.setAttribute(attr.name, attr.value);

View File

@ -8,6 +8,7 @@ import {
} from 'react';
import { markdownToHtml } from '@/services';
import ImgViewer from '@/components/ImgViewer';
import { htmlRender } from './utils';
@ -48,11 +49,13 @@ const Index = ({ value }, ref) => {
});
return (
<div
ref={previewRef}
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
dangerouslySetInnerHTML={{ __html: html }}
/>
<ImgViewer>
<div
ref={previewRef}
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
dangerouslySetInnerHTML={{ __html: html }}
/>
</ImgViewer>
);
};

View File

@ -11,8 +11,8 @@ const Index = () => {
const siteName = siteInfoStore((state) => state.siteInfo.name);
const cc = `${fullYear} ${siteName}`;
return (
<footer className="bg-light py-3">
<Container>
<footer className="bg-light">
<Container className="py-3">
<p className="text-center mb-0 fs-14 text-secondary">
<Trans i18nKey="footer.build_on" values={{ cc }}>
Built on

View File

@ -4,7 +4,12 @@ import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
const Index = ({ httpCode = '', errMsg = '' }) => {
const Index = ({
httpCode = '',
title = '',
errMsg = '',
showErrorCode = true,
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'page_error' });
useEffect(() => {
// auto height of container
@ -31,7 +36,10 @@ const Index = ({ httpCode = '', errMsg = '' }) => {
style={{ fontSize: '120px', lineHeight: 1.2 }}>
(=x=)
</div>
<h4 className="text-center">{t('http_error', { code: httpCode })}</h4>
{showErrorCode && (
<h4 className="text-center">{t('http_error', { code: httpCode })}</h4>
)}
{title && <h4 className="text-center">{title}</h4>}
<div className="text-center mb-3 fs-5">
{errMsg || t(`desc_${httpCode}`)}
</div>

View File

@ -8,15 +8,24 @@ interface IProps {
name: string;
className?: string;
size?: string;
title?: string;
onClick?: () => void;
}
const Icon: FC<IProps> = ({ type = 'br', name, className, size, onClick }) => {
const Icon: FC<IProps> = ({
type = 'br',
name,
className,
size,
onClick,
title = '',
}) => {
return (
<i
className={classNames(type, `bi-${name}`, className)}
style={{ ...(size && { fontSize: size }) }}
onClick={onClick}
onKeyDown={onClick}
title={title}
/>
);
};

View File

@ -0,0 +1,7 @@
.img-viewer .cursor-zoom-out {
cursor: zoom-out !important;
}
.img-viewer img:not(a img, img.broken) {
cursor: zoom-in;
}

View File

@ -1,14 +1,13 @@
import { useLayoutEffect, useState, MouseEvent, useEffect } from 'react';
import { FC, MouseEvent, ReactNode, useEffect, useState } from 'react';
import { Modal } from 'react-bootstrap';
import { useLocation } from 'react-router-dom';
import ReactDOM from 'react-dom/client';
import './index.css';
import classnames from 'classnames';
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
const useImgViewer = () => {
const location = useLocation();
const Index: FC<{
children: ReactNode;
className?: classnames.Argument;
}> = ({ children, className }) => {
const [visible, setVisible] = useState(false);
const [imgSrc, setImgSrc] = useState('');
const onClose = () => {
@ -47,8 +46,18 @@ const useImgViewer = () => {
}
};
useLayoutEffect(() => {
root.render(
useEffect(() => {
return () => {
onClose();
};
}, []);
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div
className={classnames('img-viewer', className)}
onClick={checkClickForImgView}>
{children}
<Modal
show={visible}
fullscreen
@ -56,23 +65,16 @@ const useImgViewer = () => {
scrollable
contentClassName="bg-transparent"
onHide={onClose}>
<Modal.Body onClick={onClose} className="p-0 d-flex">
<Modal.Body onClick={onClose} className="img-viewer 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,
};
</Modal>
</div>
);
};
export default useImgViewer;
export default Index;

View File

@ -1,19 +1,22 @@
import { memo, FC } from 'react';
import { Button } from 'react-bootstrap';
import { Button, Dropdown } from 'react-bootstrap';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components';
import { useReportModal, useToast } from '@/hooks';
import { QuestionOperationReq } from '@/common/interface';
import Share from '../Share';
import {
deleteQuestion,
deleteAnswer,
editCheck,
reopenQuestion,
questionOpetation,
} from '@/services';
import { tryNormalLogged } from '@/utils/guard';
import { floppyNavigation } from '@/utils';
import { toastStore } from '@/stores';
interface IProps {
type: 'answer' | 'question';
@ -78,7 +81,7 @@ const Index: FC<IProps> = ({
id: qid,
}).then(() => {
toast.onShow({
msg: t('tip_question_deleted'),
msg: t('post_deleted', { keyPrefix: 'messages' }),
variant: 'success',
});
callback?.('delete_question');
@ -134,7 +137,7 @@ const Index: FC<IProps> = ({
question_id: qid,
}).then(() => {
toast.onShow({
msg: t('success', { keyPrefix: 'question_detail.reopen' }),
msg: t('post_reopen', { keyPrefix: 'messages' }),
variant: 'success',
});
refreshQuestion();
@ -143,6 +146,51 @@ const Index: FC<IProps> = ({
});
};
const handleCommon = async (params) => {
await questionOpetation(params);
let msg = '';
if (params.operation === 'pin') {
msg = t('post_pin', { keyPrefix: 'messages' });
}
if (params.operation === 'unpin') {
msg = t('post_unpin', { keyPrefix: 'messages' });
}
if (params.operation === 'hide') {
msg = t('post_hide_list', { keyPrefix: 'messages' });
}
if (params.operation === 'show') {
msg = t('post_show_list', { keyPrefix: 'messages' });
}
toastStore.getState().show({
msg,
variant: 'success',
});
setTimeout(() => {
refreshQuestion();
}, 100);
};
const handlOtherActions = (action) => {
const params: QuestionOperationReq = {
id: qid,
operation: action,
};
if (action === 'pin') {
Modal.confirm({
title: t('title', { keyPrefix: 'question_detail.pin' }),
content: t('content', { keyPrefix: 'question_detail.pin' }),
cancelBtnVariant: 'link',
confirmText: t('confirm_btn', { keyPrefix: 'question_detail.pin' }),
onConfirm: () => {
handleCommon(params);
},
});
} else {
handleCommon(params);
}
};
const handleAction = (action) => {
if (!tryNormalLogged(true)) {
return;
@ -162,8 +210,33 @@ const Index: FC<IProps> = ({
if (action === 'reopen') {
handleReopen();
}
if (
action === 'pin' ||
action === 'unpin' ||
action === 'hide' ||
action === 'show'
) {
handlOtherActions(action);
}
};
const firstAction =
memberActions?.filter(
(v) =>
v.action === 'report' || v.action === 'edit' || v.action === 'delete',
) || [];
const secondAction =
memberActions?.filter(
(v) =>
v.action === 'close' ||
v.action === 'reopen' ||
v.action === 'pin' ||
v.action === 'unpin' ||
v.action === 'hide' ||
v.action === 'show',
) || [];
return (
<div className="d-flex align-items-center">
<Share
@ -173,13 +246,13 @@ const Index: FC<IProps> = ({
title={title}
slugTitle={slugTitle}
/>
{memberActions?.map((item) => {
{firstAction?.map((item) => {
if (item.action === 'edit') {
return (
<Link
key={item.action}
to={editUrl}
className="link-secondary p-0 fs-14 me-3"
className="link-secondary p-0 fs-14 ms-3"
onClick={(evt) => handleEdit(evt, editUrl)}
style={{ lineHeight: '23px' }}>
{item.name}
@ -190,12 +263,32 @@ const Index: FC<IProps> = ({
<Button
key={item.action}
variant="link"
className="link-secondary p-0 fs-14 me-3"
className="link-secondary p-0 fs-14 ms-3"
onClick={() => handleAction(item.action)}>
{item.name}
</Button>
);
})}
{secondAction.length > 0 && (
<Dropdown className="ms-3">
<Dropdown.Toggle
variant="link"
className="link-secondary p-0 fs-14 no-toggle">
{t('action', { keyPrefix: 'question_detail' })}
</Dropdown.Toggle>
<Dropdown.Menu>
{secondAction.map((item) => {
return (
<Dropdown.Item
key={item.action}
onClick={() => handleAction(item.action)}>
{item.name}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
</Dropdown>
)}
</div>
);
};

View File

@ -14,6 +14,7 @@ import {
QueryGroup,
QuestionListLoader,
Counts,
Icon,
} from '@/components';
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
@ -62,6 +63,13 @@ const QuestionList: FC<Props> = ({ source, data, isLoading = false }) => {
key={li.id}
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
<h5 className="text-wrap text-break">
{li.pin === 2 && (
<Icon
name="pin-fill"
className="me-1"
title={t('pinned', { keyPrefix: 'btns' })}
/>
)}
<NavLink
to={pathFactory.questionLanding(li.id, li.url_title)}
className="link-dark">

View File

@ -0,0 +1,53 @@
import React, { FC, useState } from 'react';
import { Button, ButtonProps } from 'react-bootstrap';
import { request } from '@/utils';
import type * as Type from '@/common/interface';
import type { UIAction } from '../index.d';
interface Props {
fieldName: string;
text: string;
action: UIAction | undefined;
formData: Type.FormDataType;
readOnly: boolean;
variant?: ButtonProps['variant'];
size?: ButtonProps['size'];
}
const Index: FC<Props> = ({
fieldName,
action,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
formData,
text = '',
readOnly = false,
variant = 'primary',
size,
}) => {
const [isLoading, setLoading] = useState(false);
const handleAction = async () => {
if (!action) {
return;
}
setLoading(true);
const method = action.method || 'get';
await request[method](action.url);
setLoading(false);
};
const disabled = isLoading || readOnly;
return (
<div className="d-flex">
<Button
name={fieldName}
onClick={handleAction}
disabled={disabled}
size={size}
variant={variant}>
{text || fieldName}
{isLoading ? '...' : ''}
</Button>
</div>
);
};
export default Index;

View File

@ -0,0 +1,67 @@
import React, { FC } from 'react';
import { Form, Stack } from 'react-bootstrap';
import type * as Type from '@/common/interface';
interface Props {
type: 'radio' | 'checkbox';
fieldName: string;
onChange?: (fd: Type.FormDataType) => void;
enumValues: (string | boolean | number)[];
enumNames: string[];
formData: Type.FormDataType;
readOnly?: boolean;
}
const Index: FC<Props> = ({
type = 'radio',
fieldName,
onChange,
enumValues,
enumNames,
formData,
readOnly = false,
}) => {
const fieldObject = formData[fieldName];
const handleCheck = (
evt: React.ChangeEvent<HTMLInputElement>,
index: number,
) => {
const { name, checked } = evt.currentTarget;
const freshVal = checked ? enumValues?.[index] : '';
const state = {
...formData,
[name]: {
...formData[name],
value: freshVal,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Stack direction="horizontal">
{enumValues?.map((item, index) => {
return (
<Form.Check
key={String(item)}
inline
type={type}
name={fieldName}
id={`form-${String(item)}`}
label={enumNames?.[index]}
checked={(fieldObject?.value || '') === item}
feedback={fieldObject?.errorMsg}
feedbackType="invalid"
isInvalid={fieldObject?.isInvalid}
disabled={readOnly}
onChange={(evt) => handleCheck(evt, index)}
/>
);
})}
</Stack>
);
};
export default Index;

View File

@ -0,0 +1,52 @@
import React, { FC } from 'react';
import { Form } from 'react-bootstrap';
import type * as Type from '@/common/interface';
interface Props {
type: string | undefined;
placeholder: string | undefined;
fieldName: string;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
readOnly: boolean;
}
const Index: FC<Props> = ({
type = 'text',
placeholder = '',
fieldName,
onChange,
formData,
readOnly = false,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Form.Control
name={fieldName}
placeholder={placeholder}
type={type}
value={fieldObject?.value || ''}
onChange={handleChange}
disabled={readOnly}
isInvalid={fieldObject?.isInvalid}
style={type === 'color' ? { width: '6rem' } : {}}
/>
);
};
export default Index;

View File

@ -0,0 +1,11 @@
import { FC } from 'react';
import { Form } from 'react-bootstrap';
interface Props {
title: string;
}
const Index: FC<Props> = ({ title }) => {
return <Form.Label>{title}</Form.Label>;
};
export default Index;

View File

@ -0,0 +1,58 @@
import React, { FC } from 'react';
import { Form } from 'react-bootstrap';
import type * as Type from '@/common/interface';
interface Props {
desc: string | undefined;
fieldName: string;
onChange?: (fd: Type.FormDataType) => void;
enumValues: (string | boolean | number)[];
enumNames: string[];
formData: Type.FormDataType;
readOnly: boolean;
}
const Index: FC<Props> = ({
desc,
fieldName,
onChange,
enumValues,
enumNames,
formData,
readOnly = false,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Form.Select
aria-label={desc}
name={fieldName}
value={fieldObject?.value || ''}
onChange={handleChange}
disabled={readOnly}
isInvalid={fieldObject?.isInvalid}>
{enumValues?.map((item, index) => {
return (
<option value={String(item)} key={String(item)}>
{enumNames?.[index]}
</option>
);
})}
</Form.Select>
);
};
export default Index;

View File

@ -0,0 +1,53 @@
import React, { FC } from 'react';
import { Form } from 'react-bootstrap';
import type * as Type from '@/common/interface';
interface Props {
title: string;
label: string | undefined;
fieldName: string;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
readOnly?: boolean;
}
const Index: FC<Props> = ({
title,
fieldName,
onChange,
label,
formData,
readOnly = false,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value: checked,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Form.Check
id={`switch-${title}`}
name={fieldName}
type="switch"
label={label}
checked={fieldObject?.value || ''}
feedback={fieldObject?.errorMsg}
feedbackType="invalid"
isInvalid={fieldObject.isInvalid}
disabled={readOnly}
onChange={handleChange}
/>
);
};
export default Index;

View File

@ -0,0 +1,57 @@
import React, { FC } from 'react';
import { Form } from 'react-bootstrap';
import classnames from 'classnames';
import type * as Type from '@/common/interface';
interface Props {
placeholder: string | undefined;
rows: number | undefined;
className: classnames.Argument;
fieldName: string;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
readOnly: boolean;
}
const Index: FC<Props> = ({
placeholder = '',
rows = 3,
className,
fieldName,
onChange,
formData,
readOnly = false,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Form.Control
as="textarea"
name={fieldName}
placeholder={placeholder}
value={fieldObject?.value || ''}
onChange={handleChange}
isInvalid={fieldObject?.isInvalid}
rows={rows}
disabled={readOnly}
className={classnames(className)}
/>
);
};
export default Index;

View File

@ -0,0 +1,44 @@
import React, { FC } from 'react';
import type * as Type from '@/common/interface';
import TimeZonePicker from '@/components/TimeZonePicker';
interface Props {
fieldName: string;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
readOnly?: boolean;
}
const Index: FC<Props> = ({
fieldName,
onChange,
formData,
readOnly = false,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<TimeZonePicker
value={fieldObject?.value || ''}
isInvalid={fieldObject?.isInvalid}
name={fieldName}
disabled={readOnly}
onChange={handleChange}
/>
);
};
export default Index;

View File

@ -0,0 +1,54 @@
import React, { FC } from 'react';
import { Form } from 'react-bootstrap';
import type * as Type from '@/common/interface';
import BrandUpload from '@/components/BrandUpload';
interface Props {
type: Type.UploadType | undefined;
acceptType: string | undefined;
fieldName: string;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
readOnly?: boolean;
}
const Index: FC<Props> = ({
type = 'avatar',
acceptType = '',
fieldName,
onChange,
formData,
readOnly = false,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (name: string, value: string) => {
const state = {
...formData,
[name]: {
...formData[name],
value,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<>
<BrandUpload
type={type}
acceptType={acceptType}
value={fieldObject?.value}
readOnly={readOnly}
onChange={(value) => handleChange(fieldName, value)}
/>
<Form.Control
name={fieldName}
className="d-none"
isInvalid={fieldObject?.isInvalid}
/>
</>
);
};
export default Index;

View File

@ -0,0 +1,21 @@
import Legend from './Legend';
import Select from './Select';
import Check from './Check';
import Switch from './Switch';
import Timezone from './Timezone';
import Upload from './Upload';
import Textarea from './Textarea';
import Input from './Input';
import Button from './Button';
export {
Legend,
Select,
Check,
Switch,
Timezone,
Upload,
Textarea,
Input,
Button,
};

View File

@ -0,0 +1,6 @@
export interface UIAction {
url: string;
method?: 'get' | 'post' | 'put' | 'delete';
event?: 'click' | 'change';
handler?: ({evt, formData, request}) => Promise<void>
}

View File

@ -1,18 +1,29 @@
import {
import React, {
ForwardRefRenderFunction,
forwardRef,
useImperativeHandle,
useEffect,
} from 'react';
import { Form, Button, Stack } from 'react-bootstrap';
import { Form, Button, ButtonProps } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import BrandUpload from '../BrandUpload';
import TimeZonePicker from '../TimeZonePicker';
import type * as Type from '@/common/interface';
import type { UIAction } from './index.d';
import {
Legend,
Select,
Check,
Switch,
Timezone,
Upload,
Textarea,
Input,
Button as CtrlButton,
} from './components';
export interface JSONSchema {
title: string;
description?: string;
@ -31,12 +42,19 @@ export interface JSONSchema {
export interface BaseUIOptions {
empty?: string;
className?: string | string[];
// Will be appended to the className of the form component itself
className?: classnames.Argument;
// The className that will be attached to a form field container
fieldClassName?: classnames.Argument;
// Make a form component render into simplified mode
readOnly?: boolean;
simplify?: boolean;
validator?: (
value,
formData?,
) => Promise<string | true | void> | true | string;
}
export interface InputOptions extends BaseUIOptions {
placeholder?: string;
inputType?:
@ -78,6 +96,14 @@ export interface TextareaOptions extends BaseUIOptions {
rows?: number;
}
export interface ButtonOptions extends BaseUIOptions {
text: string;
icon?: string;
action?: UIAction;
variant?: ButtonProps['variant'];
size?: ButtonProps['size'];
}
export type UIOptions =
| InputOptions
| SelectOptions
@ -86,7 +112,8 @@ export type UIOptions =
| TimezoneOptions
| CheckboxOptions
| RadioOptions
| TextareaOptions;
| TextareaOptions
| ButtonOptions;
export type UIWidget =
| 'textarea'
@ -96,7 +123,9 @@ export type UIWidget =
| 'select'
| 'upload'
| 'timezone'
| 'switch';
| 'switch'
| 'legend'
| 'button';
export interface UISchema {
[key: string]: {
'ui:widget'?: UIWidget;
@ -117,6 +146,16 @@ interface IRef {
validator: () => Promise<boolean>;
}
/**
* TODO:
* - Normalize and document `formData[key].hidden && 'd-none'`
* - Normalize and document `hiddenSubmit`
* - Improving field hints for `formData`
* - Optimise form data updates
* * Automatic field type conversion
* * Dynamic field generation
*/
/**
* json schema form
* @param schema json schema
@ -139,9 +178,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
const { t } = useTranslation('translation', {
keyPrefix: 'form',
});
const { required = [], properties } = schema;
const { required = [], properties = {} } = schema || {};
// check required field
const excludes = required.filter((key) => !properties[key]);
@ -175,39 +212,6 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
setDefaultValueAsDomBehaviour();
}, [formData]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const data = {
...formData,
[name]: { ...formData[name], value, isInvalid: false },
};
if (onChange instanceof Function) {
onChange(data);
}
};
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = e.target;
const data = {
...formData,
[name]: { ...formData[name], value, isInvalid: false },
};
if (onChange instanceof Function) {
onChange(data);
}
};
const handleSwitchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = e.target;
const data = {
...formData,
[name]: { ...formData[name], value: checked, isInvalid: false },
};
if (onChange instanceof Function) {
onChange(data);
}
};
const requiredValidator = () => {
const errors: string[] = [];
required.forEach((key) => {
@ -316,248 +320,146 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
}
};
const handleUploadChange = (name: string, value: string) => {
const data = { ...formData, [name]: { ...formData[name], value } };
if (onChange instanceof Function) {
onChange(data);
}
};
const handleInputCheck = (
e: React.ChangeEvent<HTMLInputElement>,
index: number,
) => {
const { name, checked } = e.currentTarget;
const freshVal = checked ? schema.properties[name]?.enum?.[index] : '';
const data = {
...formData,
[name]: {
...formData[name],
value: freshVal,
isInvalid: false,
},
};
if (onChange instanceof Function) {
onChange(data);
}
};
useImperativeHandle(ref, () => ({
validator,
}));
if (!formData || !schema || !schema.properties) {
return null;
}
return (
<Form noValidate onSubmit={handleSubmit}>
{keys.map((key) => {
const { title, description } = properties[key];
const {
title,
description,
enum: enumValues = [],
enumNames = [],
} = properties[key];
const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =
uiSchema[key] || {};
if (widget === 'select') {
return (
<Form.Group
key={title}
controlId={key}
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
<Form.Label>{title}</Form.Label>
<Form.Select
aria-label={description}
name={key}
value={formData[key]?.value || ''}
onChange={handleSelectChange}
isInvalid={formData[key].isInvalid}>
{properties[key].enum?.map((item, index) => {
return (
<option value={String(item)} key={String(item)}>
{properties[key].enumNames?.[index]}
</option>
);
})}
</Form.Select>
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
const fieldState = formData[key];
const uiSimplify = widget === 'legend' || uiOpt?.simplify;
let groupClassName: BaseUIOptions['fieldClassName'] = uiOpt?.simplify
? 'mb-2'
: 'mb-3';
if (widget === 'legend') {
groupClassName = 'mb-0';
}
if (widget === 'checkbox' || widget === 'radio') {
return (
<Form.Group
key={title}
className={classnames('mb-3', formData[key].hidden && 'd-none')}
controlId={key}>
<Form.Label>{title}</Form.Label>
<Stack direction="horizontal">
{properties[key].enum?.map((item, index) => {
return (
<Form.Check
key={String(item)}
inline
required
type={widget}
name={key}
id={`form-${String(item)}`}
label={properties[key].enumNames?.[index]}
checked={(formData[key]?.value || '') === item}
feedback={formData[key]?.errorMsg}
feedbackType="invalid"
isInvalid={formData[key].isInvalid}
onChange={(e) => handleInputCheck(e, index)}
/>
);
})}
</Stack>
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
if (uiOpt?.fieldClassName) {
groupClassName = uiOpt.fieldClassName;
}
if (widget === 'switch') {
return (
<Form.Group
key={title}
className={classnames('mb-3', formData[key].hidden && 'd-none')}
controlId={key}>
<Form.Label>{title}</Form.Label>
<Form.Check
required
id={`switch-${title}`}
name={key}
type="switch"
label={(uiOpt as SwitchOptions)?.label}
checked={formData[key]?.value || ''}
feedback={formData[key]?.errorMsg}
feedbackType="invalid"
isInvalid={formData[key].isInvalid}
onChange={handleSwitchChange}
/>
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
if (widget === 'timezone') {
return (
<Form.Group
key={title}
className={classnames('mb-3', formData[key].hidden && 'd-none')}
controlId={key}>
<Form.Label>{title}</Form.Label>
<TimeZonePicker
value={formData[key]?.value || ''}
name={key}
onChange={handleSelectChange}
/>
<Form.Control
name={key}
className="d-none"
isInvalid={formData[key].isInvalid}
/>
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
if (widget === 'upload') {
const options: UploadOptions = uiSchema[key]?.['ui:options'] || {};
return (
<Form.Group
key={title}
className={classnames('mb-3', formData[key].hidden && 'd-none')}
controlId={key}>
<Form.Label>{title}</Form.Label>
<BrandUpload
type={options.imageType || 'avatar'}
acceptType={options.acceptType || ''}
value={formData[key]?.value}
onChange={(value) => handleUploadChange(key, value)}
/>
<Form.Control
name={key}
className="d-none"
isInvalid={formData[key].isInvalid}
/>
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
if (widget === 'textarea') {
const options: TextareaOptions = uiSchema[key]?.['ui:options'] || {};
return (
<Form.Group
controlId={`form-${key}`}
key={key}
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
<Form.Label>{title}</Form.Label>
<Form.Control
as="textarea"
name={key}
placeholder={options?.placeholder || ''}
value={formData[key]?.value || ''}
onChange={handleInputChange}
isInvalid={formData[key].isInvalid}
rows={options?.rows || 3}
className={classnames(options.className)}
/>
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
const options: InputOptions = uiSchema[key]?.['ui:options'] || {};
const readOnly = uiOpt?.readOnly || false;
return (
<Form.Group
key={title}
controlId={key}
key={key}
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
<Form.Label>{title}</Form.Label>
<Form.Control
name={key}
placeholder={options?.placeholder || ''}
type={options?.inputType || 'text'}
value={formData[key]?.value || ''}
onChange={handleInputChange}
style={options?.inputType === 'color' ? { width: '6rem' } : {}}
isInvalid={formData[key].isInvalid}
/>
className={classnames(
groupClassName,
formData[key].hidden ? 'd-none' : null,
)}>
{/* Uniform processing `label` */}
{title && !uiSimplify ? <Form.Label>{title}</Form.Label> : null}
{/* Handling of individual specific controls */}
{widget === 'legend' ? <Legend title={title} /> : null}
{widget === 'select' ? (
<Select
desc={description}
fieldName={key}
onChange={onChange}
enumValues={enumValues}
enumNames={enumNames}
formData={formData}
readOnly={readOnly}
/>
) : null}
{widget === 'radio' || widget === 'checkbox' ? (
<Check
type={widget}
fieldName={key}
onChange={onChange}
enumValues={enumValues}
enumNames={enumNames}
formData={formData}
readOnly={readOnly}
/>
) : null}
{widget === 'switch' ? (
<Switch
title={title}
label={uiOpt && 'label' in uiOpt ? uiOpt.label : ''}
fieldName={key}
onChange={onChange}
formData={formData}
readOnly={readOnly}
/>
) : null}
{widget === 'timezone' ? (
<Timezone
fieldName={key}
onChange={onChange}
formData={formData}
readOnly={readOnly}
/>
) : null}
{widget === 'upload' ? (
<Upload
type={
uiOpt && 'imageType' in uiOpt ? uiOpt.imageType : undefined
}
acceptType={
uiOpt && 'acceptType' in uiOpt ? uiOpt.acceptType : ''
}
fieldName={key}
onChange={onChange}
formData={formData}
readOnly={readOnly}
/>
) : null}
{widget === 'textarea' ? (
<Textarea
placeholder={
uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
}
rows={uiOpt && 'rows' in uiOpt ? uiOpt.rows : 3}
className={uiOpt && 'className' in uiOpt ? uiOpt.className : ''}
fieldName={key}
onChange={onChange}
formData={formData}
readOnly={readOnly}
/>
) : null}
{widget === 'input' ? (
<Input
type={uiOpt && 'inputType' in uiOpt ? uiOpt.inputType : 'text'}
placeholder={
uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
}
fieldName={key}
onChange={onChange}
formData={formData}
readOnly={readOnly}
/>
) : null}
{widget === 'button' ? (
<CtrlButton
fieldName={key}
text={uiOpt && 'text' in uiOpt ? uiOpt.text : ''}
action={uiOpt && 'action' in uiOpt ? uiOpt.action : undefined}
formData={formData}
readOnly={readOnly}
variant={
uiOpt && 'variant' in uiOpt ? uiOpt.variant : undefined
}
size={uiOpt && 'size' in uiOpt ? uiOpt.size : undefined}
/>
) : null}
{/* Unified handling of `Feedback` and `Text` */}
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
{fieldState?.errorMsg}
</Form.Control.Feedback>
{description && (
{description ? (
<Form.Text className="text-muted">{description}</Form.Text>
)}
) : null}
</Form.Group>
);
})}

View File

@ -71,7 +71,7 @@ const Index: FC<IProps> = ({ type, qid, aid, title, slugTitle = '' }) => {
<Dropdown.Toggle
id="dropdown-share"
as="a"
className="no-toggle fs-14 link-secondary pointer me-3"
className="no-toggle fs-14 link-secondary pointer"
onClick={() => setShow(true)}
style={{ lineHeight: '23px' }}>
{t('share.name')}

View File

@ -38,7 +38,7 @@ const TagSelector: FC<IProps> = ({
const [initialValue, setInitialValue] = useState<Type.Tag[]>([...value]);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [repeatIndex, setRepeatIndex] = useState(-1);
const [tag, setTag] = useState<string>('');
const [searchValue, setSearchValue] = useState<string>('');
const [tags, setTags] = useState<Type.Tag[] | null>(null);
const { t } = useTranslation('translation', { keyPrefix: 'tag_selector' });
const [visibleMenu, setVisibleMenu] = useState(false);
@ -101,12 +101,12 @@ const TagSelector: FC<IProps> = ({
const fetchTags = (str) => {
queryTags(str).then((res) => {
const tagArray: Type.Tag[] = filterTags(res || []);
setTags(tagArray);
setTags(tagArray?.length > 5 ? tagArray.slice(0, 5) : tagArray);
});
};
useEffect(() => {
fetchTags(tag);
fetchTags(searchValue);
}, [visibleMenu]);
const handleClick = (val: Type.Tag) => {
@ -146,7 +146,7 @@ const TagSelector: FC<IProps> = ({
const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {
const searchStr = e.currentTarget.value.replace(';', '');
setTag(searchStr);
setSearchValue(searchStr);
fetchTags(searchStr);
};
@ -172,7 +172,7 @@ const TagSelector: FC<IProps> = ({
e.preventDefault();
if (tags.length === 0) {
tagModal.onShow(tag);
tagModal.onShow(searchValue);
return;
}
if (currentIndex <= tags.length - 1) {
@ -228,13 +228,14 @@ const TagSelector: FC<IProps> = ({
<FormControl
placeholder={t('search_tag')}
autoFocus
value={tag}
value={searchValue}
onChange={handleSearch}
/>
</Form>
</Dropdown.Header>
)}
{showRequiredTagText &&
{!searchValue &&
showRequiredTagText &&
tags &&
tags.filter((v) => v.recommend)?.length > 0 && (
<h6 className="dropdown-header">{t('tag_required_text')}</h6>
@ -251,17 +252,17 @@ const TagSelector: FC<IProps> = ({
</Dropdown.Item>
);
})}
{tag && tags && tags.length === 0 && (
{searchValue && tags && tags.length === 0 && (
<Dropdown.Item disabled className="text-secondary">
{t('no_result')}
</Dropdown.Item>
)}
{!hiddenCreateBtn && tag && (
{!hiddenCreateBtn && searchValue && (
<Button
variant="link"
className="px-3 btn-no-border w-100 text-start"
onClick={() => {
tagModal.onShow(tag);
tagModal.onShow(searchValue);
}}>
+ {t('create_btn')}
</Button>

View File

@ -1,14 +1,17 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import { uploadImage } from '@/services';
import * as Type from '@/common/interface';
interface IProps {
type: Type.UploadType;
className?: string;
className?: classnames.Argument;
children?: React.ReactNode;
acceptType?: string;
disabled?: boolean;
uploadCallback: (img: string) => void;
}
@ -18,6 +21,7 @@ const Index: React.FC<IProps> = ({
children,
acceptType = '',
className,
disabled = false,
}) => {
const { t } = useTranslation();
const [status, setStatus] = useState(false);
@ -47,11 +51,15 @@ const Index: React.FC<IProps> = ({
};
return (
<label className={`btn btn-outline-secondary uploadBtn ${className}`}>
<label
className={classnames('btn btn-outline-secondary', className, {
disabled: !!disabled,
})}>
{children || (status ? t('upload_img.loading') : t('upload_img.name'))}
<input
type="file"
className="d-none"
disabled={disabled}
accept={`image/jpeg,image/jpg,image/png,image/webp${acceptType}`}
onChange={onChange}
/>

View File

@ -38,6 +38,8 @@ import Counts from './Counts';
import QuestionList from './QuestionList';
import HotQuestions from './HotQuestions';
import HttpErrorContent from './HttpErrorContent';
import CustomSidebar from './CustomSidebar';
import ImgViewer from './ImgViewer';
export {
Avatar,
@ -82,5 +84,7 @@ export {
QuestionList,
HotQuestions,
HttpErrorContent,
CustomSidebar,
ImgViewer,
};
export type { EditorRef, JSONSchema, UISchema };

View File

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

Some files were not shown because too many files have changed in this diff Show More