diff --git a/docs/docs.go b/docs/docs.go index 9276de4f..cd4baca8 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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 diff --git a/docs/swagger.json b/docs/swagger.json index f06e078d..f0c10bfb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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 diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e9f6f1be..d3dd6775 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: diff --git a/i18n/cy_GB.yaml b/i18n/cy_GB.yaml index 95aab5eb..5b099723 100644 --- a/i18n/cy_GB.yaml +++ b/i18n/cy_GB.yaml @@ -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: >- - + 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: >-

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

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted 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. diff --git a/i18n/de_DE.yaml b/i18n/de_DE.yaml index d25eed73..da16e6c0 100644 --- a/i18n/de_DE.yaml +++ b/i18n/de_DE.yaml @@ -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: >-

Wir empfehlen nicht, akzeptierte Antworten zu löschen, da dies zukünftigen Lesern dieses Wissen entzieht.

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.
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.
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 Datei manuell im <1>/var/wwww/xxx/ 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 + label: Benutzerdefinierte CSS + text: Dies wird als eingefügt head: - label: Head - text: This will insert before + label: Kopf + text: Dies wird vor eingefügt header: label: Header - text: This will insert after + text: Dies wird nach eingefügt footer: - label: Footer - text: This will insert before . + label: Fusszeile + text: Dies wird vor 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. diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index bfe7da55..90b0e430 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -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 avoid ... 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 . + 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. diff --git a/i18n/es_ES.yaml b/i18n/es_ES.yaml index 88bfe870..a7a894f3 100644 --- a/i18n/es_ES.yaml +++ b/i18n/es_ES.yaml @@ -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: >- -

We do not allowed deleting tag with posts.

Please remove this tag from the posts first.

+

No permitimos eliminar etiquetas con publicaciones.

Primero elimine esta etiqueta de las publicaciones.

tip_with_synonyms: >- -

We do not allowed deleting tag with synonyms.

Please remove the synonyms from this tag first.

- tip: Are you sure you wish to delete? +

No permitimos eliminar etiquetas con sinónimos.

Elimine primero los sinónimos de esta etiqueta.

+ 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: >-

¿Seguro que quieres añadir otra respuesta?

Puedes utilizar el enlace de edición para detallar y mejorar tu respuesta existente en su lugar.

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 answer the question. 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.
Perhaps you'd like to select a different tab? + list_empty: No se encontraron publicaciones.
¿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 file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. - info: After you've done that, click "Next" button. - site_information: Site Information - admin_account: Admin Account + Puede crear el archivo <1>config.yaml manualmente en el directorio <1>/var/www/xxx/ 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; 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; encuéntrelo en el menú del sitio. + good_luck: "¡Diviértete y buena suerte!" + warn_title: Advertencia warn_desc: >- - The file <1>config.yaml 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. - installed: Already installed + El archivo <1>config.yaml 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. + 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 file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down. + Esto significa que la información de la base de datos en su archivo <1>config.yaml 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 don’t 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 head: - label: Head + label: Cabeza text: Esto se insertará antes de 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. diff --git a/i18n/fr_FR.yaml b/i18n/fr_FR.yaml index bf487b72..8b320017 100644 --- a/i18n/fr_FR.yaml +++ b/i18n/fr_FR.yaml @@ -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: >-

Nous ne recommandons pas de supprimer la réponse acceptée car cela prive les futurs lecteurs de cette connaissance.

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. diff --git a/i18n/id_ID.yaml b/i18n/id_ID.yaml index 827a1877..8fd9b5da 100644 --- a/i18n/id_ID.yaml +++ b/i18n/id_ID.yaml @@ -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: >-

Kami tidak menyarankan menghapus jawaban yang diterima karena hal itu menghilangkan pengetahuan ini dari pembaca di masa mendatang.

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. diff --git a/i18n/it_IT.yaml b/i18n/it_IT.yaml index 414425e4..830f9d1e 100644 --- a/i18n/it_IT.yaml +++ b/i18n/it_IT.yaml @@ -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: >-

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

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted 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. diff --git a/i18n/ja_JP.yaml b/i18n/ja_JP.yaml index 34c17404..41b9733c 100644 --- a/i18n/ja_JP.yaml +++ b/i18n/ja_JP.yaml @@ -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: >- 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: >-

