Merge branch 'github-dev' into feat/ui-1.1.0

# Conflicts:
#	i18n/zh_CN.yaml
This commit is contained in:
LinkinStars 2023-02-08 14:40:39 +08:00
commit f9a1ffb7e5
87 changed files with 36955 additions and 1198 deletions

59
.github/Dockerfile vendored Normal file
View File

@ -0,0 +1,59 @@
FROM amd64/node AS node-builder
LABEL maintainer="mingcheng<mc@sf.com>"
COPY . /answer
WORKDIR /answer
RUN make install-ui-packages ui && mv ui/build /tmp
# stage2 build the main binary within static resource
FROM golang:1.19-alpine AS golang-builder
LABEL maintainer="aichy@sf.com"
ARG GOPROXY
ENV GOPROXY=https://goproxy.cn,direct
ENV GOPATH /go
ENV GOROOT /usr/local/go
ENV PACKAGE github.com/answerdev/answer
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
ARG TAGS="sqlite sqlite_unlock_notify"
ENV TAGS "bindata timetzdata $TAGS"
ARG CGO_EXTRA_CFLAGS
COPY . ${BUILD_DIR}
WORKDIR ${BUILD_DIR}
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
RUN apk --no-cache add build-base git \
&& make clean build \
&& cp answer /usr/bin/answer
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
# stage3 copy the binary and resource files into fresh container
FROM alpine
LABEL maintainer="maintainers@sf.com"
ENV TZ "Asia/Shanghai"
RUN apk update \
&& apk --no-cache add \
bash \
ca-certificates \
curl \
dumb-init \
gettext \
openssh \
sqlite \
gnupg \
&& echo "Asia/Shanghai" > /etc/timezone
COPY --from=golang-builder /usr/bin/answer /usr/bin/answer
COPY --from=golang-builder /data /data
COPY /script/entrypoint.sh /entrypoint.sh
RUN chmod 755 /entrypoint.sh
VOLUME /data
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -1,8 +1,8 @@
name: Build Docker Hub Image
name: Build DockerHub Image
on:
push:
branches: [ "main" ]
branches: [ "main","githubaction","test" ]
tags:
- v2.*
- v1.*
@ -12,49 +12,48 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: answerdev/answer
tags: |
type=raw,value=latest
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
platforms: linux/amd64,linux/arm64
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v3
with:
images: answerdev/answer
tags: |
type=raw,value=latest
# branch event
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
# tag event
#type=ref,enable=true,priority=600,prefix=,suffix=,event=tag
type=semver,pattern={{version}}
# - name: Login to GitHub Container Registry
# uses: docker/login-action@v2
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
file: ./.github/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,65 +0,0 @@
name: Build GitHub Image
on:
push:
branches: [ "main" ]
tags:
- 2.*
- 1.*
- 0.*
# pull_request:
# branches: [ "main" ]
env:
REGISTRY: ghcr.io
IMAGE: answerdev/answer
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
tags: |
type=raw,value=latest
- name: Build Img
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
builder: ${{ steps.buildx.outputs.name }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# - name: build to hub.docker
# run: |
# docker build -t answerdev/answer -f ./Dockerfile .
# - name: Login to hub.docker Registry
# run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
# - name: Push Image to hub.docker
# run: |
# docker push answerdev/answer

41
.github/workflows/build_goreleaser.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: "Goreleaser"
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
build-goreleaser:
runs-on: [self-hosted, linux]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Node Build
run: make install-ui-packages ui
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v3
with:
name: answer
path: ./dist/*

View File

@ -2,14 +2,14 @@ name: Go Build Test
on:
push:
branches: [ "main" ]
branches: [ "main","githubaction","test" ]
pull_request:
branches: [ "main" ]
jobs:
build-and-push:
runs-on: ubuntu-latest
go-build-test:
runs-on: [self-hosted, linux]
steps:
- name: Checkout

View File

@ -2,14 +2,13 @@ name: Node Build Test
on:
push:
branches: [ "main" ]
branches: [ "main","githubaction","test"]
pull_request:
branches: [ "main" ]
jobs:
build-and-push:
runs-on: ubuntu-latest
node-build-test:
runs-on: [self-hosted, linux]
steps:
- name: Checkout
uses: actions/checkout@v3

98
.github/workflows/uffizzi-build.yml vendored Normal file
View File

@ -0,0 +1,98 @@
name: Build PR Image
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
build-answer:
name: Build and push `Answer`
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.meta.outputs.tags }}
if: ${{ github.event.action != 'closed' }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_WORKER=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: registry.uffizzi.com/${{ env.UUID_WORKER }}
tags: |
type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com - Uffizzi's ephemeral Registry
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
cache-from: type=gha
cache-to: type=gha, mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
needs:
- build-answer
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
ANSWER_IMAGE=${{ needs.build-answer.outputs.tags }}
export ANSWER_IMAGE
export UFFIZZI_URL=\$UFFIZZI_URL
# Render simple template from environment variables.
envsubst < docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2

88
.github/workflows/uffizzi-preview.yml vendored Normal file
View File

@ -0,0 +1,88 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.rendered.yml
key: ${{ env.HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write

5
.gitignore vendored
View File

@ -15,8 +15,11 @@
/configs/config-dev.yaml
/go.work*
/logs
/ui/build
/ui/node_modules
/ui/build/*/*/*
/ui/build/*.json
/ui/build/*.html
/ui/build/*.txt
/vendor
Thumbs*.db
tmp

View File

@ -2,7 +2,7 @@ env:
- GO11MODULE=on
- GO111MODULE=on
- GOPROXY=https://goproxy.io
- CGO_ENABLED=1
- CGO_ENABLED=0
before:
hooks:
@ -16,74 +16,48 @@ builds:
- linux
goarch:
- amd64
# linux windows need cgomingw64-gcc
- id: build-windows
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
env:
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
goos:
- windows
goarch:
- amd64
# linux arm64 need cgo arm64
- id: build-arm64
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
env:
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
goos:
- linux
goarch:
- arm64
- id: build-arm7
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
env:
- CC=arm-linux-gnueabihf-gcc
- CXX=arm-linux-gnueabihf-g++
- AR=arm-linux-gnueabihf-ar
goos:
- linux
goarch:
- arm
goarm:
- 7
- arm64
- id: build-darwin-arm64
main: ./cmd/answer/.
binary: answer
env:
- CC=oa64-clang
- CXX=oa64-clang++
goos:
- darwin
goarch:
- arm64
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
flags: -v
- id: build-darwin-amd64
main: ./cmd/answer/.
binary: answer
env:
- CC=o64-clang
- CXX=o64-clang++
goos:
- darwin
goarch:
- amd64
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
flags: -v
archives:
- replacements:
darwin: Darwin
darwin: macOS
amd64: x86_64
linux: Linux
windows: Windows
checksum:
name_template: 'checksums.txt'
snapshot:
@ -95,10 +69,4 @@ changelog:
- '^docs:'
- '^test:'
# sudo apt-get install build-essential
# sudo apt-get install gcc-multilib g++-multilib
# sudo apt-get install gcc-mingw-w64
# sudo apt-get -y install gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf
# sudo apt-get install clang llvm
# goreleaser release --snapshot --rm-dist
# goreleaser release --snapshot --rm-dist

View File

@ -1,30 +1,32 @@
.PHONY: build clean ui
VERSION=1.0.3
VERSION=1.0.4
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
#GO_ENV=CGO_ENABLED=0
GO_ENV=CGO_ENABLED=0 GO111MODULE=on
Revision=$(shell git rev-parse --short HEAD)
GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X 'main.Revision=$(Revision)' -X 'main.Time=`date`' -extldflags -static"
GO=$(GO_ENV) $(shell which go)
build:
@$(GO_ENV) $(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
build: generate
@$(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
# https://dev.to/thewraven/universal-macos-binaries-with-go-1-16-3mm3
universal:
universal: generate
@GOOS=darwin GOARCH=amd64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_amd64 $(DIR_SRC)
@GOOS=darwin GOARCH=arm64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_arm64 $(DIR_SRC)
@lipo -create -output ${BIN} ${BIN}_amd64 ${BIN}_arm64
@rm -f ${BIN}_amd64 ${BIN}_arm64
generate:
go get github.com/google/wire/cmd/wire@latest
go install github.com/golang/mock/mockgen@v1.6.0
go generate ./...
go mod tidy
@$(GO) get github.com/google/wire/cmd/wire@v0.5.0
@$(GO) get github.com/golang/mock/mockgen@v1.6.0
@$(GO) install github.com/google/wire/cmd/wire@v0.5.0
@$(GO) install github.com/golang/mock/mockgen@v1.6.0
@$(GO) generate ./...
@$(GO) mod tidy
test:
@$(GO) test ./internal/repo/repo_test

5
SECURITY.md Normal file
View File

@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Please report security issues to `security@answer.dev`

View File

@ -0,0 +1,36 @@
version: "3"
# uffizzi integration
x-uffizzi:
ingress:
service: answer
port: 80
services:
answer:
image: "${ANSWER_IMAGE}"
volumes:
- answer-data:/data
deploy:
resources:
limits:
memory: 4000M
mysql:
image: mysql:latest
environment:
- MYSQL_DATABASE=answer
- MYSQL_ROOT_PASSWORD=password
- MYSQL_USER=mysql
- MYSQL_PASSWORD=mysql
volumes:
- sql_data:/var/lib/mysql
deploy:
resources:
limits:
memory: 500M
volumes:
answer-data:
sql_data:

View File

@ -2911,6 +2911,45 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/post/render": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "render post content",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Upload"
],
"summary": "render post content",
"parameters": [
{
"description": "PostRenderReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.PostRenderReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question": {
"put": {
"security": [
@ -5425,8 +5464,7 @@ const docTemplate = `{
"type": "object",
"required": [
"object_id",
"original_text",
"parsed_text"
"original_text"
],
"properties": {
"mention_username_list": {
@ -5442,11 +5480,9 @@ const docTemplate = `{
},
"original_text": {
"description": "original comment content",
"type": "string"
},
"parsed_text": {
"description": "parsed comment content",
"type": "string"
"type": "string",
"maxLength": 600,
"minLength": 2
},
"reply_comment_id": {
"description": "reply comment id",
@ -5535,46 +5571,41 @@ const docTemplate = `{
},
"schema.AnswerAddReq": {
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"description": "content",
"type": "string"
},
"html": {
"description": "html",
"type": "string"
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"question_id": {
"description": "question_id",
"type": "string"
}
}
},
"schema.AnswerUpdateReq": {
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"description": "content",
"type": "string"
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"edit_summary": {
"description": "edit_summary",
"type": "string"
},
"html": {
"description": "html",
"type": "string"
},
"id": {
"description": "id",
"type": "string"
},
"question_id": {
"description": "question_id",
"type": "string"
},
"title": {
"description": "title",
"type": "string"
}
}
@ -6589,11 +6620,18 @@ const docTemplate = `{
}
}
},
"schema.PostRenderReq": {
"type": "object",
"properties": {
"content": {
"type": "string"
}
}
},
"schema.QuestionAdd": {
"type": "object",
"required": [
"content",
"html",
"tags",
"title"
],
@ -6604,12 +6642,6 @@ const docTemplate = `{
"maxLength": 65535,
"minLength": 6
},
"html": {
"description": "html",
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"tags": {
"description": "tags",
"type": "array",
@ -6669,6 +6701,9 @@ const docTemplate = `{
"collection_count": {
"type": "integer"
},
"created_at": {
"type": "integer"
},
"description": {
"type": "string"
},
@ -6739,7 +6774,6 @@ const docTemplate = `{
"type": "object",
"required": [
"content",
"html",
"id",
"tags",
"title"
@ -6755,12 +6789,6 @@ const docTemplate = `{
"description": "edit summary",
"type": "string"
},
"html": {
"description": "html",
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"id": {
"description": "question id",
"type": "string"
@ -7326,10 +7354,6 @@ const docTemplate = `{
"description": "original text",
"type": "string"
},
"parsed_text": {
"description": "parsed text",
"type": "string"
},
"slug_name": {
"description": "slug_name",
"type": "string",
@ -7416,7 +7440,8 @@ const docTemplate = `{
"schema.UpdateCommentReq": {
"type": "object",
"required": [
"comment_id"
"comment_id",
"original_text"
],
"properties": {
"comment_id": {
@ -7425,11 +7450,9 @@ const docTemplate = `{
},
"original_text": {
"description": "original comment content",
"type": "string"
},
"parsed_text": {
"description": "parsed comment content",
"type": "string"
"type": "string",
"maxLength": 600,
"minLength": 2
}
}
},
@ -7460,11 +7483,6 @@ const docTemplate = `{
"type": "string",
"maxLength": 4096
},
"bio_html": {
"description": "bio",
"type": "string",
"maxLength": 4096
},
"display_name": {
"description": "display_name",
"type": "string",
@ -7549,10 +7567,6 @@ const docTemplate = `{
"description": "original text",
"type": "string"
},
"parsed_text": {
"description": "parsed text",
"type": "string"
},
"slug_name": {
"description": "slug_name",
"type": "string",

View File

@ -2899,6 +2899,45 @@
}
}
},
"/answer/api/v1/post/render": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "render post content",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Upload"
],
"summary": "render post content",
"parameters": [
{
"description": "PostRenderReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.PostRenderReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question": {
"put": {
"security": [
@ -5413,8 +5452,7 @@
"type": "object",
"required": [
"object_id",
"original_text",
"parsed_text"
"original_text"
],
"properties": {
"mention_username_list": {
@ -5430,11 +5468,9 @@
},
"original_text": {
"description": "original comment content",
"type": "string"
},
"parsed_text": {
"description": "parsed comment content",
"type": "string"
"type": "string",
"maxLength": 600,
"minLength": 2
},
"reply_comment_id": {
"description": "reply comment id",
@ -5523,46 +5559,41 @@
},
"schema.AnswerAddReq": {
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"description": "content",
"type": "string"
},
"html": {
"description": "html",
"type": "string"
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"question_id": {
"description": "question_id",
"type": "string"
}
}
},
"schema.AnswerUpdateReq": {
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"description": "content",
"type": "string"
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"edit_summary": {
"description": "edit_summary",
"type": "string"
},
"html": {
"description": "html",
"type": "string"
},
"id": {
"description": "id",
"type": "string"
},
"question_id": {
"description": "question_id",
"type": "string"
},
"title": {
"description": "title",
"type": "string"
}
}
@ -6577,11 +6608,18 @@
}
}
},
"schema.PostRenderReq": {
"type": "object",
"properties": {
"content": {
"type": "string"
}
}
},
"schema.QuestionAdd": {
"type": "object",
"required": [
"content",
"html",
"tags",
"title"
],
@ -6592,12 +6630,6 @@
"maxLength": 65535,
"minLength": 6
},
"html": {
"description": "html",
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"tags": {
"description": "tags",
"type": "array",
@ -6657,6 +6689,9 @@
"collection_count": {
"type": "integer"
},
"created_at": {
"type": "integer"
},
"description": {
"type": "string"
},
@ -6727,7 +6762,6 @@
"type": "object",
"required": [
"content",
"html",
"id",
"tags",
"title"
@ -6743,12 +6777,6 @@
"description": "edit summary",
"type": "string"
},
"html": {
"description": "html",
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"id": {
"description": "question id",
"type": "string"
@ -7314,10 +7342,6 @@
"description": "original text",
"type": "string"
},
"parsed_text": {
"description": "parsed text",
"type": "string"
},
"slug_name": {
"description": "slug_name",
"type": "string",
@ -7404,7 +7428,8 @@
"schema.UpdateCommentReq": {
"type": "object",
"required": [
"comment_id"
"comment_id",
"original_text"
],
"properties": {
"comment_id": {
@ -7413,11 +7438,9 @@
},
"original_text": {
"description": "original comment content",
"type": "string"
},
"parsed_text": {
"description": "parsed comment content",
"type": "string"
"type": "string",
"maxLength": 600,
"minLength": 2
}
}
},
@ -7448,11 +7471,6 @@
"type": "string",
"maxLength": 4096
},
"bio_html": {
"description": "bio",
"type": "string",
"maxLength": 4096
},
"display_name": {
"description": "display_name",
"type": "string",
@ -7537,10 +7555,6 @@
"description": "original text",
"type": "string"
},
"parsed_text": {
"description": "parsed text",
"type": "string"
},
"slug_name": {
"description": "slug_name",
"type": "string",

View File

@ -147,9 +147,8 @@ definitions:
type: string
original_text:
description: original comment content
type: string
parsed_text:
description: parsed comment content
maxLength: 600
minLength: 2
type: string
reply_comment_id:
description: reply comment id
@ -157,7 +156,6 @@ definitions:
required:
- object_id
- original_text
- parsed_text
type: object
schema.AddReportReq:
properties:
@ -217,35 +215,30 @@ definitions:
schema.AnswerAddReq:
properties:
content:
description: content
type: string
html:
description: html
maxLength: 65535
minLength: 6
type: string
question_id:
description: question_id
type: string
required:
- content
type: object
schema.AnswerUpdateReq:
properties:
content:
description: content
maxLength: 65535
minLength: 6
type: string
edit_summary:
description: edit_summary
type: string
html:
description: html
type: string
id:
description: id
type: string
question_id:
description: question_id
type: string
title:
description: title
type: string
required:
- content
type: object
schema.AvatarInfo:
properties:
@ -976,6 +969,11 @@ definitions:
type:
type: string
type: object
schema.PostRenderReq:
properties:
content:
type: string
type: object
schema.QuestionAdd:
properties:
content:
@ -983,11 +981,6 @@ definitions:
maxLength: 65535
minLength: 6
type: string
html:
description: html
maxLength: 65535
minLength: 6
type: string
tags:
description: tags
items:
@ -1000,7 +993,6 @@ definitions:
type: string
required:
- content
- html
- tags
- title
type: object
@ -1036,6 +1028,8 @@ definitions:
type: integer
collection_count:
type: integer
created_at:
type: integer
description:
type: string
follow_count:
@ -1090,11 +1084,6 @@ definitions:
edit_summary:
description: edit summary
type: string
html:
description: html
maxLength: 65535
minLength: 6
type: string
id:
description: question id
type: string
@ -1110,7 +1099,6 @@ definitions:
type: string
required:
- content
- html
- id
- tags
- title
@ -1490,9 +1478,6 @@ definitions:
original_text:
description: original text
type: string
parsed_text:
description: parsed text
type: string
slug_name:
description: slug_name
maxLength: 35
@ -1558,12 +1543,12 @@ definitions:
type: string
original_text:
description: original comment content
type: string
parsed_text:
description: parsed comment content
maxLength: 600
minLength: 2
type: string
required:
- comment_id
- original_text
type: object
schema.UpdateFollowTagsReq:
properties:
@ -1582,10 +1567,6 @@ definitions:
description: bio
maxLength: 4096
type: string
bio_html:
description: bio
maxLength: 4096
type: string
display_name:
description: display_name
maxLength: 30
@ -1648,9 +1629,6 @@ definitions:
original_text:
description: original text
type: string
parsed_text:
description: parsed text
type: string
slug_name:
description: slug_name
maxLength: 35
@ -3684,6 +3662,30 @@ paths:
summary: user's votes
tags:
- Activity
/answer/api/v1/post/render:
post:
consumes:
- application/json
description: render post content
parameters:
- description: PostRenderReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.PostRenderReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: render post content
tags:
- Upload
/answer/api/v1/question:
delete:
consumes:

22
go.mod
View File

@ -5,6 +5,7 @@ go 1.18
require (
github.com/Chain-Zhang/pinyin v0.1.3
github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/bwmarrin/snowflake v0.3.0
github.com/davecgh/go-spew v1.1.1
github.com/disintegration/imaging v1.6.2
@ -14,7 +15,7 @@ require (
github.com/go-playground/validator/v10 v10.11.1
github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-json v0.9.11
github.com/golang/mock v1.4.4
github.com/golang/mock v1.6.0
github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0
github.com/gosimple/slug v1.13.1
@ -22,7 +23,6 @@ require (
github.com/jinzhu/copier v0.3.5
github.com/jinzhu/now v1.1.5
github.com/lib/pq v1.10.7
github.com/mattn/go-sqlite3 v1.14.16
github.com/microcosm-cc/bluemonday v1.0.21
github.com/mojocn/base64Captcha v1.3.5
github.com/ory/dockertest/v3 v3.9.1
@ -43,6 +43,7 @@ require (
golang.org/x/net v0.1.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.14.2
xorm.io/builder v0.3.12
xorm.io/core v0.7.3
xorm.io/xorm v1.3.2
@ -79,12 +80,14 @@ require (
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@ -97,6 +100,7 @@ require (
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
@ -113,6 +117,7 @@ require (
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/image v0.1.0 // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/tools v0.2.0 // indirect
@ -120,5 +125,18 @@ require (
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.35.18 // indirect
modernc.org/ccgo/v3 v3.12.82 // indirect
modernc.org/libc v1.11.87 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.0.5 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
// github action runner Sometimes it will time out.
replace gitee.com/travelliu/dm v1.8.11192 => github.com/aichy126/dm v1.8.11192
replace modernc.org/z v1.2.19 => github.com/aichy126/modernc.org_z v1.2.19

18
go.sum
View File

@ -38,7 +38,6 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -64,6 +63,7 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/aichy126/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@ -79,6 +79,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
@ -142,6 +144,7 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
@ -237,8 +240,9 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -696,6 +700,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@ -793,6 +798,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -832,6 +838,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -912,9 +919,11 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1009,6 +1018,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
@ -1219,7 +1229,9 @@ modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA=
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
@ -1271,9 +1283,11 @@ modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.8.13 h1:V0sTNBw0Re86PvXZxuCub3oO9WrSTqALgrwNZNvLFGw=
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.2.19 h1:BGyRFWhDVn5LFS5OcX4Yd/MlpRTOc7hOPTdcIpCiUao=
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=

1353
i18n/af_ZA.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/ar_SA.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/az_AZ.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/bal_BA.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/ban_ID.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/bn_BD.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/bs_BA.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/ca_ES.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/cs_CZ.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/da_DK.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -585,7 +585,8 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
@ -1222,14 +1223,14 @@ ui:
branding:
page_title: Branding
logo:
label: Logo
label: Logo (optional)
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:

1353
i18n/el_GR.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,240 +3,247 @@
backend:
base:
success:
other: "Success."
other: Success.
unknown:
other: "Unknown error."
other: Unknown error.
request_format_error:
other: "Request format is not valid."
other: Request format is not valid.
unauthorized_error:
other: "Unauthorized."
other: Unauthorized.
database_error:
other: "Data server error."
other: Data server error.
role:
name:
user:
other: "User"
other: User
admin:
other: "Admin"
other: Admin
moderator:
other: "Moderator"
other: Moderator
description:
user:
other: "Default with no special access."
other: Default with no special access.
admin:
other: "Have the full power to access the site."
other: Have the full power to access the site.
moderator:
other: "Has access to all posts except admin settings."
other: Has access to all posts except admin settings.
email:
other: "Email"
other: Email
password:
other: "Password"
email_or_password_wrong_error: &email_or_password_wrong
other: "Email and password do not match."
other: Password
email_or_password_wrong_error:
other: Email and password do not match.
error:
admin:
email_or_password_wrong: *email_or_password_wrong
email_or_password_wrong:
other: Email and password do not match.
answer:
not_found:
other: "Answer do not found."
other: Answer do not found.
cannot_deleted:
other: "No permission to delete."
other: No permission to delete.
cannot_update:
other: "No permission to update."
other: No permission to update.
comment:
edit_without_permission:
other: "Comment are not allowed to edit."
other: Comment are not allowed to edit.
not_found:
other: "Comment not found."
other: Comment not found.
cannot_edit_after_deadline:
other: The comment time has been too long to modify.
email:
duplicate:
other: "Email already exists."
other: Email already exists.
need_to_be_verified:
other: "Email should be verified."
other: Email should be verified.
verify_url_expired:
other: "Email verified URL has expired, please resend the email."
other: Email verified URL has expired, please resend the email.
lang:
not_found:
other: "Language file not found."
other: Language file not found.
object:
captcha_verification_failed:
other: "Captcha wrong."
other: Captcha wrong.
disallow_follow:
other: "You are not allowed to follow."
other: You are not allowed to follow.
disallow_vote:
other: "You are not allowed to vote."
other: You are not allowed to vote.
disallow_vote_your_self:
other: "You can't vote for your own post."
other: You can't vote for your own post.
not_found:
other: "Object not found."
other: Object not found.
verification_failed:
other: "Verification failed."
other: Verification failed.
email_or_password_incorrect:
other: "Email and password do not match."
other: Email and password do not match.
old_password_verification_failed:
other: "The old password verification failed"
other: The old password verification failed
new_password_same_as_previous_setting:
other: "The new password is the same as the previous one."
other: The new password is the same as the previous one.
question:
not_found:
other: "Question not found."
other: Question not found.
cannot_deleted:
other: "No permission to delete."
other: No permission to delete.
cannot_close:
other: "No permission to close."
other: No permission to close.
cannot_update:
other: "No permission to update."
other: No permission to update.
rank:
fail_to_meet_the_condition:
other: "Rank fail to meet the condition."
other: Rank fail to meet the condition.
report:
handle_failed:
other: "Report handle failed."
other: Report handle failed.
not_found:
other: "Report not found."
other: Report not found.
tag:
not_found:
other: "Tag not found."
other: Tag not found.
recommend_tag_not_found:
other: "Recommend Tag is not exist."
other: Recommend Tag is not exist.
recommend_tag_enter:
other: "Please enter at least one required tag."
other: Please enter at least one required tag.
not_contain_synonym_tags:
other: "Should not contain synonym tags."
other: Should not contain synonym tags.
cannot_update:
other: "No permission to update."
other: No permission to update.
cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself."
other: You cannot set the synonym of the current tag as itself.
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
other: The From Name cannot be a email address.
theme:
not_found:
other: "Theme not found."
other: Theme not found.
revision:
review_underway:
other: "Can't edit currently, there is a version in the review queue."
other: Can't edit currently, there is a version in the review queue.
no_permission:
other: "No permission to Revision."
other: No permission to Revision.
user:
email_or_password_wrong:
other: *email_or_password_wrong
other:
other: Email and password do not match.
not_found:
other: "User not found."
other: User not found.
suspended:
other: "User has been suspended."
other: User has been suspended.
username_invalid:
other: "Username is invalid."
other: Username is invalid.
username_duplicate:
other: "Username is already in use."
other: Username is already in use.
set_avatar:
other: "Avatar set failed."
other: Avatar set failed.
cannot_update_your_role:
other: "You cannot modify your role."
other: You cannot modify your role.
not_allowed_registration:
other: "Currently the site is not open for registration"
other: Currently the site is not open for registration
config:
read_config_failed:
other: "Read config failed"
other: Read config failed
database:
connection_failed:
other: "Database connection failed"
other: Database connection failed
create_table_failed:
other: "Create table failed"
other: Create table failed
install:
create_config_failed:
other: "Cant create the config.yaml file."
other: Can't create the config.yaml file.
upload:
unsupported_file_format:
other: Unsupported file format.
report:
spam:
name:
other: "spam"
other: spam
desc:
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."
other: This post is an advertisement, or vandalism. It is not useful or relevant
to the current topic.
rude:
name:
other: "rude or abusive"
other: rude or abusive
desc:
other: "A reasonable person would find this content inappropriate for respectful discourse."
other: A reasonable person would find this content inappropriate for respectful
discourse.
duplicate:
name:
other: "a duplicate"
other: a duplicate
desc:
other: "This question has been asked before and already has an answer."
other: This question has been asked before and already has an answer.
not_answer:
name:
other: "not an answer"
other: not an answer
desc:
other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether."
other: This was posted as an answer, but it does not attempt to answer the
question. It should possibly be an edit, a comment, another question,
or deleted altogether.
not_need:
name:
other: "no longer needed"
other: no longer needed
desc:
other: "This comment is outdated, conversational or not relevant to this post."
other: This comment is outdated, conversational or not relevant to this post.
other:
name:
other: "something else"
other: something else
desc:
other: "This post requires staff attention for another reason not listed above."
other: This post requires staff attention for another reason not listed above.
question:
close:
duplicate:
name:
other: "spam"
other: spam
desc:
other: "This question has been asked before and already has an answer."
other: This question has been asked before and already has an answer.
guideline:
name:
other: "a community-specific reason"
other: a community-specific reason
desc:
other: "This question doesn't meet a community guideline."
other: This question doesn't meet a community guideline.
multiple:
name:
other: "needs details or clarity"
other: needs details or clarity
desc:
other: "This question currently includes multiple questions in one. It should focus on one problem only."
other: This question currently includes multiple questions in one. It should
focus on one problem only.
other:
name:
other: "something else"
other: something else
desc:
other: "This post requires another reason not listed above."
other: This post requires another reason not listed above.
operation_type:
asked:
other: "asked"
other: asked
answered:
other: "answered"
other: answered
modified:
other: "modified"
other: modified
notification:
action:
update_question:
other: "updated question"
other: updated question
answer_the_question:
other: "answered question"
other: answered question
update_answer:
other: "updated answer"
other: updated answer
accept_answer:
other: "accepted answer"
other: accepted answer
comment_question:
other: "commented question"
other: commented question
comment_answer:
other: "commented answer"
other: commented answer
reply_to_you:
other: "replied to you"
other: replied to you
mention_you:
other: "mentioned you"
other: mentioned you
your_question_is_closed:
other: "Your question has been closed"
other: Your question has been closed
your_question_was_deleted:
other: "Your question has been deleted"
other: Your question has been deleted
your_answer_was_deleted:
other: "Your answer has been deleted"
other: Your answer has been deleted
your_comment_was_deleted:
other: "Your comment has been deleted"
other: Your comment has been deleted
# The following fields are used for interface presentation(Front-end)
ui:
@ -493,7 +500,7 @@ ui:
btn_flag: Flag
btn_save_edits: Save edits
btn_cancel: Cancel
show_more: Show more comment
show_more: Show more comments
tip_question: >-
Use comments to ask for more information or suggest improvements. Avoid
answering questions in comments.
@ -692,12 +699,12 @@ ui:
display_name:
label: Display Name
msg: Display name cannot be empty.
msg_range: Display name up to 30 characters
msg_range: Display name up to 30 characters.
username:
label: Username
caption: People can mention you as "@username".
msg: Username cannot be empty.
msg_range: Username up to 30 characters
msg_range: Username up to 30 characters.
character: 'Must use the character set "a-z", "0-9", " - . _"'
avatar:
label: Profile Image
@ -871,7 +878,7 @@ ui:
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.
link: Change settings
question:
following_tags: Following Tags
@ -933,7 +940,7 @@ ui:
title: Answer
next: Next
done: Done
config_yaml_error: Cant create the config.yaml file.
config_yaml_error: Can't create the config.yaml file.
lang:
label: Please Choose a Language
db_type:
@ -964,7 +971,7 @@ ui:
desc: >-
You can create the <1>config.yaml</1> file manually in the
<1>/var/wwww/xxx/</1> directory and paste the following text into it.
info: "After youve done that, click “Next” button."
info: After you've done that, click "Next" button.
site_information: Site Information
admin_account: Admin Account
site_name:
@ -1027,7 +1034,7 @@ ui:
desc: The server encountered an error and could not complete your request.
back_home: Back to homepage
page_maintenance:
desc: "We are under maintenance, well be back soon."
desc: "We are under maintenance, we'll be back soon."
nav_menus:
dashboard: Dashboard
contents: Contents
@ -1169,7 +1176,7 @@ ui:
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.
msg: Password must be at 8-32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
user_modal:
@ -1178,13 +1185,13 @@ ui:
fields:
display_name:
label: Display Name
msg: display_name must be at 4 - 30 characters in length.
msg: Display Name must be at 3-30 characters in length.
email:
label: Email
msg: Email is not valid.
password:
label: Password
msg: Password must be at 8 - 32 characters in length.
msg: Password must be at 8-32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
@ -1305,14 +1312,14 @@ ui:
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used.
square_icon:
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:
label: Favicon (optional)
text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used.
text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used.
legal:
page_title: Legal
terms_of_service:

View File

@ -2,7 +2,7 @@
backend:
base:
success:
other: "Success."
other: "Completado."
unknown:
other: "Error desconocido."
request_format_error:
@ -88,17 +88,17 @@ backend:
other: "Sin permiso para actualizar."
rank:
fail_to_meet_the_condition:
other: "Rank fail to meet the condition."
other: "El rango no cumple la condición."
report:
handle_failed:
other: "Report handle failed."
other: "Error en el manejador del reporte."
not_found:
other: "Report not found."
other: "Informe no encontrado."
tag:
not_found:
other: "Etiqueta no encontrada."
recommend_tag_not_found:
other: "Recommend Tag is not exist."
other: "Etiqueta recomendada no existe."
recommend_tag_enter:
other: "Por favor, introduce al menos una de las etiquetas requeridas."
not_contain_synonym_tags:
@ -109,13 +109,13 @@ backend:
other: "No se puede establecer como sinónimo de una etiqueta la propia etiqueta."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
other: "¡Su nombre de usuario no puede ser una dirección de correo electrónico!"
theme:
not_found:
other: "Tema no encontrado."
revision:
review_underway:
other: "Can't edit currently, there is a version in the review queue."
other: "No se puede editar actualmente, hay una versión en la cola de revisiones."
no_permission:
other: "Sin permiso para revisar."
user:
@ -236,9 +236,30 @@ backend:
#The following fields are used for interface presentation(Front-end)
ui:
how_to_format:
title: How to Format
title: Cómo formatear
desc: >-
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">put returns between paragraphs</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by placing <code>&gt;</code> at start of line</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">create code fences with backticks <code>`</code></p><pre class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul>
<ul class="mb-0">
<li><p class="mb-2">Para hacer enlaces</p>
<pre class="mb-2">
<code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)
</code>
</pre>
</li>
<li><p class="mb-2">Colocar saltos de línea entre párrafos</p></li>
<li><p class="mb-2"><em>_cursiva_</em> o **<strong>negrita</strong>**</p></li>
<li><p class="mb-2">Indentar el código con 4 espacios</p></li>
<li><p class="mb-2">Citar colocando <code>&gt;</code> al inicio de la línea</p></li>
<li><p class="mb-2">Escape con backticks <code>`como _esto_`</code></p></li>
<li><p class="mb-2">Crear barreras de código con backticks <code>`</code></p>
<pre class="mb-0">
<code>
```<br/>
código aquí<br/>
```
</code>
</pre>
</li>
</ul>
pagination:
prev: Anterior
next: Siguiente
@ -287,13 +308,13 @@ ui:
chart:
text: Gráfica
flow_chart: Diagrama de flujo
sequence_diagram: Sequence diagram
class_diagram: Class diagram
state_diagram: State diagram
entity_relationship_diagram: Entity relationship diagram
user_defined_diagram: User defined diagram
sequence_diagram: Diagrama de secuencia
class_diagram: Diagrama de clase
state_diagram: Diagrama de estado
entity_relationship_diagram: Diagrama de relación de entidad
user_defined_diagram: Diagrama definido por el usuario
gantt_chart: Diagrama de Gantt
pie_chart: Pie chart
pie_chart: Grafico de torta
code:
text: Código
add_code: Añadir código
@ -381,7 +402,7 @@ ui:
heading: Encabezado
cell: Celda
close_modal:
title: I am closing this post as...
title: Estoy cerrando este post como...
btn_cancel: Cancelar
btn_submit: Enviar
remark:
@ -389,11 +410,11 @@ ui:
msg:
empty: Por favor selecciona una razón.
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
flag_title: Estoy marcando este post como...
close_title: Estoy cerrando este post como...
review_question_title: Revisar pregunta
review_answer_title: Revisar respuesta
review_comment_title: Revisar comentario
btn_cancel: Cancelar
btn_submit: Enviar
remark:
@ -408,7 +429,7 @@ ui:
label: Nombre a mostrar
msg:
empty: El nombre a mostrar no puede estar vacío.
range: Display name up to 35 characters.
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", "+ # - ."'
@ -426,16 +447,16 @@ ui:
history: Historial
synonyms:
title: Sinónimos
text: The following tags will be remapped to
text: Las siguientes etiquetas serán reasignadas a
empty: No se encontraron sinónimos.
btn_add: Añadir un sinónimo
btn_edit: Editar
btn_save: Guardar
synonyms_text: The following tags will be remapped to
synonyms_text: Las siguientes etiquetas serán reasignadas a
delete:
title: Eliminar esta etiqueta
content: >-
<p>We do not allowed deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>
<p>No se permite la eliminación de etiquetas con posts.</p><p>Por favor antes elimina esta etiqueta del post.</p>
content2: '¿Estás seguro de que deseas borrarlo?'
close: Cerrar
edit_tag:
@ -508,16 +529,16 @@ ui:
tag_label: preguntas
search_placeholder: Filtrar por nombre de etiqueta
no_desc: La etiqueta no tiene descripción.
more: More
more: Mas
ask:
title: Add Question
edit_title: Edit Question
default_reason: Edit question
title: Agregar una pregunta
edit_title: Editar pregunta
default_reason: Editar pregunta
similar_questions: Preguntas similares
form:
fields:
revision:
label: Revision
label: Revisión
title:
label: Título
placeholder: Sé preciso e imagina que le estás preguntando esto a una persona
@ -585,7 +606,8 @@ ui:
msg:
empty: No puede estar en blanco.
login:
page_title: Bienvenido a Answer
page_title: Bienvenido a {{site_name}}
login_to_continue: Inicia sesión para continuar
info_sign: '¿No tienes cuenta? <1>Regístrate</1>'
info_login: '¿Ya tienes una cuenta? <1>Inicia sesión</1>'
agreements: Al registrarte, aceptas la <1>política de privacidad</1> y los <3>términos de servicio</3>.
@ -666,7 +688,7 @@ ui:
custom: Propia
btn_refresh: Actualizar
custom_text: Puedes subir tu propia imagen.
default: System
default: Sistema
msg: Por favor, sube una imagen
bio:
label: Sobre mí (opcional)
@ -693,11 +715,11 @@ ui:
msg: El correo electrónico no puede estar vacío.
password_title: Password
current_pass:
label: Current Password
label: Contraseña actual
msg:
empty: Current Password cannot be empty.
length: The length needs to be between 8 and 32.
different: The two entered passwords do not match.
empty: La contraseña actual no puede estar vacía.
length: El largo necesita estar entre 8 y 32 caracteres.
different: Las contraseñas no coinciden.
new_pass:
label: Nueva Contraseña
pass_confirm:
@ -712,7 +734,7 @@ ui:
update_password: Contraseña cambiada con éxito.
flag_success: Gracias por reportar.
forbidden_operate_self: No puedes modificar tu propio usuario
review: Your revision will show after review.
review: Tu revisión será visible luego de ser aprobada.
related_question:
title: Preguntas relacionadas
btn: Añadir pregunta
@ -726,7 +748,7 @@ ui:
Follow: Seguir
Following: Siguiendo
answered: respondida
closed_in: Closed in
closed_in: Cerrado el
show_exist: Mostrar una pregunta existente.
answers:
title: Respuestas
@ -750,9 +772,13 @@ ui:
delete:
title: Eliminar esta publicación
question: >-
We do not recommend <strong>deleting questions with answers</strong> because doing so deprives future readers of this knowledge.</p><p>Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete?
No recomendamos <strong>borrar preguntas con respuestas</strong> porque esto priva a los lectores futuros de este conocimiento.
</p><p>
El borrado repetido de preguntas respondidas puede resultar en que tu cuenta se bloquee para hacer preguntas. ¿Estás seguro de que deseas borrarlo?
answer_accepted: >-
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
<p>No recomendamos <strong>borrar la respuesta aceptada</strong> porque esto priva a los lectores futuros de este conocimiento.</p>
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
@ -782,13 +808,13 @@ ui:
newest: Más reciente
active: Activas
score: Puntuación
more: More
more: Mas
tips:
title: Advanced Search Tips
tag: "<1>[tag]</1> search withing a tag"
user: "<1>user:username</1> search by author"
answer: "<1>answers:0</1> unanswered questions"
score: "<1>score:3</1> posts with a 3+ score"
title: Consejos de búsqueda avanzada
tag: "<1>[tag]</1> búsqueda por etiquetas"
user: "<1>user:username</1> búsqueda por autor"
answer: "<1>answers:0</1> preguntas sin responder"
score: "<1>score:3</1> Publicaciones con un puntaje de 3 o más"
question: "<1>is:question</1> buscar preguntas"
is_answer: "<1>is:answer</1> buscar respuestas"
empty: No pudimos encontrar nada. <br /> Prueba a buscar con palabras diferentes o menos específicas.
@ -810,14 +836,14 @@ ui:
Lo sentimos, este link de confirmación de cuenta ya no es válido. ¿Quizás tu cuenta ya está activa?
confirm_new_email: Tu email ha sido actualizado.
confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
Lo siento, este enlace de confirmación ya no es válido. ¿Quizás ya se haya cambiado tu correo electrónico?
unsubscribe:
page_title: Desuscribir
success_title: Desuscrito con éxito
success_desc: Has sido eliminado con éxito de esta lista de suscriptores y no recibirás más correos electrónicos nuestros.
link: Cambiar ajustes
question:
following_tags: Following Tags
following_tags: Etiquetas seguidas
edit: Editar
save: Guardar
follow_tag_tip: Sigue etiquetas para personalizar tu lista de preguntas.
@ -828,8 +854,8 @@ ui:
questions: Preguntas
answers: Respuestas
newest: Más reciente
active: Active
frequent: Frequent
active: Activo
frequent: Frecuente
score: Puntuación
unanswered: Sin respuesta
modified: modificada
@ -852,8 +878,8 @@ ui:
score: Puntuación
edit_profile: Editar perfil
visited_x_days: "Visitado {{ count }} días"
viewed: Viewed
joined: Joined
viewed: Visto
joined: Unido
last_login: Seen
about_me: Sobre mí
about_me_empty: "// ¡Hola Mundo!"
@ -1222,14 +1248,14 @@ ui:
branding:
page_title: Branding
logo:
label: Logo
label: Logo (optional)
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:

1353
i18n/fi_FI.yaml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1353
i18n/he_IL.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/hu_HU.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/hy_AM.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

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

1353
i18n/id_ID.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -129,7 +129,7 @@ backend:
username_invalid:
other: "utente non valido"
username_duplicate:
other: "utente già in uso"
other: "Nome utente già in uso"
set_avatar:
other: "Inserimento dell'Avatar non riuscito."
cannot_update_your_role:
@ -585,7 +585,8 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
@ -1222,14 +1223,14 @@ ui:
branding:
page_title: Branding
logo:
label: Logo
label: Logo (optional)
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:

View File

@ -585,7 +585,8 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
@ -1222,14 +1223,14 @@ ui:
branding:
page_title: Branding
logo:
label: Logo
label: Logo (optional)
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:

View File

@ -585,7 +585,8 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
@ -1222,14 +1223,14 @@ ui:
branding:
page_title: Branding
logo:
label: Logo
label: Logo (optional)
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:

1353
i18n/nl_NL.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/no_NO.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/pl_PL.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/pt_BR.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -585,7 +585,8 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
@ -1222,14 +1223,14 @@ ui:
branding:
page_title: Branding
logo:
label: Logo
label: Logo (optional)
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:

1353
i18n/ro_RO.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,122 +2,122 @@
backend:
base:
success:
other: "Success."
other: "Выполнено."
unknown:
other: "Unknown error."
other: "Неизвестная ошибка."
request_format_error:
other: "Request format is not valid."
other: "Формат файла не корректен."
unauthorized_error:
other: "Unauthorized."
other: "Авторизация не выполнена."
database_error:
other: "Data server error."
other: "Ошибка сервера данных."
role:
name:
user:
other: "User"
other: "Пользователь"
admin:
other: "Admin"
other: "Администратор"
moderator:
other: "Moderator"
other: "Модератор"
description:
user:
other: "Default with no special access."
other: "По умолчанию, без специального доступа."
admin:
other: "Have the full power to access the site."
other: "Имейте все полномочия для доступа к сайту."
moderator:
other: "Has access to all posts except admin settings."
other: "Имеет доступ ко всем сообщениям, кроме настроек администратора."
email:
other: "Email"
other: "Эл. почта"
password:
other: "Password"
other: "Пароль"
email_or_password_wrong_error:
other: "Email and password do not match."
other: "Неверное имя пользователя или пароль."
error:
admin:
email_or_password_wrong:
other: Email and password do not match.
other: Неверное имя пользователя или пароль.
answer:
not_found:
other: "Answer do not found."
other: "Ответ не найден."
cannot_deleted:
other: "No permission to delete."
other: "Недостаточно прав для удаления."
cannot_update:
other: "No permission to update."
other: "Нет прав для обновления."
comment:
edit_without_permission:
other: "Comment are not allowed to edit."
other: "Комментарий не может редактироваться."
not_found:
other: "Comment not found."
other: "Комментарий не найден."
email:
duplicate:
other: "Email already exists."
other: "Адрес электронной почты уже существует."
need_to_be_verified:
other: "Email should be verified."
other: "Адрес электронной почты должен быть подтвержден."
verify_url_expired:
other: "Email verified URL has expired, please resend the email."
other: "Срок действия подтверждённого адреса электронной почты истек, пожалуйста, отправьте письмо повторно."
lang:
not_found:
other: "Language file not found."
other: "Языковой файл не найден."
object:
captcha_verification_failed:
other: "Captcha wrong."
other: "Captcha введена неверно."
disallow_follow:
other: "You are not allowed to follow."
other: "Вы не можете подписаться."
disallow_vote:
other: "You are not allowed to vote."
other: "Вы не можете голосовать."
disallow_vote_your_self:
other: "You can't vote for your own post."
other: "Вы не можете голосовать за собственный отзыв."
not_found:
other: "Object not found."
other: "Объект не найден."
verification_failed:
other: "Verification failed."
other: "Проверка не удалась."
email_or_password_incorrect:
other: "Email and password do not match."
other: "Email или пароль не совпадают."
old_password_verification_failed:
other: "The old password verification failed"
other: "Не удалось подтвердить старый пароль"
new_password_same_as_previous_setting:
other: "The new password is the same as the previous one."
other: "Пароль не может быть таким же как прежний."
question:
not_found:
other: "Question not found."
other: "Вопрос не найден."
cannot_deleted:
other: "No permission to delete."
other: "Недостаточно прав для удаления."
cannot_close:
other: "No permission to close."
other: "Нет разрешения на закрытие."
cannot_update:
other: "No permission to update."
other: "Нет разрешения на обновление."
rank:
fail_to_meet_the_condition:
other: "Rank fail to meet the condition."
other: "Ранг не соответствует условию."
report:
handle_failed:
other: "Report handle failed."
other: "Не удалось обработать отчет."
not_found:
other: "Report not found."
other: "Отчет не найден."
tag:
not_found:
other: "Tag not found."
other: "Тег не найден."
recommend_tag_not_found:
other: "Recommend Tag is not exist."
other: "Рекомендуемый тег не существует."
recommend_tag_enter:
other: "Please enter at least one required tag."
other: "Пожалуйста, введите хотя бы один тег."
not_contain_synonym_tags:
other: "Should not contain synonym tags."
other: "Не должно содержать теги синонимы."
cannot_update:
other: "No permission to update."
other: "Нет прав для обновления."
cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself."
other: "Вы не можете установить синоним текущего тега."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
other: "Имя пользователя не может быть адрес электронной почты."
theme:
not_found:
other: "Theme not found."
other: "Тема не найдена."
revision:
review_underway:
other: "Can't edit currently, there is a version in the review queue."
other: "В настоящее время не удается редактировать версию, в очереди на проверку."
no_permission:
other: "No permission to Revision."
other: "Нет прав на ревизию."
user:
email_or_password_wrong:
other:
@ -241,68 +241,68 @@ ui:
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">put returns between paragraphs</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by placing <code>&gt;</code> at start of line</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">create code fences with backticks <code>`</code></p><pre class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul>
pagination:
prev: Prev
next: Next
next: Следующий
page_title:
question: Question
questions: Questions
tag: Tag
tags: Tags
tag_wiki: tag wiki
edit_tag: Edit Tag
ask_a_question: Add Question
edit_question: Edit Question
edit_answer: Edit Answer
search: Search
posts_containing: Posts containing
settings: Settings
notifications: Notifications
login: Log In
sign_up: Sign Up
account_recovery: Account Recovery
account_activation: Account Activation
confirm_email: Confirm Email
account_suspended: Account Suspended
admin: Admin
change_email: Modify Email
install: Answer Installation
upgrade: Answer Upgrade
maintenance: Website Maintenance
users: Users
question: Вопрос
questions: Вопросы
tag: Тэг
tags: Теги
tag_wiki: wiki тэг
edit_tag: Изменить тег
ask_a_question: Добавить вопрос
edit_question: Редактировать вопрос
edit_answer: Редактировать ответ
search: Поиск
posts_containing: Посты содержащие
settings: Настройки
notifications: Уведомления
login: Вход
sign_up: Регистрация
account_recovery: Восстановление аккаунта
account_activation: Активация учётной записи
confirm_email: Подтвердить адрес электронной почты
account_suspended: Аккаунт заблокирован
admin: Управление
change_email: Изменить Email
install: Установка ответа
upgrade: Обновить ответ
maintenance: Обслуживание сайта
users: Пользователи
notifications:
title: Notifications
inbox: Inbox
achievement: Achievements
all_read: Mark all as read
show_more: Show more
title: Уведомления
inbox: Входящие
achievement: Достижения
all_read: Отметить всё как прочитанное
show_more: Показать еще
suspended:
title: Your Account has been Suspended
until_time: "Your account was suspended until {{ time }}."
forever: This user was suspended forever.
end: You don't meet a community guideline.
title: Ваш аккаунт заблокирован
until_time: "Ваша учетная запись была заблокирована до {{ time }}."
forever: Этот пользователь был навсегда заблокирован.
end: Вы не соответствуете правилам сообщества.
editor:
blockquote:
text: Blockquote
text: Цитата
bold:
text: Strong
text: Жирный
chart:
text: Chart
flow_chart: Flow chart
sequence_diagram: Sequence diagram
class_diagram: Class diagram
state_diagram: State diagram
entity_relationship_diagram: Entity relationship diagram
user_defined_diagram: User defined diagram
gantt_chart: Gantt chart
pie_chart: Pie chart
text: Диаграмма
flow_chart: Блок-схема
sequence_diagram: Диаграмма последовательности
class_diagram: Диаграмма классов
state_diagram: Диаграмма состояний
entity_relationship_diagram: Диаграмма связей сущностей
user_defined_diagram: Пользовательская диаграмма
gantt_chart: Диаграмма Гантта
pie_chart: Круговая диаграмма
code:
text: Code Sample
add_code: Add code sample
text: Фрагмент кода
add_code: Добавить пример кода
form:
fields:
code:
label: Code
label: Код
msg:
empty: Code cannot be empty.
empty: Код не может быть пустым.
language:
label: Language (optional)
placeholder: Automatic detection
@ -385,79 +385,79 @@ ui:
btn_cancel: Cancel
btn_submit: 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
flag_title: Я помечаю, чтобы пожаловаться на эту публикацию как...
close_title: Я закрываю этот пост как...
review_question_title: Проверить вопрос
review_answer_title: Проверить ответ
review_comment_title: Просмотр комментариев
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
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
label: Идентификатор URL
desc: 'Необходимо использовать набор символов "a-z", "0-9", "+ # - ."'
msg:
empty: URL slug cannot be empty.
range: URL slug up to 35 characters.
character: URL slug contains unallowed character set.
empty: URL не может быть пустым.
range: URL slug до 35 символов.
character: URL slug содержит недопустимый набор символов.
desc:
label: Description (optional)
btn_cancel: Cancel
btn_submit: Submit
label: Описание (опционально)
btn_cancel: Отмена
btn_submit: Отправить
tag_info:
created_at: Created
edited_at: Edited
history: History
created_at: Создано
edited_at: Отредактировано
history: История
synonyms:
title: Synonyms
text: The following tags will be remapped to
empty: No synonyms found.
btn_add: Add a synonym
btn_edit: Edit
btn_save: Save
synonyms_text: The following tags will be remapped to
title: Синонимы
text: Следующие теги будут переназначены на
empty: Синонимы не найдены.
btn_add: Добавить синоним
btn_edit: Редактировать
btn_save: Сохранить
synonyms_text: Следующие теги будут переназначены на
delete:
title: Delete this tag
title: Удалить этот тег
content: >-
<p>We do not allowed deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>
content2: Are you sure you wish to delete?
close: Close
<p>Мы не разрешаем удалять тег с сообщениями.</p><p>Пожалуйста, сначала удалите этот тег из сообщений</p>
content2: Вы действительно хотите удалить?
close: Закрыть
edit_tag:
title: Edit Tag
default_reason: Edit tag
title: Изменить тег
default_reason: Правка тега
form:
fields:
revision:
label: Revision
label: Редакция
display_name:
label: Display Name
label: Показывать имя
slug_name:
label: URL Slug
info: 'Must use the character set "a-z", "0-9", "+ # - ."'
label: Идентификатор URL
info: 'Необходимо использовать набор символов "a-z", "0-9", "+ # - ."'
desc:
label: Description
label: Описание
edit_summary:
label: Edit Summary
label: Изменить краткое описание
placeholder: >-
Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)
btn_save_edits: Save edits
btn_cancel: Cancel
Кратко опишите ваши изменения (исправленная орфография, исправленная грамматика, улучшенное форматирование)
btn_save_edits: Сохранить изменения
btn_cancel: Отмена
dates:
long_date: MMM D
long_date_with_year: "MMM D, YYYY"
@ -585,7 +585,8 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
@ -1222,14 +1223,14 @@ ui:
branding:
page_title: Branding
logo:
label: Logo
label: Logo (optional)
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:

1353
i18n/sq_AL.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/sr_SP.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/sv_SE.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/tr_TR.yaml Normal file

File diff suppressed because it is too large Load Diff

1353
i18n/uk_UA.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -585,7 +585,8 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
@ -1222,14 +1223,14 @@ ui:
branding:
page_title: Branding
logo:
label: Logo
label: Logo (optional)
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:

View File

@ -405,17 +405,17 @@ ui:
form:
fields:
display_name:
label: 显示名称(别名)
label: 显示名称
msg:
empty: 不能为空
range: 不能超过 35 个字符
empty: 显示名称不能为空
range: 显示名称不能超过 35 个字符
slug_name:
label: URL 固定链接
desc: '必须使用字符集 "a-z"、"0-9"、"+ # - ."'
desc: '必须由 "a-z", "0-9", "+ # - ." 组成'
msg:
empty: 不能为空
range: 不能超过 35 个字符
character: 包含非法字符
empty: URL 固定链接不能为空
range: URL 固定链接不能超过 35 个字符
character: URL 固定链接包含非法字符
desc:
label: 描述(可选)
btn_cancel: 取消
@ -446,7 +446,7 @@ ui:
revision:
label: 编辑历史
display_name:
label: 名称
label: 显示名称
slug_name:
label: URL 固定链接
info: '必须由 "a-z", "0-9", "+ # - ." 组成'
@ -585,6 +585,8 @@ ui:
msg:
empty: 不能为空
login:
page_title: 欢迎来到 {{site_name}}
login_to_continue: 登录以继续
info_sign: 没有账户?<1>注册</1>
info_login: 已经有一个账户?<1>登录</1>
agreements: 登录即表示您同意<1>隐私政策</1>和<3>服务条款</3>。
@ -613,6 +615,7 @@ ui:
msg:
empty: 邮箱不能为空
change_email:
page_title: 欢迎来到 {{site_name}}
btn_cancel: 取消
btn_update: 更新电子邮件地址
send_success: >-
@ -649,8 +652,8 @@ ui:
btn_name: 保存
display_name:
label: 昵称
msg: 昵称不能为空
msg_range: 昵称不能超过 30 个字符
msg: 昵称不能为空
msg_range: 昵称不能超过 30 个字符
username:
label: 用户名
caption: 用户之间可以通过 "@用户名" 进行交互。
@ -801,6 +804,7 @@ ui:
modal_confirm:
title: 发生错误...
account_result:
page_title: 欢迎来到 {{site_name}}
success: 你的账号已通过验证,即将返回首页。
link: 返回首页
invalid: >-
@ -1100,7 +1104,7 @@ ui:
fields:
display_name:
label: 昵称
msg: 昵称的长度必须是4-30个字符。
msg: 昵称的长度必须是 3-30 个字符。
email:
label: 邮箱
msg: 电子邮箱无效。
@ -1220,14 +1224,14 @@ ui:
branding:
page_title: 品牌
logo:
label: 图标
label: Logo (可选)
msg: 图标不能为空。
text: 在你的网站左上方的Logo图标。使用一个高度为56长宽比大于3:1的宽长方形图像。如果留空将显示网站标题文本。
mobile_logo:
label: 移动端图标(可选)
text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空将使用 "Logo"设置中的图像。
square_icon:
label: 方形图标
label: 方形图标 (可选)
msg: 方形图标不能为空。
text: 用作元数据图标的基础的图像。最好是大于512x512。
favicon:
@ -1245,7 +1249,7 @@ ui:
page_title: 编辑
recommend_tags:
label: 推荐标签
text: "请输入以上标签,每行一个标签。"
text: "请在上方输入标签固定链接,每行一个标签。"
required_tag:
title: 必需的标签
label: 根据需要设置推荐标签

View File

@ -10,24 +10,24 @@ backend:
unauthorized_error:
other: "未授權。"
database_error:
other: "Data server error."
other: "資料庫錯誤。"
role:
name:
user:
other: "User"
other: "使用者"
admin:
other: "Admin"
other: "管理者"
moderator:
other: "Moderator"
other: "版主"
description:
user:
other: "Default with no special access."
other: "預設沒有特別閱讀權限"
admin:
other: "Have the full power to access the site."
other: "擁有所有權限"
moderator:
other: "Has access to all posts except admin settings."
other: "可以訪問除了管理員設定以外的所有貼文"
email:
other: "Email"
other: "電子郵件"
password:
other: "密碼"
email_or_password_wrong_error:
@ -38,37 +38,37 @@ backend:
other: 電子郵箱和密碼不匹配。
answer:
not_found:
other: "Answer do not found."
other: "無答案。"
cannot_deleted:
other: "No permission to delete."
other: "無刪除權限。"
cannot_update:
other: "No permission to update."
other: "無更新權限。"
comment:
edit_without_permission:
other: "Comment are not allowed to edit."
other: "不允許編輯評論。"
not_found:
other: "Comment not found."
other: "無評論。"
email:
duplicate:
other: "Email already exists."
other: "該電子郵件已被使用"
need_to_be_verified:
other: "Email should be verified."
other: "需驗證電子郵件信箱。"
verify_url_expired:
other: "Email verified URL has expired, please resend the email."
other: "電子郵件驗證網址已過期,請重發確認郵件。"
lang:
not_found:
other: "Language file not found."
other: "無此語系檔。"
object:
captcha_verification_failed:
other: "Captcha wrong."
other: "驗證碼錯誤。"
disallow_follow:
other: "You are not allowed to follow."
other: "你不能追蹤"
disallow_vote:
other: "You are not allowed to vote."
other: "你不能投票"
disallow_vote_your_self:
other: "You can't vote for your own post."
other: "你不能為自己的貼文投票"
not_found:
other: "Object not found."
other: "找不到物件"
verification_failed:
other: "驗證失敗。"
email_or_password_incorrect:
@ -88,7 +88,7 @@ backend:
other: "沒有更新的權限。"
rank:
fail_to_meet_the_condition:
other: "Rank fail to meet the condition."
other: "無法為條件排序"
report:
handle_failed:
other: "Report handle failed."
@ -585,7 +585,8 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to Answer
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
@ -1222,14 +1223,14 @@ ui:
branding:
page_title: Branding
logo:
label: Logo
label: Logo (optional)
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
label: Square Icon (optional)
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:

View File

@ -0,0 +1,7 @@
package constant
import "time"
const (
CommentEditDeadline = time.Minute * 5
)

View File

@ -7,10 +7,10 @@ import (
"github.com/answerdev/answer/pkg/dir"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"github.com/segmentfault/pacman/cache"
"github.com/segmentfault/pacman/contrib/cache/memory"
"github.com/segmentfault/pacman/log"
_ "modernc.org/sqlite"
"xorm.io/core"
"xorm.io/xorm"
ormlog "xorm.io/xorm/log"
@ -38,11 +38,13 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) {
dataConf.Driver = string(schemas.MYSQL)
}
if dataConf.Driver == string(schemas.SQLITE) {
dataConf.Driver = "sqlite"
dbFileDir := filepath.Dir(dataConf.Connection)
log.Debugf("try to create database directory %s", dbFileDir)
if err := dir.CreateDirIfNotExist(dbFileDir); err != nil {
log.Errorf("create database dir failed: %s", err)
}
dataConf.MaxOpenConn = 1
}
engine, err := xorm.NewEngine(dataConf.Driver, dataConf.Connection)
if err != nil {

View File

@ -4,6 +4,7 @@ import (
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"strings"
@ -53,6 +54,8 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
ctx.Next()
return
}
ext := strings.ToLower(path.Ext(filePath)[1:])
ctx.Header("content-type", fmt.Sprintf("image/%s", ext))
_, err = ctx.Writer.WriteString(string(avatarfile))
if err != nil {
log.Error(err)
@ -60,6 +63,17 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
ctx.Abort()
return
} else {
uUrl, err := url.Parse(u)
if err != nil {
ctx.Next()
return
}
_, urlfileName := filepath.Split(uUrl.Path)
uploadPath := am.serviceConfig.UploadPath
filePath := fmt.Sprintf("%s/%s", uploadPath, urlfileName)
ext := strings.ToLower(path.Ext(filePath)[1:])
ctx.Header("content-type", fmt.Sprintf("image/%s", ext))
}
ctx.Next()
}

View File

@ -16,6 +16,7 @@ const (
const (
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
CommentNotFound = "error.comment.not_found"
CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline"
QuestionNotFound = "error.question.not_found"
QuestionCannotDeleted = "error.question.cannot_deleted"
QuestionCannotClose = "error.question.cannot_close"
@ -53,6 +54,7 @@ const (
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"

View File

@ -8,6 +8,7 @@ import (
"github.com/google/wire"
myTran "github.com/segmentfault/pacman/contrib/i18n"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
"gopkg.in/yaml.v3"
)
@ -68,12 +69,14 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
content, err := yaml.Marshal(translation)
if err != nil {
return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err)
log.Debugf("marshal translation content failed: %s %s", file.Name(), err)
continue
}
// add translator use backend translation
if err = myTran.AddTranslator(content, file.Name()); err != nil {
return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err)
log.Debugf("add translator failed: %s %s", file.Name(), err)
continue
}
}
GlobalTrans = myTran.GlobalTrans

View File

@ -97,8 +97,29 @@ func getTran(lo locales.Translator) ut.Translator {
return tran
}
func NotBlank(fl validator.FieldLevel) (res bool) {
field := fl.Field()
switch field.Kind() {
case reflect.String:
trimSpace := strings.TrimSpace(field.String())
res := len(trimSpace) > 0
if !res {
field.SetString(trimSpace)
}
return true
case reflect.Chan, reflect.Map, reflect.Slice, reflect.Array:
return field.Len() > 0
case reflect.Ptr, reflect.Interface, reflect.Func:
return !field.IsNil()
default:
return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface()
}
}
func createDefaultValidator(la i18n.Language) *validator.Validate {
validate := validator.New()
// _ = validate.RegisterValidation("notblank", validators.NotBlank)
_ = validate.RegisterValidation("notblank", NotBlank)
validate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) {
defer func() {
if len(res) > 0 {

View File

@ -111,6 +111,7 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentEdit, req.CommentID)
if err != nil {
handler.HandleResponse(ctx, err, nil)

View File

@ -139,6 +139,9 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) {
}
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl)
if page > 1 {
siteInfo.Canonical = fmt.Sprintf("%s/questions?page=%d", siteInfo.General.SiteUrl, page)
}
UrlUseTitle := false
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
@ -327,6 +330,9 @@ func (tc *TemplateController) TagList(ctx *gin.Context) {
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl)
if req.Page > 1 {
siteInfo.Canonical = fmt.Sprintf("%s/tags?page=%d", siteInfo.General.SiteUrl, req.Page)
}
siteInfo.Title = fmt.Sprintf("%s - %s", "Tags", siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{
"page": page,
@ -353,6 +359,9 @@ func (tc *TemplateController) TagInfo(ctx *gin.Context) {
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag)
if req.Page > 1 {
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s?page=%d", siteInfo.General.SiteUrl, tag, req.Page)
}
siteInfo.Description = htmltext.FetchExcerpt(taginifo.ParsedText, "...", 240)
if len(taginifo.ParsedText) == 0 {
siteInfo.Description = "The tag has no description."
@ -437,6 +446,7 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead
data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter
data["Version"] = constant.Version
_, ok := data["path"]
if !ok {
data["path"] = ""

View File

@ -157,8 +157,8 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
return
}
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
code, err := uc.userService.RetrievePassWord(ctx, req)
handler.HandleResponse(ctx, err, code)
_, err := uc.userService.RetrievePassWord(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UseRePassWord godoc

View File

@ -5,6 +5,10 @@ import (
"net/url"
"strings"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/pkg/checker"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm/schemas"
)
@ -82,6 +86,18 @@ type InitBaseInfoReq struct {
AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"`
}
func (r *InitBaseInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
if checker.IsInvalidUsername(r.AdminName) {
errField := &validator.FormErrorField{
ErrorField: "name",
ErrorMsg: reason.UsernameInvalid,
}
errFields = append(errFields, errField)
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
return
}
func (r *InitBaseInfoReq) FormatSiteUrl() {
parsedUrl, err := url.Parse(r.SiteURL)
if err != nil {

View File

@ -11,6 +11,7 @@ import (
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// collectionRepo collection repository
@ -29,15 +30,28 @@ func NewCollectionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) collec
// AddCollection add collection
func (cr *collectionRepo) AddCollection(ctx context.Context, collection *entity.Collection) (err error) {
id, err := cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName())
if err == nil {
collection.ID = id
_, err = cr.data.DB.Insert(collection)
_, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
var has bool
dbcollection := &entity.Collection{}
result = nil
has, err = session.Where("user_id = ? and object_id = ?", collection.UserID, collection.ObjectID).Get(dbcollection)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
}
return nil
if has {
return
}
id, err := cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName())
if err == nil {
collection.ID = id
_, err = session.Insert(collection)
if err != nil {
return result, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
}
return
})
return err
}
// RemoveCollection delete collection

View File

@ -33,7 +33,7 @@ var (
"`user_id`",
"`vote_count`",
"`answer_count`",
"0 as `accepted`",
"CASE WHEN `accepted_answer_id` > 0 THEN 2 ELSE 0 END as `accepted`",
"`question`.`status` as `status`",
"`post_update_time`",
}

View File

@ -20,10 +20,10 @@ const (
)
type AnswerAddReq struct {
QuestionID string `json:"question_id" ` // question_id
Content string `json:"content" ` // content
HTML string `json:"html" ` // html
UserID string `json:"-" ` // user_id
QuestionID string `json:"question_id"`
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
HTML string `json:"-"`
UserID string `json:"-"`
}
func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) {
@ -32,13 +32,13 @@ func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err err
}
type AnswerUpdateReq struct {
ID string `json:"id"` // id
QuestionID string `json:"question_id" ` // question_id
UserID string `json:"-" ` // user_id
Title string `json:"title" ` // title
Content string `json:"content"` // content
HTML string `json:"html" ` // html
EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary
ID string `json:"id"`
QuestionID string `json:"question_id"`
Title string `json:"title"`
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
EditSummary string `validate:"omitempty" json:"edit_summary"`
HTML string `json:"-"`
UserID string `json:"-"`
NoNeedReview bool `json:"-"`
// whether user can edit it
CanEdit bool `json:"-"`

View File

@ -14,9 +14,9 @@ type AddCommentReq struct {
// reply comment id
ReplyCommentID string `validate:"omitempty" json:"reply_comment_id"`
// original comment content
OriginalText string `validate:"required" json:"original_text"`
OriginalText string `validate:"required,notblank,gte=2,lte=600" json:"original_text"`
// parsed comment content
ParsedText string `validate:"required" json:"parsed_text"`
ParsedText string `json:"-"`
// @ user id list
MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"`
// user id
@ -47,11 +47,12 @@ type UpdateCommentReq struct {
// comment id
CommentID string `validate:"required" json:"comment_id"`
// original comment content
OriginalText string `validate:"omitempty" json:"original_text"`
OriginalText string `validate:"required,notblank,gte=2,lte=600" json:"original_text"`
// parsed comment content
ParsedText string `validate:"omitempty" json:"parsed_text"`
ParsedText string `json:"-"`
// user id
UserID string `json:"-"`
UserID string `json:"-"`
IsAdmin bool `json:"-"`
}
func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) {

View File

@ -41,11 +41,11 @@ type ReopenQuestionReq struct {
type QuestionAdd struct {
// question title
Title string `validate:"required,gte=6,lte=150" json:"title"`
Title string `validate:"required,notblank,gte=6,lte=150" json:"title"`
// content
Content string `validate:"required,gte=6,lte=65535" json:"content"`
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
// html
HTML string `validate:"required,gte=6,lte=65535" json:"html"`
HTML string `json:"-"`
// tags
Tags []*TagItem `validate:"required,dive" json:"tags"`
// user id
@ -90,11 +90,11 @@ type QuestionUpdate struct {
// question id
ID string `validate:"required" json:"id"`
// question title
Title string `validate:"required,gte=6,lte=150" json:"title"`
Title string `validate:"required,notblank,gte=6,lte=150" json:"title"`
// content
Content string `validate:"required,gte=6,lte=65535" json:"content"`
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
// html
HTML string `validate:"required,gte=6,lte=65535" json:"html"`
HTML string `json:"-"`
// tags
Tags []*TagItem `validate:"required,dive" json:"tags"`
// edit summary
@ -219,7 +219,7 @@ type UserQuestionInfo struct {
ViewCount int `json:"view_count"`
AnswerCount int `json:"answer_count"`
CollectionCount int `json:"collection_count"`
CreateTime int `json:"create_time"`
CreatedAt int64 `json:"created_at"`
AcceptedAnswerID string `json:"accepted_answer_id"`
Status string `json:"status"`
}
@ -253,6 +253,7 @@ const (
type QuestionPageResp struct {
ID string `json:"id" `
CreatedAt int64 `json:"created_at"`
Title string `json:"title"`
UrlTitle string `json:"url_title"`
Description string `json:"description"`

View File

@ -144,7 +144,7 @@ type TagItem struct {
// original text
OriginalText string `validate:"omitempty" json:"original_text"`
// parsed text
ParsedText string `validate:"omitempty" json:"parsed_text"`
ParsedText string `json:"-"`
}
// RemoveTagReq delete tag request
@ -166,7 +166,7 @@ type UpdateTagReq struct {
// original text
OriginalText string `validate:"omitempty" json:"original_text"`
// parsed text
ParsedText string `validate:"omitempty" json:"parsed_text"`
ParsedText string `json:"-"`
// edit summary
EditSummary string `validate:"omitempty" json:"edit_summary"`
// user id

View File

@ -2,12 +2,12 @@ package schema
import (
"encoding/json"
"regexp"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/converter"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
)
@ -283,7 +283,7 @@ type UpdateInfoRequest struct {
// bio
Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"`
// bio
BioHTML string `validate:"omitempty,gt=0,lte=4096" json:"bio_html"`
BioHTML string `json:"-"`
// website
Website string `validate:"omitempty,gt=0,lte=500" json:"website"`
// location
@ -298,12 +298,9 @@ type AvatarInfo struct {
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
}
func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
if len(u.Username) > 0 {
errFields := make([]*validator.FormErrorField, 0)
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
match := re.MatchString(u.Username)
if !match {
func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
if len(req.Username) > 0 {
if checker.IsInvalidUsername(req.Username) {
errField := &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: reason.UsernameInvalid,
@ -312,6 +309,7 @@ func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
}
req.BioHTML = converter.Markdown2HTML(req.Bio)
return nil, nil
}

View File

@ -121,33 +121,14 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
return nil, err
}
if objInfo.ObjectType == constant.QuestionObjectType {
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, comment.OriginalText)
} else if objInfo.ObjectType == constant.AnswerObjectType {
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
objInfo.ObjectCreatorUserID, comment.ID, req.UserID, comment.OriginalText)
}
if len(req.MentionUsernameList) > 0 {
cs.notificationMention(ctx, req.MentionUsernameList, comment.ID, req.UserID)
}
resp = &schema.GetCommentResp{}
resp.SetFromComment(comment)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
time.Now(), req.CanEdit, req.CanDelete)
// get reply user info
if len(resp.ReplyUserID) > 0 {
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
resp.ReplyUsername = replyUser.Username
resp.ReplyUserDisplayName = replyUser.DisplayName
resp.ReplyUserStatus = replyUser.Status
}
cs.notificationCommentReply(ctx, replyUser.ID, objInfo.QuestionID, req.UserID)
commentResp, err := cs.addCommentNotification(ctx, req, resp, comment, objInfo)
if err != nil {
return commentResp, err
}
// get user info
@ -178,6 +159,50 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
return resp, nil
}
func (cs *CommentService) addCommentNotification(
ctx context.Context, req *schema.AddCommentReq, resp *schema.GetCommentResp,
comment *entity.Comment, objInfo *schema.SimpleObjectInfo) (*schema.GetCommentResp, error) {
// The priority of the notification
// 1. reply to user
// 2. comment mention to user
// 3. answer or question was commented
alreadyNotifiedUserID := make(map[string]bool)
// get reply user info
if len(resp.ReplyUserID) > 0 && resp.ReplyUserID != req.UserID {
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
resp.ReplyUsername = replyUser.Username
resp.ReplyUserDisplayName = replyUser.DisplayName
resp.ReplyUserStatus = replyUser.Status
}
cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID)
alreadyNotifiedUserID[replyUser.ID] = true
return nil, nil
}
if len(req.MentionUsernameList) > 0 {
alreadyNotifiedUserIDs := cs.notificationMention(
ctx, req.MentionUsernameList, comment.ID, req.UserID, alreadyNotifiedUserID)
for _, userID := range alreadyNotifiedUserIDs {
alreadyNotifiedUserID[userID] = true
}
return nil, nil
}
if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, comment.OriginalText)
} else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
objInfo.ObjectCreatorUserID, comment.ID, req.UserID, comment.OriginalText)
}
return nil, nil
}
// RemoveComment delete comment
func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) {
return cs.commentRepo.RemoveComment(ctx, req.CommentID)
@ -185,6 +210,19 @@ func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveC
// UpdateComment update comment
func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (err error) {
old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
if err != nil {
return
}
if !exist {
return errors.BadRequest(reason.CommentNotFound)
}
// user can edit the comment that was posted by himself before deadline.
if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) {
return errors.BadRequest(reason.CommentCannotEditAfterDeadline)
}
comment := &entity.Comment{}
_ = copier.Copy(comment, req)
comment.ID = req.CommentID
@ -198,7 +236,7 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
return
}
if !exist {
return nil, errors.BadRequest(reason.UnknownError)
return nil, errors.BadRequest(reason.CommentNotFound)
}
resp = &schema.GetCommentResp{
@ -243,7 +281,8 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
// check if current user vote this comment
resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
comment.CreatedAt, req.CanEdit, req.CanDelete)
return resp, nil
}
@ -339,7 +378,7 @@ func (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *sc
commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID)
commentResp.MemberActions = permission.GetCommentPermission(ctx,
req.UserID, commentResp.UserID, req.CanEdit, req.CanDelete)
req.UserID, commentResp.UserID, comment.CreatedAt, req.CanEdit, req.CanDelete)
return commentResp, nil
}
@ -521,14 +560,16 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
notice_queue.AddNotification(msg)
}
func (cs *CommentService) notificationMention(ctx context.Context, mentionUsernameList []string, commentID, commentUserID string) {
func (cs *CommentService) notificationMention(
ctx context.Context, mentionUsernameList []string, commentID, commentUserID string,
alreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) {
for _, username := range mentionUsernameList {
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username)
if err != nil {
log.Error(err)
continue
}
if exist {
if exist && !alreadyNotifiedUserID[userInfo.ID] {
msg := &schema.NotificationMsg{
ReceiverUserID: userInfo.ID,
TriggerUserID: commentUserID,
@ -538,6 +579,8 @@ func (cs *CommentService) notificationMention(ctx context.Context, mentionUserna
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.MentionYou
notice_queue.AddNotification(msg)
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
}
}
return alreadyNotifiedUserIDs
}

View File

@ -2,13 +2,15 @@ package permission
import (
"context"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/schema"
)
// GetCommentPermission get comment permission
func GetCommentPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) (
actions []*schema.PermissionMemberAction) {
func GetCommentPermission(ctx context.Context, userID string, creatorUserID string,
createdAt time.Time, canEdit, canDelete bool) (actions []*schema.PermissionMemberAction) {
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
actions = append(actions, &schema.PermissionMemberAction{
@ -17,7 +19,8 @@ func GetCommentPermission(ctx context.Context, userID string, creatorUserID stri
Type: "reason",
})
}
if canEdit || userID == creatorUserID {
deadline := createdAt.Add(constant.CommentEditDeadline)
if canEdit || (userID == creatorUserID && time.Now().Before(deadline)) {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",

View File

@ -254,6 +254,7 @@ func (qs *QuestionCommon) FormatQuestionsPage(
for _, questionInfo := range questionList {
t := &schema.QuestionPageResp{
ID: questionInfo.ID,
CreatedAt: questionInfo.CreatedAt.Unix(),
Title: questionInfo.Title,
UrlTitle: htmltext.UrlTitle(questionInfo.Title),
Description: htmltext.FetchExcerpt(questionInfo.ParsedText, "...", 240),

View File

@ -11,10 +11,10 @@ import (
"path/filepath"
"strings"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/dir"
"github.com/answerdev/answer/pkg/uid"
"github.com/disintegration/imaging"
@ -40,10 +40,10 @@ var (
".jpg": imaging.JPEG,
".jpeg": imaging.JPEG,
".png": imaging.PNG,
".gif": imaging.GIF,
".tif": imaging.TIFF,
".tiff": imaging.TIFF,
".bmp": imaging.BMP,
//".gif": imaging.GIF,
//".tif": imaging.TIFF,
//".tiff": imaging.TIFF,
//".bmp": imaging.BMP,
}
)
@ -74,13 +74,11 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err e
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
@ -147,13 +145,11 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
@ -167,14 +163,12 @@ func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
fileExt := strings.ToLower(path.Ext(file.Filename))
_, ok := FormatExts[fileExt]
if !ok && fileExt != ".ico" {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
@ -192,6 +186,17 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead
if err := ctx.SaveUploadedFile(file, filePath); err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
src, err := file.Open()
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
defer src.Close()
if !checker.IsSupportedImageFile(src, filepath.Ext(fileSubPath)) {
return "", errors.BadRequest(reason.UploadFileUnsupportedFileFormat)
}
url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath)
return url, nil
}

View File

@ -4,7 +4,6 @@ import (
"context"
"encoding/hex"
"math/rand"
"regexp"
"strings"
"github.com/Chain-Zhang/pinyin"
@ -120,9 +119,7 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use
username = strings.ToLower(username)
suffix := ""
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
match := re.MatchString(username)
if !match {
if checker.IsInvalidUsername(username) {
return "", errors.BadRequest(reason.UsernameInvalid)
}

29
pkg/checker/file_type.go Normal file
View File

@ -0,0 +1,29 @@
package checker
import (
"image/jpeg"
"image/png"
"io"
"strings"
)
// IsSupportedImageFile currently answers support image type is `image/jpeg,image/jpg,image/png`
func IsSupportedImageFile(file io.Reader, ext string) bool {
ext = strings.TrimPrefix(ext, ".")
var err error
switch strings.ToUpper(ext) {
case "JPG", "JPEG":
_, err = jpeg.Decode(file)
case "PNG":
_, err = png.Decode(file)
case "ICO":
// TODO: There is currently no good Golang library to parse whether the image is in ico format.
return true
default:
return false
}
if err != nil {
return false
}
return true
}

11
pkg/checker/username.go Normal file
View File

@ -0,0 +1,11 @@
package checker
import "regexp"
var (
usernameReg = regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
)
func IsInvalidUsername(username string) bool {
return !usernameReg.MatchString(username)
}

View File

@ -3,6 +3,7 @@ package converter
import (
"bytes"
"github.com/asaskevich/govalidator"
"github.com/microcosm-cc/bluemonday"
"github.com/segmentfault/pacman/log"
"github.com/yuin/goldmark"
@ -10,6 +11,7 @@ import (
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
goldmarkHTML "github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
@ -54,6 +56,9 @@ type DangerousHTMLRenderer struct {
func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
reg.Register(ast.KindRawHTML, r.renderRawHTML)
reg.Register(ast.KindLink, r.renderLink)
reg.Register(ast.KindAutoLink, r.renderAutoLink)
}
func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
@ -85,3 +90,56 @@ func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte,
}
return ast.WalkContinue, nil
}
func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Link)
if entering && r.renderLinkIsUrl(string(n.Destination)) {
_, _ = w.WriteString("<a href=\"")
if r.Unsafe || !html.IsDangerousURL(n.Destination) {
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
}
_ = w.WriteByte('"')
if n.Title != nil {
_, _ = w.WriteString(` title="`)
r.Writer.Write(w, n.Title)
_ = w.WriteByte('"')
}
if n.Attributes() != nil {
html.RenderAttributes(w, n, html.LinkAttributeFilter)
}
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("</a>")
}
return ast.WalkContinue, nil
}
func (r *DangerousHTMLRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.AutoLink)
if !entering || !r.renderLinkIsUrl(string(n.URL(source))) {
return ast.WalkContinue, nil
}
_, _ = w.WriteString(`<a href="`)
url := n.URL(source)
label := n.Label(source)
if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
_, _ = w.WriteString("mailto:")
}
_, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))
if n.Attributes() != nil {
_ = w.WriteByte('"')
html.RenderAttributes(w, n, html.LinkAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString(`">`)
}
_, _ = w.Write(util.EscapeHTML(label))
_, _ = w.WriteString(`</a>`)
return ast.WalkContinue, nil
}
func (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyUrl string) bool {
return govalidator.IsURL(verifyUrl)
}

6
ui/.gitignore vendored
View File

@ -9,7 +9,11 @@
/coverage
# production
/build
/build/*/*/*
/build/*.json
/build/*.html
/build/*.txt
# misc
.DS_Store

BIN
ui/build/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1 +0,0 @@
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.cb9bf782.js"></script><link href="/static/css/main.b8d8739f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask"><noscript><style>#spin-mask{display:none!important}</style></noscript><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>

View File

@ -2,7 +2,6 @@ import React, { FormEvent, useState, useEffect } from 'react';
import { Form, Button, Stack, ButtonGroup } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next';
import { marked } from 'marked';
import MD5 from 'md5';
import type { FormDataType } from '@/common/interface';
@ -205,7 +204,6 @@ const Index: React.FC = () => {
bio: formData.bio.value,
website: formData.website.value,
location: formData.location.value,
bio_html: marked.parse(formData.bio.value),
};
modifyUserInfo(params)

View File

@ -6,9 +6,9 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{{.title}}</title>
<meta name="description" content="{{.description}}" data-rh="true" />
{{if .keywords }}
<meta name="keywords" content="{{.keywords}}" data-rh="true" />
{{end}}
<meta name="generator" content="Answer {{.Version}} - https://github.com/answerdev/answer">
{{if .keywords }}<meta name="keywords" content="{{.keywords}}" data-rh="true" />{{end}}
<link rel="canonical" href="{{.siteinfo.Canonical}}" />
<link rel="manifest" href="/manifest.json" />
<link href="{{.cssPath}}" rel="stylesheet" />