We do not allowed deleting tag with posts.

Please remove this tag from the posts first.

tip_with_synonyms: >-

We do not allowed deleting tag with synonyms.

Please remove the synonyms from this tag first.

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 {{mail}}. 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 info_login: Already have an account? <1>Log in agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. - forgot_pass: Forgot password? + 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 {{mail}}, 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 {{mail}}, 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: >-

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

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted 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 search answers" empty: We couldn't find anything.
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.
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 file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. info: After you've done that, click "Next" button. site_information: Site Information - admin_account: Admin Account + admin_account: 管理者アカウント 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. diff --git a/i18n/ko_KR.yaml b/i18n/ko_KR.yaml index bf82398b..5daf9db3 100644 --- a/i18n/ko_KR.yaml +++ b/i18n/ko_KR.yaml @@ -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: >-

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

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted 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. diff --git a/i18n/pt_PT.yaml b/i18n/pt_PT.yaml index b670e931..2ed0c03a 100644 --- a/i18n/pt_PT.yaml +++ b/i18n/pt_PT.yaml @@ -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: >-

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

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted 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. diff --git a/i18n/ru_RU.yaml b/i18n/ru_RU.yaml index b3de553b..f5a7be38 100644 --- a/i18n/ru_RU.yaml +++ b/i18n/ru_RU.yaml @@ -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: >-

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

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted 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. diff --git a/i18n/sk_SK.yaml b/i18n/sk_SK.yaml index b5860d25..8084368c 100644 --- a/i18n/sk_SK.yaml +++ b/i18n/sk_SK.yaml @@ -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: >- - + 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 - open-source software, ktorý poháňa Q&A komunity.
Vyrobené s láskou © {{cc}}. + Postavené na <1> Answer – softvéri s otvoreným zdrojom, ktorý poháňa komunity otázok a odpovedí.
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 + gravatar_text: Obrázok môžete zmeniť na <1>gravatar.com 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: >-

Neodporúčame odstránenie akceptovanej odpovede pretože týmto oberáte budúcich čitateľov o tieto vedomostí.

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] vyhľadávať v rámci značky" + tag: "<1>[tag] hľadať v rámci značky" user: "<1>user:username hľadať podľa autora" answer: "<1>answers:0 nezodpovedané otázky" score: "<1>score:3 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.
Chceli by ste vybrať inú kartu? + list_empty: Nenašli sa žiadne príspevky.
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 súbor manuálne v <1>/var/wwww/xxx/ adresári a vložte do neho nasledujúci text. + Súbor <1>config.yaml môžete vytvoriť manuálne v adresári <1>/var/www/xxx/ 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 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 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. 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 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 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. diff --git a/i18n/tr_TR.yaml b/i18n/tr_TR.yaml index 32a56a3c..0cb69e65 100644 --- a/i18n/tr_TR.yaml +++ b/i18n/tr_TR.yaml @@ -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: >-

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

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted 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. diff --git a/i18n/vi_VN.yaml b/i18n/vi_VN.yaml index 1d4771b0..ba414c65 100644 --- a/i18n/vi_VN.yaml +++ b/i18n/vi_VN.yaml @@ -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: >-

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

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted tip_answer_deleted: This answer has been deleted 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. diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 7e174512..4cc782bf 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -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 answer the question. Provide details and share your research. - li1_2: Back up any statements you make with references or personal experience. - header_2: But avoid ... - li2_1: Asking for help, seeking clarification, or responding to other answers. + header_1: 感谢你的回答. + li1_1: 请务必 回答问题。提供详细信息并分享您的研究。 + li1_2: 用参考资料或个人经历来支持你所做的任何陈述。 + header_2: 但是 请避免... + li2_1: 请求帮助,寻求澄清,或答复其他答案。 reopen: confirm_btn: 重新打开 title: 重新打开这个帖子 content: 确定要重新打开吗? - success: 这个帖子已被重新打开 + pin: + title: 置顶此条帖子 + content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。 + confirm_btn: 置顶 delete: title: 删除 question: >- @@ -809,7 +879,6 @@ ui: answer_accepted: >-

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? 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 don’t 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: 这个帖子已被重新打开. diff --git a/i18n/zh_TW.yaml b/i18n/zh_TW.yaml index fe91ec2d..322f14de 100644 --- a/i18n/zh_TW.yaml +++ b/i18n/zh_TW.yaml @@ -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: >-

我們不建議刪除被採納的回答。因為這樣做會使得後來的讀者無法從該回答中獲得幫助。

如果刪除過多被採納的貼文,你的帳號將會被禁止回答任何提問。你確定要刪除嗎? 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. diff --git a/internal/base/constant/acticity.go b/internal/base/constant/acticity.go index 8300147b..334d8e6d 100644 --- a/internal/base/constant/acticity.go +++ b/internal/base/constant/acticity.go @@ -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 ( diff --git a/internal/base/constant/rank.go b/internal/base/constant/rank.go index c7c86a48..f1cbf255 100644 --- a/internal/base/constant/rank.go +++ b/internal/base/constant/rank.go @@ -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}, } ) diff --git a/internal/base/middleware/user_center_plugin_auth.go b/internal/base/middleware/user_center_plugin_auth.go index c6a190a4..bf19c319 100644 --- a/internal/base/middleware/user_center_plugin_auth.go +++ b/internal/base/middleware/user_center_plugin_auth.go @@ -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 diff --git a/internal/base/reason/privilege.go b/internal/base/reason/privilege.go new file mode 100644 index 00000000..76d4e7bb --- /dev/null +++ b/internal/base/reason/privilege.go @@ -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" +) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 814bda95..3c257944 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -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" ) diff --git a/internal/base/server/http.go b/internal/base/server/http.go index 55ff1a31..2845e786 100644 --- a/internal/base/server/http.go +++ b/internal/base/server/http.go @@ -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) diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go index 160c0f48..bb6c37ad 100644 --- a/internal/controller/connector_controller.go +++ b/internal/controller/connector_controller.go @@ -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", diff --git a/internal/controller/plugin_user_center_controller.go b/internal/controller/plugin_user_center_controller.go index 8c624a2d..024a0692 100644 --- a/internal/controller/plugin_user_center_controller.go +++ b/internal/controller/plugin_user_center_controller.go @@ -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)) -} diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 128b1e9e..3ecf6bc3 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -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 { diff --git a/internal/controller_admin/user_backyard_controller.go b/internal/controller_admin/user_backyard_controller.go index 76fb8378..f1a93ca3 100644 --- a/internal/controller_admin/user_backyard_controller.go +++ b/internal/controller_admin/user_backyard_controller.go @@ -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 } diff --git a/internal/entity/auth_user_entity.go b/internal/entity/auth_user_entity.go index a417cd2a..79d24fb1 100644 --- a/internal/entity/auth_user_entity.go +++ b/internal/entity/auth_user_entity.go @@ -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"` } diff --git a/internal/entity/question_entity.go b/internal/entity/question_entity.go index 4b4f4923..41c9236b 100644 --- a/internal/entity/question_entity.go +++ b/internal/entity/question_entity.go @@ -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"` diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 0d8ffbb5..a2cc8904 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -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 { diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index af6b01fc..47acace5 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -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), } diff --git a/internal/migrations/v10.go b/internal/migrations/v10.go new file mode 100644 index 00000000..7ad0c990 --- /dev/null +++ b/internal/migrations/v10.go @@ -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 +} diff --git a/internal/migrations/v3.go b/internal/migrations/v3.go index 076a72b5..2d9b28eb 100644 --- a/internal/migrations/v3.go +++ b/internal/migrations/v3.go @@ -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`}, diff --git a/internal/migrations/v8.go b/internal/migrations/v8.go new file mode 100644 index 00000000..faac7f21 --- /dev/null +++ b/internal/migrations/v8.go @@ -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 +} diff --git a/internal/migrations/v9.go b/internal/migrations/v9.go index 90331820..fadd1bc6 100644 --- a/internal/migrations/v9.go +++ b/internal/migrations/v9.go @@ -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 } diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 26de8796..fdb1066a 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -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) diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index 987fea5c..dc07e5ae 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -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 { diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index b5cee0c4..8727267f 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -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. diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index ece32cad..7f9a801f 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -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) diff --git a/internal/schema/plugin_admin_schema.go b/internal/schema/plugin_admin_schema.go index be63d3ab..3ea8aeab 100644 --- a/internal/schema/plugin_admin_schema.go +++ b/internal/schema/plugin_admin_schema.go @@ -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"` diff --git a/internal/schema/plugin_user_center.go b/internal/schema/plugin_user_center.go index afa4727c..9a5f65a8 100644 --- a/internal/schema/plugin_user_center.go +++ b/internal/schema/plugin_user_center.go @@ -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 { diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index 8ee24194..b435b6e9 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -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"` diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index a0167de4..f0b659e3 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -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) } } diff --git a/internal/schema/user_external_login_schema.go b/internal/schema/user_external_login_schema.go index 54735c3d..221ff17f 100644 --- a/internal/schema/user_external_login_schema.go +++ b/internal/schema/user_external_login_schema.go @@ -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 { diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go index 618ef5bd..22eba69a 100644 --- a/internal/service/auth/auth.go +++ b/internal/service/auth/auth.go @@ -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 } diff --git a/internal/service/permission/permission_name.go b/internal/service/permission/permission_name.go index 4a62ec86..1d49ce3f 100644 --- a/internal/service/permission/permission_name.go +++ b/internal/service/permission/permission_name.go @@ -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" ) diff --git a/internal/service/permission/question_permission.go b/internal/service/permission/question_permission.go index 1321af45..6f4d126f 100644 --- a/internal/service/permission/question_permission.go +++ b/internal/service/permission/question_permission.go @@ -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", diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index 93623569..ce5b77e5 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -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" { diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 9acb7f27..7bfb908d 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -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 } diff --git a/internal/service/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go index e0d09fe4..b3bb6c80 100644 --- a/internal/service/siteinfo/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -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 { diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index e863c8c9..5432acf8 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -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) diff --git a/internal/service/user_external_login/user_center_login_service.go b/internal/service/user_external_login/user_center_login_service.go index e7f75664..773196e9 100644 --- a/internal/service/user_external_login/user_center_login_service.go +++ b/internal/service/user_external_login/user_center_login_service.go @@ -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 } diff --git a/internal/service/user_external_login/user_external_login_service.go b/internal/service/user_external_login/user_external_login_service.go index fefecf12..24b9037d 100644 --- a/internal/service/user_external_login/user_external_login_service.go +++ b/internal/service/user_external_login/user_external_login_service.go @@ -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) } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 47256b06..f9a300c2 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -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 } diff --git a/plugin/config.go b/plugin/config.go index d7c25cae..856c71ec 100644 --- a/plugin/config.go +++ b/plugin/config.go @@ -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 diff --git a/plugin/user_center.go b/plugin/user_center.go index 66687dc1..57061394 100644 --- a/plugin/user_center.go +++ b/plugin/user_center.go @@ -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 diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 032c2103..f4bf831f 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -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', diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 60b66a51..a82d9822 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -1,6 +1,6 @@ const { addWebpackModuleRule, - addWebpackAlias + addWebpackAlias, } = require("customize-cra"); const path = require("path"); diff --git a/ui/package.json b/ui/package.json index 22c97cbe..95a88c55 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 13cf786b..b33ae23c 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -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 diff --git a/ui/public/index.html b/ui/public/index.html index cb72d38a..f754cf9e 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -2,7 +2,7 @@ - + diff --git a/ui/src/assets/images/carousel-wecom-1.jpg b/ui/src/assets/images/carousel-wecom-1.jpg new file mode 100644 index 00000000..4dac6292 Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-1.jpg differ diff --git a/ui/src/assets/images/carousel-wecom-2.jpg b/ui/src/assets/images/carousel-wecom-2.jpg new file mode 100644 index 00000000..618db5af Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-2.jpg differ diff --git a/ui/src/assets/images/carousel-wecom-3.jpg b/ui/src/assets/images/carousel-wecom-3.jpg new file mode 100644 index 00000000..c8c7abb4 Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-3.jpg differ diff --git a/ui/src/assets/images/carousel-wecom-4.jpg b/ui/src/assets/images/carousel-wecom-4.jpg new file mode 100644 index 00000000..7f581fb4 Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-4.jpg differ diff --git a/ui/src/assets/images/carousel-wecom-5.jpg b/ui/src/assets/images/carousel-wecom-5.jpg new file mode 100644 index 00000000..e4068ebe Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-5.jpg differ diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 7a540ac9..727b91b9 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -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 = [ diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 8fc79f91..9907e0a9 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -312,7 +312,6 @@ export interface HelmetUpdate extends Omit { 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'; +} diff --git a/ui/src/common/pattern.ts b/ui/src/common/pattern.ts index 406e5e3e..f79f7cc3 100644 --- a/ui/src/common/pattern.ts +++ b/ui/src/common/pattern.ts @@ -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; diff --git a/ui/src/components/AccordionNav/index.tsx b/ui/src/components/AccordionNav/index.tsx index 472a3e63..4e64611f 100644 --- a/ui/src/components/AccordionNav/index.tsx +++ b/ui/src/components/AccordionNav/index.tsx @@ -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 ( - + { 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 }, )}> {menu.displayName ? menu.displayName : t(menu.name)} @@ -44,7 +44,7 @@ function MenuNode({ )} {menu.children.length ? ( - + <> {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 = ({ 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 = ({ 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 = ({ 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 = ({ menus = [], path = '/' }) => { path={path} callback={menuClick} activeKey={activeKey} - expanding={openKey === li.name} - key={li.name} + expanding={openKey === li.path} + key={li.path} /> ); })} diff --git a/ui/src/components/BrandUpload/index.tsx b/ui/src/components/BrandUpload/index.tsx index c921fbdf..419e1d8d 100644 --- a/ui/src/components/BrandUpload/index.tsx +++ b/ui/src/components/BrandUpload/index.tsx @@ -9,9 +9,16 @@ interface Props { value: string; onChange: (value: string) => void; acceptType?: string; + readOnly?: boolean; } -const Index: FC = ({ type = 'post', value, onChange, acceptType }) => { +const Index: FC = ({ + type = 'post', + value, + onChange, + acceptType, + readOnly = false, +}) => { const onUpload = (imgPath: string) => { onChange(imgPath); }; @@ -29,11 +36,15 @@ const Index: FC = ({ type = 'post', value, onChange, acceptType }) => { type={type} uploadCallback={onUpload} className="mb-0" + disabled={readOnly} acceptType={acceptType}> - diff --git a/ui/src/components/CustomSidebar/index.tsx b/ui/src/components/CustomSidebar/index.tsx new file mode 100644 index 00000000..5beccb51 --- /dev/null +++ b/ui/src/components/CustomSidebar/index.tsx @@ -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
; +}; + +export default memo(Index); diff --git a/ui/src/components/Customize/index.tsx b/ui/src/components/Customize/index.tsx index 8f99cc05..33875414 100644 --- a/ui/src/components/Customize/index.tsx +++ b/ui/src/components/Customize/index.tsx @@ -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); diff --git a/ui/src/components/Editor/Viewer.tsx b/ui/src/components/Editor/Viewer.tsx index db036f90..357be508 100644 --- a/ui/src/components/Editor/Viewer.tsx +++ b/ui/src/components/Editor/Viewer.tsx @@ -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 ( -
+ +
+ ); }; diff --git a/ui/src/components/Footer/index.tsx b/ui/src/components/Footer/index.tsx index 578576ce..40e20090 100644 --- a/ui/src/components/Footer/index.tsx +++ b/ui/src/components/Footer/index.tsx @@ -11,8 +11,8 @@ const Index = () => { const siteName = siteInfoStore((state) => state.siteInfo.name); const cc = `${fullYear} ${siteName}`; return ( -
- +
+

Built on diff --git a/ui/src/components/HttpErrorContent/index.tsx b/ui/src/components/HttpErrorContent/index.tsx index 51ce4bcc..8bd7cd98 100644 --- a/ui/src/components/HttpErrorContent/index.tsx +++ b/ui/src/components/HttpErrorContent/index.tsx @@ -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‘=)

-

{t('http_error', { code: httpCode })}

+ {showErrorCode && ( +

{t('http_error', { code: httpCode })}

+ )} + {title &&

{title}

}
{errMsg || t(`desc_${httpCode}`)}
diff --git a/ui/src/components/Icon/index.tsx b/ui/src/components/Icon/index.tsx index b1951b0e..ba973873 100644 --- a/ui/src/components/Icon/index.tsx +++ b/ui/src/components/Icon/index.tsx @@ -8,15 +8,24 @@ interface IProps { name: string; className?: string; size?: string; + title?: string; onClick?: () => void; } -const Icon: FC = ({ type = 'br', name, className, size, onClick }) => { +const Icon: FC = ({ + type = 'br', + name, + className, + size, + onClick, + title = '', +}) => { return ( ); }; diff --git a/ui/src/components/ImgViewer/index.css b/ui/src/components/ImgViewer/index.css new file mode 100644 index 00000000..c70399f1 --- /dev/null +++ b/ui/src/components/ImgViewer/index.css @@ -0,0 +1,7 @@ +.img-viewer .cursor-zoom-out { + cursor: zoom-out !important; +} + +.img-viewer img:not(a img, img.broken) { + cursor: zoom-in; +} diff --git a/ui/src/hooks/useImgViewer/index.tsx b/ui/src/components/ImgViewer/index.tsx similarity index 67% rename from ui/src/hooks/useImgViewer/index.tsx rename to ui/src/components/ImgViewer/index.tsx index 6b3a30c2..eceb45b4 100644 --- a/ui/src/hooks/useImgViewer/index.tsx +++ b/ui/src/components/ImgViewer/index.tsx @@ -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 +
+ {children} { scrollable contentClassName="bg-transparent" onHide={onClose}> - + {imgSrc} - , - ); - }); - useEffect(() => { - onClose(); - }, [location]); - return { - onClose, - checkClickForImgView, - }; + +
+ ); }; -export default useImgViewer; +export default Index; diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index 06e9377e..96e94b7a 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -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 = ({ 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 = ({ 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 = ({ }); }; + 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 = ({ 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 (
= ({ title={title} slugTitle={slugTitle} /> - {memberActions?.map((item) => { + {firstAction?.map((item) => { if (item.action === 'edit') { return ( handleEdit(evt, editUrl)} style={{ lineHeight: '23px' }}> {item.name} @@ -190,12 +263,32 @@ const Index: FC = ({ ); })} + {secondAction.length > 0 && ( + + + {t('action', { keyPrefix: 'question_detail' })} + + + {secondAction.map((item) => { + return ( + handleAction(item.action)}> + {item.name} + + ); + })} + + + )}
); }; diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 94377b18..26599c79 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -14,6 +14,7 @@ import { QueryGroup, QuestionListLoader, Counts, + Icon, } from '@/components'; const QuestionOrderKeys: Type.QuestionOrderBy[] = [ @@ -62,6 +63,13 @@ const QuestionList: FC = ({ source, data, isLoading = false }) => { key={li.id} className="bg-transparent py-3 px-0 border-start-0 border-end-0">
+ {li.pin === 2 && ( + + )} diff --git a/ui/src/components/SchemaForm/components/Button.tsx b/ui/src/components/SchemaForm/components/Button.tsx new file mode 100644 index 00000000..ea24c7aa --- /dev/null +++ b/ui/src/components/SchemaForm/components/Button.tsx @@ -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 = ({ + 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 ( +
+ +
+ ); +}; + +export default Index; diff --git a/ui/src/components/SchemaForm/components/Check.tsx b/ui/src/components/SchemaForm/components/Check.tsx new file mode 100644 index 00000000..fd18fb89 --- /dev/null +++ b/ui/src/components/SchemaForm/components/Check.tsx @@ -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 = ({ + type = 'radio', + fieldName, + onChange, + enumValues, + enumNames, + formData, + readOnly = false, +}) => { + const fieldObject = formData[fieldName]; + const handleCheck = ( + evt: React.ChangeEvent, + 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 ( + + {enumValues?.map((item, index) => { + return ( + handleCheck(evt, index)} + /> + ); + })} + + ); +}; + +export default Index; diff --git a/ui/src/components/SchemaForm/components/Input.tsx b/ui/src/components/SchemaForm/components/Input.tsx new file mode 100644 index 00000000..84fda2eb --- /dev/null +++ b/ui/src/components/SchemaForm/components/Input.tsx @@ -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 = ({ + type = 'text', + placeholder = '', + fieldName, + onChange, + formData, + readOnly = false, +}) => { + const fieldObject = formData[fieldName]; + const handleChange = (evt: React.ChangeEvent) => { + const { name, value } = evt.currentTarget; + const state = { + ...formData, + [name]: { + ...formData[name], + value, + isInvalid: false, + }, + }; + if (typeof onChange === 'function') { + onChange(state); + } + }; + + return ( + + ); +}; + +export default Index; diff --git a/ui/src/components/SchemaForm/components/Legend.tsx b/ui/src/components/SchemaForm/components/Legend.tsx new file mode 100644 index 00000000..96b58cf7 --- /dev/null +++ b/ui/src/components/SchemaForm/components/Legend.tsx @@ -0,0 +1,11 @@ +import { FC } from 'react'; +import { Form } from 'react-bootstrap'; + +interface Props { + title: string; +} +const Index: FC = ({ title }) => { + return {title}; +}; + +export default Index; diff --git a/ui/src/components/SchemaForm/components/Select.tsx b/ui/src/components/SchemaForm/components/Select.tsx new file mode 100644 index 00000000..0680360d --- /dev/null +++ b/ui/src/components/SchemaForm/components/Select.tsx @@ -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 = ({ + desc, + fieldName, + onChange, + enumValues, + enumNames, + formData, + readOnly = false, +}) => { + const fieldObject = formData[fieldName]; + const handleChange = (evt: React.ChangeEvent) => { + const { name, value } = evt.currentTarget; + const state = { + ...formData, + [name]: { + ...formData[name], + value, + isInvalid: false, + }, + }; + if (typeof onChange === 'function') { + onChange(state); + } + }; + return ( + + {enumValues?.map((item, index) => { + return ( + + ); + })} + + ); +}; + +export default Index; diff --git a/ui/src/components/SchemaForm/components/Switch.tsx b/ui/src/components/SchemaForm/components/Switch.tsx new file mode 100644 index 00000000..9e19bf35 --- /dev/null +++ b/ui/src/components/SchemaForm/components/Switch.tsx @@ -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 = ({ + title, + fieldName, + onChange, + label, + formData, + readOnly = false, +}) => { + const fieldObject = formData[fieldName]; + const handleChange = (evt: React.ChangeEvent) => { + const { name, checked } = evt.currentTarget; + const state = { + ...formData, + [name]: { + ...formData[name], + value: checked, + isInvalid: false, + }, + }; + if (typeof onChange === 'function') { + onChange(state); + } + }; + return ( + + ); +}; + +export default Index; diff --git a/ui/src/components/SchemaForm/components/Textarea.tsx b/ui/src/components/SchemaForm/components/Textarea.tsx new file mode 100644 index 00000000..792465cc --- /dev/null +++ b/ui/src/components/SchemaForm/components/Textarea.tsx @@ -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 = ({ + placeholder = '', + rows = 3, + className, + fieldName, + onChange, + formData, + readOnly = false, +}) => { + const fieldObject = formData[fieldName]; + const handleChange = (evt: React.ChangeEvent) => { + const { name, value } = evt.currentTarget; + const state = { + ...formData, + [name]: { + ...formData[name], + value, + isInvalid: false, + }, + }; + if (typeof onChange === 'function') { + onChange(state); + } + }; + + return ( + + ); +}; + +export default Index; diff --git a/ui/src/components/SchemaForm/components/Timezone.tsx b/ui/src/components/SchemaForm/components/Timezone.tsx new file mode 100644 index 00000000..364c120e --- /dev/null +++ b/ui/src/components/SchemaForm/components/Timezone.tsx @@ -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 = ({ + fieldName, + onChange, + formData, + readOnly = false, +}) => { + const fieldObject = formData[fieldName]; + const handleChange = (evt: React.ChangeEvent) => { + const { name, value } = evt.currentTarget; + const state = { + ...formData, + [name]: { + ...formData[name], + value, + isInvalid: false, + }, + }; + if (typeof onChange === 'function') { + onChange(state); + } + }; + return ( + + ); +}; + +export default Index; diff --git a/ui/src/components/SchemaForm/components/Upload.tsx b/ui/src/components/SchemaForm/components/Upload.tsx new file mode 100644 index 00000000..df8dbc60 --- /dev/null +++ b/ui/src/components/SchemaForm/components/Upload.tsx @@ -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 = ({ + 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 ( + <> + handleChange(fieldName, value)} + /> + + + ); +}; + +export default Index; diff --git a/ui/src/components/SchemaForm/components/index.ts b/ui/src/components/SchemaForm/components/index.ts new file mode 100644 index 00000000..2c6da07b --- /dev/null +++ b/ui/src/components/SchemaForm/components/index.ts @@ -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, +}; diff --git a/ui/src/components/SchemaForm/index.d.ts b/ui/src/components/SchemaForm/index.d.ts new file mode 100644 index 00000000..2c525579 --- /dev/null +++ b/ui/src/components/SchemaForm/index.d.ts @@ -0,0 +1,6 @@ +export interface UIAction { + url: string; + method?: 'get' | 'post' | 'put' | 'delete'; + event?: 'click' | 'change'; + handler?: ({evt, formData, request}) => Promise +} diff --git a/ui/src/components/SchemaForm/index.tsx b/ui/src/components/SchemaForm/index.tsx index c57ca5b2..c6e9faa4 100644 --- a/ui/src/components/SchemaForm/index.tsx +++ b/ui/src/components/SchemaForm/index.tsx @@ -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 | 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; } +/** + * 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 = ( 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 = ( setDefaultValueAsDomBehaviour(); }, [formData]); - const handleInputChange = (e: React.ChangeEvent) => { - 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) => { - 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) => { - 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 = ( } }; - const handleUploadChange = (name: string, value: string) => { - const data = { ...formData, [name]: { ...formData[name], value } }; - if (onChange instanceof Function) { - onChange(data); - } - }; - - const handleInputCheck = ( - e: React.ChangeEvent, - 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 (
{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 ( - - {title} - - {properties[key].enum?.map((item, index) => { - return ( - - ); - })} - - - {formData[key]?.errorMsg} - - {description && ( - {description} - )} - - ); + 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 ( - - {title} - - {properties[key].enum?.map((item, index) => { - return ( - handleInputCheck(e, index)} - /> - ); - })} - - - {formData[key]?.errorMsg} - - {description && ( - {description} - )} - - ); + if (uiOpt?.fieldClassName) { + groupClassName = uiOpt.fieldClassName; } - - if (widget === 'switch') { - return ( - - {title} - - - {formData[key]?.errorMsg} - - {description && ( - {description} - )} - - ); - } - if (widget === 'timezone') { - return ( - - {title} - - - - {formData[key]?.errorMsg} - - {description && ( - {description} - )} - - ); - } - - if (widget === 'upload') { - const options: UploadOptions = uiSchema[key]?.['ui:options'] || {}; - return ( - - {title} - handleUploadChange(key, value)} - /> - - - {formData[key]?.errorMsg} - - {description && ( - {description} - )} - - ); - } - - if (widget === 'textarea') { - const options: TextareaOptions = uiSchema[key]?.['ui:options'] || {}; - - return ( - - {title} - - - {formData[key]?.errorMsg} - - - {description && ( - {description} - )} - - ); - } - - const options: InputOptions = uiSchema[key]?.['ui:options'] || {}; - + const readOnly = uiOpt?.readOnly || false; return ( - {title} - + className={classnames( + groupClassName, + formData[key].hidden ? 'd-none' : null, + )}> + {/* Uniform processing `label` */} + {title && !uiSimplify ? {title} : null} + {/* Handling of individual specific controls */} + {widget === 'legend' ? : null} + {widget === 'select' ? ( +